Compare commits

..

22 Commits

Author SHA1 Message Date
Tim
15ad85e6f1 doc: add OpenAPI annotations to remaining controllers 2025-09-09 16:37:08 +08:00
Tim
1e0f62b421 fix: 正式环境/预发环境切换为英文 2025-09-09 15:40:55 +08:00
Tim
62cccb794d Merge pull request #953 from nagisa77/codex/fix-compilation-errors-in-postservice
Fix PostServiceTest constructor with RedisTemplate mock
2025-09-09 15:32:22 +08:00
Tim
afa0c7fb8f test: update PostServiceTest for redis template 2025-09-09 15:32:03 +08:00
Tim
1852f87341 Merge pull request #951 from nagisa77/codex/update-openapi-servers-configuration
feat: allow configuring multiple OpenAPI servers
2025-09-09 15:03:43 +08:00
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
50 changed files with 745 additions and 34 deletions

View File

@@ -4,6 +4,8 @@
高效的开源社区前后端平台
<br><br><br>
<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>
## 💡 简介

View File

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

View File

@@ -8,13 +8,18 @@ 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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@RequiredArgsConstructor
public class OpenApiConfig {
private final SpringDocProperties springDocProperties;
@Value("${springdoc.info.title}")
private String title;
@@ -30,20 +35,21 @@ public class OpenApiConfig {
@Value("${springdoc.info.header}")
private String header;
@Value("${springdoc.api-docs.server-url}")
private String serverUrl;
@Bean
public OpenAPI openAPI() {
SecurityScheme securityScheme = new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme(scheme.toLowerCase())
.bearerFormat("JWT")
.in(SecurityScheme.In.HEADER)
.name(header);
.type(SecurityScheme.Type.HTTP)
.scheme(scheme.toLowerCase())
.bearerFormat("JWT")
.in(SecurityScheme.In.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()
.servers(List.of(new Server().url(serverUrl)))
.servers(servers)
.info(new Info()
.title(title)
.description(description)

View File

@@ -90,6 +90,9 @@ public class SecurityConfig {
"http://192.168.7.98",
"http://192.168.7.98:3000",
"https://petstore.swagger.io",
// 允许自建OpenAPI地址
"https://docs.open-isle.com",
"https://www.docs.open-isle.com",
websiteUrl,
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

@@ -12,6 +12,12 @@ import com.openisle.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.List;
import java.util.stream.Collectors;
@@ -25,6 +31,9 @@ public class ActivityController {
private final ActivityMapper activityMapper;
@GetMapping
@Operation(summary = "List activities", description = "Retrieve all activities")
@ApiResponse(responseCode = "200", description = "List of activities",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ActivityDto.class))))
public List<ActivityDto> list() {
return activityService.list().stream()
.map(activityMapper::toDto)
@@ -32,6 +41,9 @@ public class ActivityController {
}
@GetMapping("/milk-tea")
@Operation(summary = "Milk tea info", description = "Get milk tea activity information")
@ApiResponse(responseCode = "200", description = "Milk tea info",
content = @Content(schema = @Schema(implementation = MilkTeaInfoDto.class)))
public MilkTeaInfoDto milkTea() {
Activity a = activityService.getByType(ActivityType.MILK_TEA);
long count = activityService.countParticipants(a);
@@ -45,6 +57,10 @@ public class ActivityController {
}
@PostMapping("/milk-tea/redeem")
@Operation(summary = "Redeem milk tea", description = "Redeem milk tea activity reward")
@ApiResponse(responseCode = "200", description = "Redeem result",
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
@SecurityRequirement(name = "JWT")
public java.util.Map<String, String> redeemMilkTea(@RequestBody MilkTeaRedeemRequest req, Authentication auth) {
User user = userService.findByIdentifier(auth.getName()).orElseThrow();
Activity a = activityService.getByType(ActivityType.MILK_TEA);

View File

@@ -3,6 +3,11 @@ package com.openisle.controller;
import com.openisle.dto.CommentDto;
import com.openisle.mapper.CommentMapper;
import com.openisle.service.CommentService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
@@ -18,11 +23,19 @@ public class AdminCommentController {
private final CommentMapper commentMapper;
@PostMapping("/{id}/pin")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Pin comment", description = "Pin a comment by its id")
@ApiResponse(responseCode = "200", description = "Pinned comment",
content = @Content(schema = @Schema(implementation = CommentDto.class)))
public CommentDto pin(@PathVariable Long id, Authentication auth) {
return commentMapper.toDto(commentService.pinComment(auth.getName(), id));
}
@PostMapping("/{id}/unpin")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Unpin comment", description = "Remove pin from a comment")
@ApiResponse(responseCode = "200", description = "Unpinned comment",
content = @Content(schema = @Schema(implementation = CommentDto.class)))
public CommentDto unpin(@PathVariable Long id, Authentication auth) {
return commentMapper.toDto(commentService.unpinComment(auth.getName(), id));
}

View File

@@ -5,6 +5,11 @@ import com.openisle.service.AiUsageService;
import com.openisle.service.PasswordValidator;
import com.openisle.service.PostService;
import com.openisle.service.RegisterModeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@@ -18,6 +23,10 @@ public class AdminConfigController {
private final RegisterModeService registerModeService;
@GetMapping
@SecurityRequirement(name = "JWT")
@Operation(summary = "Get configuration", description = "Retrieve application configuration settings")
@ApiResponse(responseCode = "200", description = "Current configuration",
content = @Content(schema = @Schema(implementation = ConfigDto.class)))
public ConfigDto getConfig() {
ConfigDto dto = new ConfigDto();
dto.setPublishMode(postService.getPublishMode());
@@ -28,6 +37,10 @@ public class AdminConfigController {
}
@PostMapping
@SecurityRequirement(name = "JWT")
@Operation(summary = "Update configuration", description = "Update application configuration settings")
@ApiResponse(responseCode = "200", description = "Updated configuration",
content = @Content(schema = @Schema(implementation = ConfigDto.class)))
public ConfigDto updateConfig(@RequestBody ConfigDto dto) {
if (dto.getPublishMode() != null) {
postService.setPublishMode(dto.getPublishMode());

View File

@@ -1,5 +1,10 @@
package com.openisle.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@@ -10,6 +15,10 @@ import java.util.Map;
@RestController
public class AdminController {
@GetMapping("/api/admin/hello")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Admin greeting", description = "Returns a greeting for admin users")
@ApiResponse(responseCode = "200", description = "Greeting payload",
content = @Content(schema = @Schema(implementation = Map.class)))
public Map<String, String> adminHello() {
return Map.of("message", "Hello, Admin User");
}

View File

@@ -3,6 +3,12 @@ package com.openisle.controller;
import com.openisle.dto.PostSummaryDto;
import com.openisle.mapper.PostMapper;
import com.openisle.service.PostService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@@ -20,6 +26,10 @@ public class AdminPostController {
private final PostMapper postMapper;
@GetMapping("/pending")
@SecurityRequirement(name = "JWT")
@Operation(summary = "List pending posts", description = "Retrieve posts awaiting approval")
@ApiResponse(responseCode = "200", description = "Pending posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
public List<PostSummaryDto> pendingPosts() {
return postService.listPendingPosts().stream()
.map(postMapper::toSummaryDto)
@@ -27,31 +37,55 @@ public class AdminPostController {
}
@PostMapping("/{id}/approve")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Approve post", description = "Approve a pending post")
@ApiResponse(responseCode = "200", description = "Approved post",
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
public PostSummaryDto approve(@PathVariable Long id) {
return postMapper.toSummaryDto(postService.approvePost(id));
}
@PostMapping("/{id}/reject")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Reject post", description = "Reject a pending post")
@ApiResponse(responseCode = "200", description = "Rejected post",
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
public PostSummaryDto reject(@PathVariable Long id) {
return postMapper.toSummaryDto(postService.rejectPost(id));
}
@PostMapping("/{id}/pin")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Pin post", description = "Pin a post to the top")
@ApiResponse(responseCode = "200", description = "Pinned post",
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
public PostSummaryDto pin(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
return postMapper.toSummaryDto(postService.pinPost(id, auth.getName()));
}
@PostMapping("/{id}/unpin")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Unpin post", description = "Remove a post from the top")
@ApiResponse(responseCode = "200", description = "Unpinned post",
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
public PostSummaryDto unpin(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
return postMapper.toSummaryDto(postService.unpinPost(id, auth.getName()));
}
@PostMapping("/{id}/rss-exclude")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Exclude from RSS", description = "Exclude a post from RSS feed")
@ApiResponse(responseCode = "200", description = "Updated post",
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
public PostSummaryDto excludeFromRss(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
return postMapper.toSummaryDto(postService.excludeFromRss(id, auth.getName()));
}
@PostMapping("/{id}/rss-include")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Include in RSS", description = "Include a post in the RSS feed")
@ApiResponse(responseCode = "200", description = "Updated post",
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
public PostSummaryDto includeInRss(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
return postMapper.toSummaryDto(postService.includeInRss(id, auth.getName()));
}

View File

@@ -5,6 +5,12 @@ import com.openisle.mapper.TagMapper;
import com.openisle.model.Tag;
import com.openisle.service.PostService;
import com.openisle.service.TagService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@@ -20,6 +26,10 @@ public class AdminTagController {
private final TagMapper tagMapper;
@GetMapping("/pending")
@SecurityRequirement(name = "JWT")
@Operation(summary = "List pending tags", description = "Retrieve tags awaiting approval")
@ApiResponse(responseCode = "200", description = "Pending tags",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class))))
public List<TagDto> pendingTags() {
return tagService.listPendingTags().stream()
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
@@ -27,6 +37,10 @@ public class AdminTagController {
}
@PostMapping("/{id}/approve")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Approve tag", description = "Approve a pending tag")
@ApiResponse(responseCode = "200", description = "Approved tag",
content = @Content(schema = @Schema(implementation = TagDto.class)))
public TagDto approve(@PathVariable Long id) {
Tag tag = tagService.approveTag(id);
long count = postService.countPostsByTag(tag.getId());

View File

@@ -6,6 +6,9 @@ import com.openisle.model.User;
import com.openisle.service.EmailSender;
import com.openisle.repository.NotificationRepository;
import com.openisle.repository.UserRepository;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
@@ -22,6 +25,9 @@ public class AdminUserController {
private String websiteUrl;
@PostMapping("/{id}/approve")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Approve user", description = "Approve a pending user registration")
@ApiResponse(responseCode = "200", description = "User approved")
public ResponseEntity<?> approve(@PathVariable Long id) {
User user = userRepository.findById(id).orElseThrow();
user.setApproved(true);
@@ -33,6 +39,9 @@ public class AdminUserController {
}
@PostMapping("/{id}/reject")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Reject user", description = "Reject a pending user registration")
@ApiResponse(responseCode = "200", description = "User rejected")
public ResponseEntity<?> reject(@PathVariable Long id) {
User user = userRepository.findById(id).orElseThrow();
user.setApproved(false);

View File

@@ -9,6 +9,11 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.Map;
@@ -21,6 +26,10 @@ public class AiController {
private final AiUsageService aiUsageService;
@PostMapping("/format")
@Operation(summary = "Format markdown", description = "Format text via AI")
@ApiResponse(responseCode = "200", description = "Formatted content",
content = @Content(schema = @Schema(implementation = Map.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<Map<String, String>> format(@RequestBody Map<String, String> req,
Authentication auth) {
String text = req.get("text");

View File

@@ -8,6 +8,11 @@ import com.openisle.model.User;
import com.openisle.repository.UserRepository;
import com.openisle.service.*;
import com.openisle.util.VerifyType;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
@@ -47,6 +52,9 @@ public class AuthController {
private boolean loginCaptchaEnabled;
@PostMapping("/register")
@Operation(summary = "Register user", description = "Register a new user account")
@ApiResponse(responseCode = "200", description = "Registration result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> register(@RequestBody RegisterRequest req) {
if (captchaEnabled && registerCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha"));
@@ -84,6 +92,9 @@ public class AuthController {
}
@PostMapping("/verify")
@Operation(summary = "Verify account", description = "Verify registration code")
@ApiResponse(responseCode = "200", description = "Verification result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> verify(@RequestBody VerifyRequest req) {
Optional<User> userOpt = userService.findByUsername(req.getUsername());
if (userOpt.isEmpty()) {
@@ -111,6 +122,9 @@ public class AuthController {
}
@PostMapping("/login")
@Operation(summary = "Login", description = "Authenticate with username/email and password")
@ApiResponse(responseCode = "200", description = "Authentication result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> login(@RequestBody LoginRequest req) {
if (captchaEnabled && loginCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha"));
@@ -149,6 +163,9 @@ public class AuthController {
}
@PostMapping("/google")
@Operation(summary = "Login with Google", description = "Authenticate using Google account")
@ApiResponse(responseCode = "200", description = "Authentication result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> loginWithGoogle(@RequestBody GoogleLoginRequest req) {
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
@@ -196,6 +213,9 @@ public class AuthController {
@PostMapping("/reason")
@Operation(summary = "Submit register reason", description = "Submit registration reason for approval")
@ApiResponse(responseCode = "200", description = "Submission result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> reason(@RequestBody MakeReasonRequest req) {
String username = jwtService.validateAndGetSubjectForReason(req.getToken());
Optional<User> userOpt = userService.findByUsername(username);
@@ -224,6 +244,9 @@ public class AuthController {
}
@PostMapping("/github")
@Operation(summary = "Login with GitHub", description = "Authenticate using GitHub account")
@ApiResponse(responseCode = "200", description = "Authentication result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> loginWithGithub(@RequestBody GithubLoginRequest req) {
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
@@ -272,6 +295,9 @@ public class AuthController {
}
@PostMapping("/discord")
@Operation(summary = "Login with Discord", description = "Authenticate using Discord account")
@ApiResponse(responseCode = "200", description = "Authentication result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> loginWithDiscord(@RequestBody DiscordLoginRequest req) {
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
@@ -319,6 +345,9 @@ public class AuthController {
}
@PostMapping("/twitter")
@Operation(summary = "Login with Twitter", description = "Authenticate using Twitter account")
@ApiResponse(responseCode = "200", description = "Authentication result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> loginWithTwitter(@RequestBody TwitterLoginRequest req) {
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
@@ -367,6 +396,9 @@ public class AuthController {
}
@PostMapping("/telegram")
@Operation(summary = "Login with Telegram", description = "Authenticate using Telegram data")
@ApiResponse(responseCode = "200", description = "Authentication result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> loginWithTelegram(@RequestBody TelegramLoginRequest req) {
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
@@ -412,11 +444,18 @@ public class AuthController {
}
@GetMapping("/check")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Check token", description = "Validate JWT token")
@ApiResponse(responseCode = "200", description = "Token valid",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> checkToken() {
return ResponseEntity.ok(Map.of("valid", true));
}
@PostMapping("/forgot/send")
@Operation(summary = "Send reset code", description = "Send verification code for password reset")
@ApiResponse(responseCode = "200", description = "Sending result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> sendReset(@RequestBody ForgotPasswordRequest req) {
Optional<User> userOpt = userService.findByEmail(req.getEmail());
if (userOpt.isEmpty()) {
@@ -427,6 +466,9 @@ public class AuthController {
}
@PostMapping("/forgot/verify")
@Operation(summary = "Verify reset code", description = "Verify password reset code")
@ApiResponse(responseCode = "200", description = "Verification result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> verifyReset(@RequestBody VerifyForgotRequest req) {
Optional<User> userOpt = userService.findByEmail(req.getEmail());
if (userOpt.isEmpty()) {
@@ -441,6 +483,9 @@ public class AuthController {
}
@PostMapping("/forgot/reset")
@Operation(summary = "Reset password", description = "Reset user password after verification")
@ApiResponse(responseCode = "200", description = "Reset result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> resetPassword(@RequestBody ResetPasswordRequest req) {
String username = jwtService.validateAndGetSubjectForReset(req.getToken());
try {

View File

@@ -10,6 +10,11 @@ import com.openisle.service.CategoryService;
import com.openisle.service.PostService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.util.List;
import java.util.Map;
@@ -25,6 +30,9 @@ public class CategoryController {
private final CategoryMapper categoryMapper;
@PostMapping
@Operation(summary = "Create category", description = "Create a new category")
@ApiResponse(responseCode = "200", description = "Created category",
content = @Content(schema = @Schema(implementation = CategoryDto.class)))
public CategoryDto create(@RequestBody CategoryRequest req) {
Category c = categoryService.createCategory(req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
long count = postService.countPostsByCategory(c.getId());
@@ -32,6 +40,9 @@ public class CategoryController {
}
@PutMapping("/{id}")
@Operation(summary = "Update category", description = "Update an existing category")
@ApiResponse(responseCode = "200", description = "Updated category",
content = @Content(schema = @Schema(implementation = CategoryDto.class)))
public CategoryDto update(@PathVariable Long id, @RequestBody CategoryRequest req) {
Category c = categoryService.updateCategory(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
long count = postService.countPostsByCategory(c.getId());
@@ -39,11 +50,16 @@ public class CategoryController {
}
@DeleteMapping("/{id}")
@Operation(summary = "Delete category", description = "Remove a category by id")
@ApiResponse(responseCode = "200", description = "Category deleted")
public void delete(@PathVariable Long id) {
categoryService.deleteCategory(id);
}
@GetMapping
@Operation(summary = "List categories", description = "Get all categories")
@ApiResponse(responseCode = "200", description = "List of categories",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = CategoryDto.class))))
public List<CategoryDto> list() {
List<Category> all = categoryService.listCategories();
List<Long> ids = all.stream().map(Category::getId).toList();
@@ -55,6 +71,9 @@ public class CategoryController {
}
@GetMapping("/{id}")
@Operation(summary = "Get category", description = "Get category by id")
@ApiResponse(responseCode = "200", description = "Category detail",
content = @Content(schema = @Schema(implementation = CategoryDto.class)))
public CategoryDto get(@PathVariable Long id) {
Category c = categoryService.getCategory(id);
long count = postService.countPostsByCategory(c.getId());
@@ -62,6 +81,9 @@ public class CategoryController {
}
@GetMapping("/{id}/posts")
@Operation(summary = "List posts by category", description = "Get posts under a category")
@ApiResponse(responseCode = "200", description = "List of posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
public List<PostSummaryDto> listPostsByCategory(@PathVariable Long id,
@RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "pageSize", required = false) Integer pageSize) {

View File

@@ -8,6 +8,12 @@ import com.openisle.service.MessageService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.List;
@@ -26,16 +32,28 @@ public class ChannelController {
}
@GetMapping
@Operation(summary = "List channels", description = "List channels for the current user")
@ApiResponse(responseCode = "200", description = "Channels",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ChannelDto.class))))
@SecurityRequirement(name = "JWT")
public List<ChannelDto> listChannels(Authentication auth) {
return channelService.listChannels(getCurrentUserId(auth));
}
@PostMapping("/{channelId}/join")
@Operation(summary = "Join channel", description = "Join a channel")
@ApiResponse(responseCode = "200", description = "Joined channel",
content = @Content(schema = @Schema(implementation = ChannelDto.class)))
@SecurityRequirement(name = "JWT")
public ChannelDto joinChannel(@PathVariable Long channelId, Authentication auth) {
return channelService.joinChannel(channelId, getCurrentUserId(auth));
}
@GetMapping("/unread-count")
@Operation(summary = "Unread count", description = "Get unread channel count")
@ApiResponse(responseCode = "200", description = "Unread count",
content = @Content(schema = @Schema(implementation = Long.class)))
@SecurityRequirement(name = "JWT")
public long unreadCount(Authentication auth) {
return messageService.getUnreadChannelCount(getCurrentUserId(auth));
}

View File

@@ -14,6 +14,12 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.List;
import java.util.stream.Collectors;
@@ -36,6 +42,10 @@ public class CommentController {
private boolean commentCaptchaEnabled;
@PostMapping("/posts/{postId}/comments")
@Operation(summary = "Create comment", description = "Add a comment to a post")
@ApiResponse(responseCode = "200", description = "Created comment",
content = @Content(schema = @Schema(implementation = CommentDto.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<CommentDto> createComment(@PathVariable Long postId,
@RequestBody CommentRequest req,
Authentication auth) {
@@ -53,6 +63,10 @@ public class CommentController {
}
@PostMapping("/comments/{commentId}/replies")
@Operation(summary = "Reply to comment", description = "Reply to an existing comment")
@ApiResponse(responseCode = "200", description = "Reply created",
content = @Content(schema = @Schema(implementation = CommentDto.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<CommentDto> replyComment(@PathVariable Long commentId,
@RequestBody CommentRequest req,
Authentication auth) {
@@ -69,6 +83,9 @@ public class CommentController {
}
@GetMapping("/posts/{postId}/comments")
@Operation(summary = "List comments", description = "List comments for a post")
@ApiResponse(responseCode = "200", description = "Comments",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = CommentDto.class))))
public List<CommentDto> listComments(@PathVariable Long postId,
@RequestParam(value = "sort", required = false, defaultValue = "OLDEST") com.openisle.model.CommentSort sort) {
log.debug("listComments called for post {} with sort {}", postId, sort);
@@ -80,6 +97,9 @@ public class CommentController {
}
@DeleteMapping("/comments/{id}")
@Operation(summary = "Delete comment", description = "Delete a comment")
@ApiResponse(responseCode = "200", description = "Deleted")
@SecurityRequirement(name = "JWT")
public void deleteComment(@PathVariable Long id, Authentication auth) {
log.debug("deleteComment called by user {} for comment {}", auth.getName(), id);
commentService.deleteComment(auth.getName(), id);
@@ -87,12 +107,20 @@ public class CommentController {
}
@PostMapping("/comments/{id}/pin")
@Operation(summary = "Pin comment", description = "Pin a comment")
@ApiResponse(responseCode = "200", description = "Pinned comment",
content = @Content(schema = @Schema(implementation = CommentDto.class)))
@SecurityRequirement(name = "JWT")
public CommentDto pinComment(@PathVariable Long id, Authentication auth) {
log.debug("pinComment called by user {} for comment {}", auth.getName(), id);
return commentMapper.toDto(commentService.pinComment(auth.getName(), id));
}
@PostMapping("/comments/{id}/unpin")
@Operation(summary = "Unpin comment", description = "Unpin a comment")
@ApiResponse(responseCode = "200", description = "Unpinned comment",
content = @Content(schema = @Schema(implementation = CommentDto.class)))
@SecurityRequirement(name = "JWT")
public CommentDto unpinComment(@PathVariable Long id, Authentication auth) {
log.debug("unpinComment called by user {} for comment {}", auth.getName(), id);
return commentMapper.toDto(commentService.unpinComment(auth.getName(), id));

View File

@@ -6,6 +6,10 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@RestController
@RequestMapping("/api")
@@ -33,6 +37,9 @@ public class ConfigController {
private final RegisterModeService registerModeService;
@GetMapping("/config")
@Operation(summary = "Site config", description = "Get site configuration")
@ApiResponse(responseCode = "200", description = "Site configuration",
content = @Content(schema = @Schema(implementation = SiteConfigDto.class)))
public SiteConfigDto getConfig() {
SiteConfigDto resp = new SiteConfigDto();
resp.setCaptchaEnabled(captchaEnabled);

View File

@@ -9,6 +9,11 @@ import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@RestController
@RequestMapping("/api/drafts")
@@ -18,12 +23,20 @@ public class DraftController {
private final DraftMapper draftMapper;
@PostMapping
@Operation(summary = "Save draft", description = "Save a draft for current user")
@ApiResponse(responseCode = "200", description = "Draft saved",
content = @Content(schema = @Schema(implementation = DraftDto.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<DraftDto> saveDraft(@RequestBody DraftRequest req, Authentication auth) {
Draft draft = draftService.saveDraft(auth.getName(), req.getCategoryId(), req.getTitle(), req.getContent(), req.getTagIds());
return ResponseEntity.ok(draftMapper.toDto(draft));
}
@GetMapping("/me")
@Operation(summary = "Get my draft", description = "Get current user's draft")
@ApiResponse(responseCode = "200", description = "Draft details",
content = @Content(schema = @Schema(implementation = DraftDto.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<DraftDto> getMyDraft(Authentication auth) {
return draftService.getDraft(auth.getName())
.map(d -> ResponseEntity.ok(draftMapper.toDto(d)))
@@ -31,6 +44,9 @@ public class DraftController {
}
@DeleteMapping("/me")
@Operation(summary = "Delete my draft", description = "Delete current user's draft")
@ApiResponse(responseCode = "200", description = "Draft deleted")
@SecurityRequirement(name = "JWT")
public ResponseEntity<?> deleteMyDraft(Authentication auth) {
draftService.deleteDraft(auth.getName());
return ResponseEntity.ok().build();

View File

@@ -1,5 +1,10 @@
package com.openisle.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@@ -7,6 +12,10 @@ import java.util.Map;
@RestController
public class HelloController {
@GetMapping("/api/hello")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Hello endpoint", description = "Returns a greeting for authenticated users")
@ApiResponse(responseCode = "200", description = "Greeting payload",
content = @Content(schema = @Schema(implementation = Map.class)))
public Map<String, String> hello() {
return Map.of("message", "Hello, Authenticated User");
}

View File

@@ -6,6 +6,11 @@ import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.Map;
@@ -16,6 +21,10 @@ public class InviteController {
private final InviteService inviteService;
@PostMapping("/generate")
@Operation(summary = "Generate invite", description = "Generate an invite token")
@ApiResponse(responseCode = "200", description = "Invite token",
content = @Content(schema = @Schema(implementation = Map.class)))
@SecurityRequirement(name = "JWT")
public Map<String, String> generate(Authentication auth) {
String token = inviteService.generate(auth.getName());
return Map.of("token", token);

View File

@@ -7,6 +7,12 @@ import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.List;
@@ -17,11 +23,17 @@ public class MedalController {
private final MedalService medalService;
@GetMapping
@Operation(summary = "List medals", description = "List medals for user or globally")
@ApiResponse(responseCode = "200", description = "List of medals",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = MedalDto.class))))
public List<MedalDto> getMedals(@RequestParam(value = "userId", required = false) Long userId) {
return medalService.getMedals(userId);
}
@PostMapping("/select")
@Operation(summary = "Select medal", description = "Select a medal for current user")
@ApiResponse(responseCode = "200", description = "Medal selected")
@SecurityRequirement(name = "JWT")
public ResponseEntity<Void> selectMedal(@RequestBody MedalSelectRequest req, Authentication auth) {
try {
medalService.selectMedal(auth.getName(), req.getType());

View File

@@ -18,6 +18,12 @@ import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.List;
@@ -37,12 +43,20 @@ public class MessageController {
}
@GetMapping("/conversations")
@Operation(summary = "List conversations", description = "Get all conversations of current user")
@ApiResponse(responseCode = "200", description = "List of conversations",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ConversationDto.class))))
@SecurityRequirement(name = "JWT")
public ResponseEntity<List<ConversationDto>> getConversations(Authentication auth) {
List<ConversationDto> conversations = messageService.getConversations(getCurrentUserId(auth));
return ResponseEntity.ok(conversations);
}
@GetMapping("/conversations/{conversationId}")
@Operation(summary = "Get conversation", description = "Get messages of a conversation")
@ApiResponse(responseCode = "200", description = "Conversation detail",
content = @Content(schema = @Schema(implementation = ConversationDetailDto.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<ConversationDetailDto> getMessages(@PathVariable Long conversationId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@@ -53,12 +67,20 @@ public class MessageController {
}
@PostMapping
@Operation(summary = "Send message", description = "Send a direct message to a user")
@ApiResponse(responseCode = "200", description = "Message sent",
content = @Content(schema = @Schema(implementation = MessageDto.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<MessageDto> sendMessage(@RequestBody MessageRequest req, Authentication auth) {
Message message = messageService.sendMessage(getCurrentUserId(auth), req.getRecipientId(), req.getContent(), req.getReplyToId());
return ResponseEntity.ok(messageService.toDto(message));
}
@PostMapping("/conversations/{conversationId}/messages")
@Operation(summary = "Send message to conversation", description = "Reply within a conversation")
@ApiResponse(responseCode = "200", description = "Message sent",
content = @Content(schema = @Schema(implementation = MessageDto.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<MessageDto> sendMessageToConversation(@PathVariable Long conversationId,
@RequestBody ChannelMessageRequest req,
Authentication auth) {
@@ -67,18 +89,29 @@ public class MessageController {
}
@PostMapping("/conversations/{conversationId}/read")
@Operation(summary = "Mark conversation read", description = "Mark messages in conversation as read")
@ApiResponse(responseCode = "200", description = "Marked as read")
@SecurityRequirement(name = "JWT")
public ResponseEntity<Void> markAsRead(@PathVariable Long conversationId, Authentication auth) {
messageService.markConversationAsRead(conversationId, getCurrentUserId(auth));
return ResponseEntity.ok().build();
}
@PostMapping("/conversations")
@Operation(summary = "Find or create conversation", description = "Find existing or create new conversation with recipient")
@ApiResponse(responseCode = "200", description = "Conversation id",
content = @Content(schema = @Schema(implementation = CreateConversationResponse.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<CreateConversationResponse> findOrCreateConversation(@RequestBody CreateConversationRequest req, Authentication auth) {
MessageConversation conversation = messageService.findOrCreateConversation(getCurrentUserId(auth), req.getRecipientId());
return ResponseEntity.ok(new CreateConversationResponse(conversation.getId()));
}
@GetMapping("/unread-count")
@Operation(summary = "Unread message count", description = "Get unread message count for current user")
@ApiResponse(responseCode = "200", description = "Unread count",
content = @Content(schema = @Schema(implementation = Long.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<Long> getUnreadCount(Authentication auth) {
return ResponseEntity.ok(messageService.getUnreadMessageCount(getCurrentUserId(auth)));
}

View File

@@ -10,6 +10,12 @@ import com.openisle.service.NotificationService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.List;
import java.util.stream.Collectors;
@@ -23,6 +29,10 @@ public class NotificationController {
private final NotificationMapper notificationMapper;
@GetMapping
@Operation(summary = "List notifications", description = "Retrieve notifications for the current user")
@ApiResponse(responseCode = "200", description = "Notifications",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = NotificationDto.class))))
@SecurityRequirement(name = "JWT")
public List<NotificationDto> list(@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "30") int size,
Authentication auth) {
@@ -32,6 +42,10 @@ public class NotificationController {
}
@GetMapping("/unread")
@Operation(summary = "List unread notifications", description = "Retrieve unread notifications for the current user")
@ApiResponse(responseCode = "200", description = "Unread notifications",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = NotificationDto.class))))
@SecurityRequirement(name = "JWT")
public List<NotificationDto> listUnread(@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "30") int size,
Authentication auth) {
@@ -41,6 +55,10 @@ public class NotificationController {
}
@GetMapping("/unread-count")
@Operation(summary = "Unread count", description = "Get count of unread notifications")
@ApiResponse(responseCode = "200", description = "Unread count",
content = @Content(schema = @Schema(implementation = NotificationUnreadCountDto.class)))
@SecurityRequirement(name = "JWT")
public NotificationUnreadCountDto unreadCount(Authentication auth) {
long count = notificationService.countUnread(auth.getName());
NotificationUnreadCountDto uc = new NotificationUnreadCountDto();
@@ -49,26 +67,43 @@ public class NotificationController {
}
@PostMapping("/read")
@Operation(summary = "Mark notifications read", description = "Mark notifications as read")
@ApiResponse(responseCode = "200", description = "Marked read")
@SecurityRequirement(name = "JWT")
public void markRead(@RequestBody NotificationMarkReadRequest req, Authentication auth) {
notificationService.markRead(auth.getName(), req.getIds());
}
@GetMapping("/prefs")
@Operation(summary = "List preferences", description = "List notification preferences")
@ApiResponse(responseCode = "200", description = "Preferences",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = NotificationPreferenceDto.class))))
@SecurityRequirement(name = "JWT")
public List<NotificationPreferenceDto> prefs(Authentication auth) {
return notificationService.listPreferences(auth.getName());
}
@PostMapping("/prefs")
@Operation(summary = "Update preference", description = "Update notification preference")
@ApiResponse(responseCode = "200", description = "Preference updated")
@SecurityRequirement(name = "JWT")
public void updatePref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) {
notificationService.updatePreference(auth.getName(), req.getType(), req.isEnabled());
}
@GetMapping("/email-prefs")
@Operation(summary = "List email preferences", description = "List email notification preferences")
@ApiResponse(responseCode = "200", description = "Email preferences",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = NotificationPreferenceDto.class))))
@SecurityRequirement(name = "JWT")
public List<NotificationPreferenceDto> emailPrefs(Authentication auth) {
return notificationService.listEmailPreferences(auth.getName());
}
@PostMapping("/email-prefs")
@Operation(summary = "Update email preference", description = "Update email notification preference")
@ApiResponse(responseCode = "200", description = "Email preference updated")
@SecurityRequirement(name = "JWT")
public void updateEmailPref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) {
notificationService.updateEmailPreference(auth.getName(), req.getType(), req.isEnabled());
}

View File

@@ -5,6 +5,10 @@ import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.time.Duration;
@@ -22,11 +26,16 @@ public class OnlineController {
private static final String ONLINE_KEY = CachingConfig.ONLINE_CACHE_NAME +":";
@PostMapping("/heartbeat")
@Operation(summary = "Heartbeat", description = "Record user heartbeat")
@ApiResponse(responseCode = "200", description = "Heartbeat recorded")
public void ping(@RequestParam String userId){
redisTemplate.opsForValue().set(ONLINE_KEY+userId,"1", Duration.ofSeconds(150));
}
@GetMapping("/count")
@Operation(summary = "Online count", description = "Get current online user count")
@ApiResponse(responseCode = "200", description = "Online count",
content = @Content(schema = @Schema(implementation = Long.class)))
public long count(){
return redisTemplate.keys(ONLINE_KEY+"*").size();
}

View File

@@ -9,6 +9,12 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.List;
import java.util.Map;
@@ -22,6 +28,10 @@ public class PointHistoryController {
private final PointHistoryMapper pointHistoryMapper;
@GetMapping
@Operation(summary = "Point history", description = "List point history for current user")
@ApiResponse(responseCode = "200", description = "List of point histories",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PointHistoryDto.class))))
@SecurityRequirement(name = "JWT")
public List<PointHistoryDto> list(Authentication auth) {
return pointService.listHistory(auth.getName()).stream()
.map(pointHistoryMapper::toDto)
@@ -29,6 +39,10 @@ public class PointHistoryController {
}
@GetMapping("/trend")
@Operation(summary = "Point trend", description = "Get point trend data for current user")
@ApiResponse(responseCode = "200", description = "Trend data",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class))))
@SecurityRequirement(name = "JWT")
public List<Map<String, Object>> trend(Authentication auth,
@RequestParam(value = "days", defaultValue = "30") int days) {
return pointService.trend(auth.getName(), days);

View File

@@ -9,6 +9,12 @@ import com.openisle.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.List;
import java.util.Map;
@@ -24,6 +30,9 @@ public class PointMallController {
private final PointGoodMapper pointGoodMapper;
@GetMapping
@Operation(summary = "List goods", description = "List all point goods")
@ApiResponse(responseCode = "200", description = "List of goods",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PointGoodDto.class))))
public List<PointGoodDto> list() {
return pointMallService.listGoods().stream()
.map(pointGoodMapper::toDto)
@@ -31,6 +40,10 @@ public class PointMallController {
}
@PostMapping("/redeem")
@Operation(summary = "Redeem good", description = "Redeem a point good")
@ApiResponse(responseCode = "200", description = "Remaining points",
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
@SecurityRequirement(name = "JWT")
public Map<String, Integer> redeem(@RequestBody PointRedeemRequest req, Authentication auth) {
User user = userService.findByIdentifier(auth.getName()).orElseThrow();
int point = pointMallService.redeem(user, req.getGoodId(), req.getContact());

View File

@@ -5,6 +5,11 @@ import com.openisle.mapper.PostChangeLogMapper;
import com.openisle.service.PostChangeLogService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.util.List;
import java.util.stream.Collectors;
@@ -17,6 +22,9 @@ public class PostChangeLogController {
private final PostChangeLogMapper mapper;
@GetMapping("/{id}/change-logs")
@Operation(summary = "Post change logs", description = "List change logs for a post")
@ApiResponse(responseCode = "200", description = "Change logs",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostChangeLogDto.class))))
public List<PostChangeLogDto> listLogs(@PathVariable Long id) {
return changeLogService.listLogs(id).stream()
.map(mapper::toDto)

View File

@@ -7,6 +7,12 @@ import com.openisle.dto.PollDto;
import com.openisle.mapper.PostMapper;
import com.openisle.model.Post;
import com.openisle.service.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
@@ -35,6 +41,10 @@ public class PostController {
private boolean postCaptchaEnabled;
@PostMapping
@SecurityRequirement(name = "JWT")
@Operation(summary = "Create post", description = "Create a new post")
@ApiResponse(responseCode = "200", description = "Created post",
content = @Content(schema = @Schema(implementation = PostDetailDto.class)))
public ResponseEntity<PostDetailDto> createPost(@RequestBody PostRequest req, Authentication auth) {
if (captchaEnabled && postCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
return ResponseEntity.badRequest().build();
@@ -53,6 +63,10 @@ public class PostController {
}
@PutMapping("/{id}")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Update post", description = "Update an existing post")
@ApiResponse(responseCode = "200", description = "Updated post",
content = @Content(schema = @Schema(implementation = PostDetailDto.class)))
public ResponseEntity<PostDetailDto> updatePost(@PathVariable Long id, @RequestBody PostRequest req,
Authentication auth) {
Post post = postService.updatePost(id, auth.getName(), req.getCategoryId(),
@@ -61,21 +75,35 @@ public class PostController {
}
@DeleteMapping("/{id}")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Delete post", description = "Delete a post")
@ApiResponse(responseCode = "200", description = "Post deleted")
public void deletePost(@PathVariable Long id, Authentication auth) {
postService.deletePost(id, auth.getName());
}
@PostMapping("/{id}/close")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Close post", description = "Close a post to prevent further replies")
@ApiResponse(responseCode = "200", description = "Closed post",
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
public PostSummaryDto close(@PathVariable Long id, Authentication auth) {
return postMapper.toSummaryDto(postService.closePost(id, auth.getName()));
}
@PostMapping("/{id}/reopen")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Reopen post", description = "Reopen a closed post")
@ApiResponse(responseCode = "200", description = "Reopened post",
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
public PostSummaryDto reopen(@PathVariable Long id, Authentication auth) {
return postMapper.toSummaryDto(postService.reopenPost(id, auth.getName()));
}
@GetMapping("/{id}")
@Operation(summary = "Get post", description = "Get post details by id")
@ApiResponse(responseCode = "200", description = "Post detail",
content = @Content(schema = @Schema(implementation = PostDetailDto.class)))
public ResponseEntity<PostDetailDto> getPost(@PathVariable Long id, Authentication auth) {
String viewer = auth != null ? auth.getName() : null;
Post post = postService.viewPost(id, viewer);
@@ -83,23 +111,35 @@ public class PostController {
}
@PostMapping("/{id}/lottery/join")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Join lottery", description = "Join a lottery for the post")
@ApiResponse(responseCode = "200", description = "Joined lottery")
public ResponseEntity<Void> joinLottery(@PathVariable Long id, Authentication auth) {
postService.joinLottery(id, auth.getName());
return ResponseEntity.ok().build();
}
@GetMapping("/{id}/poll/progress")
@Operation(summary = "Poll progress", description = "Get poll progress for a post")
@ApiResponse(responseCode = "200", description = "Poll progress",
content = @Content(schema = @Schema(implementation = PollDto.class)))
public ResponseEntity<PollDto> pollProgress(@PathVariable Long id) {
return ResponseEntity.ok(postMapper.toSummaryDto(postService.getPoll(id)).getPoll());
}
@PostMapping("/{id}/poll/vote")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Vote poll", description = "Vote on a poll option")
@ApiResponse(responseCode = "200", description = "Vote recorded")
public ResponseEntity<Void> vote(@PathVariable Long id, @RequestParam("option") List<Integer> option, Authentication auth) {
postService.votePoll(id, auth.getName(), option);
return ResponseEntity.ok().build();
}
@GetMapping
@Operation(summary = "List posts", description = "List posts by various filters")
@ApiResponse(responseCode = "200", description = "List of posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
public List<PostSummaryDto> listPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
@RequestParam(value = "tagId", required = false) Long tagId,
@@ -137,6 +177,9 @@ public class PostController {
}
@GetMapping("/ranking")
@Operation(summary = "Ranking posts", description = "List posts by view rankings")
@ApiResponse(responseCode = "200", description = "Ranked posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
public List<PostSummaryDto> rankingPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
@RequestParam(value = "tagId", required = false) Long tagId,
@@ -162,6 +205,9 @@ public class PostController {
}
@GetMapping("/latest-reply")
@Operation(summary = "Latest reply posts", description = "List posts by latest replies")
@ApiResponse(responseCode = "200", description = "Posts sorted by latest reply",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
public List<PostSummaryDto> latestReplyPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
@RequestParam(value = "tagId", required = false) Long tagId,
@@ -187,6 +233,9 @@ public class PostController {
}
@GetMapping("/featured")
@Operation(summary = "Featured posts", description = "List featured posts")
@ApiResponse(responseCode = "200", description = "Featured posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
public List<PostSummaryDto> featuredPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
@RequestParam(value = "tagId", required = false) Long tagId,

View File

@@ -7,6 +7,11 @@ import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@RestController
@RequestMapping("/api/push")
@@ -17,6 +22,9 @@ public class PushSubscriptionController {
private String publicKey;
@GetMapping("/public-key")
@Operation(summary = "Get public key", description = "Retrieve web push public key")
@ApiResponse(responseCode = "200", description = "Public key",
content = @Content(schema = @Schema(implementation = PushPublicKeyDto.class)))
public PushPublicKeyDto getPublicKey() {
PushPublicKeyDto r = new PushPublicKeyDto();
r.setKey(publicKey);
@@ -24,6 +32,9 @@ public class PushSubscriptionController {
}
@PostMapping("/subscribe")
@Operation(summary = "Subscribe", description = "Subscribe to push notifications")
@ApiResponse(responseCode = "200", description = "Subscribed")
@SecurityRequirement(name = "JWT")
public void subscribe(@RequestBody PushSubscriptionRequest req, Authentication auth) {
pushSubscriptionService.saveSubscription(auth.getName(), req.getEndpoint(), req.getP256dh(), req.getAuth());
}

View File

@@ -12,6 +12,11 @@ import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@RestController
@RequestMapping("/api")
@@ -26,11 +31,18 @@ public class ReactionController {
* Get all available reaction types.
*/
@GetMapping("/reaction-types")
@Operation(summary = "List reaction types", description = "Get all available reaction types")
@ApiResponse(responseCode = "200", description = "Reaction types",
content = @Content(schema = @Schema(implementation = ReactionType[].class)))
public ReactionType[] listReactionTypes() {
return ReactionType.values();
}
@PostMapping("/posts/{postId}/reactions")
@Operation(summary = "React to post", description = "React to a post")
@ApiResponse(responseCode = "200", description = "Reaction result",
content = @Content(schema = @Schema(implementation = ReactionDto.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<ReactionDto> reactToPost(@PathVariable Long postId,
@RequestBody ReactionRequest req,
Authentication auth) {
@@ -46,6 +58,10 @@ public class ReactionController {
}
@PostMapping("/comments/{commentId}/reactions")
@Operation(summary = "React to comment", description = "React to a comment")
@ApiResponse(responseCode = "200", description = "Reaction result",
content = @Content(schema = @Schema(implementation = ReactionDto.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<ReactionDto> reactToComment(@PathVariable Long commentId,
@RequestBody ReactionRequest req,
Authentication auth) {
@@ -61,6 +77,10 @@ public class ReactionController {
}
@PostMapping("/messages/{messageId}/reactions")
@Operation(summary = "React to message", description = "React to a message")
@ApiResponse(responseCode = "200", description = "Reaction result",
content = @Content(schema = @Schema(implementation = ReactionDto.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<ReactionDto> reactToMessage(@PathVariable Long messageId,
@RequestBody ReactionRequest req,
Authentication auth) {

View File

@@ -13,6 +13,10 @@ import org.jsoup.safety.Safelist;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import com.vladsch.flexmark.ext.autolink.AutolinkExtension;
import com.vladsch.flexmark.ext.tables.TablesExtension;
@@ -63,6 +67,8 @@ public class RssController {
}
@GetMapping(value = "/api/rss", produces = "application/rss+xml;charset=UTF-8")
@Operation(summary = "RSS feed", description = "Generate RSS feed for latest posts")
@ApiResponse(responseCode = "200", description = "RSS XML", content = @Content(schema = @Schema(implementation = String.class)))
public String feed() {
// 建议 20你现在是 10这里保留你的 10
List<Post> posts = postService.listLatestRssPosts(10);

View File

@@ -11,6 +11,11 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.util.List;
import java.util.stream.Collectors;
@@ -24,6 +29,9 @@ public class SearchController {
private final PostMapper postMapper;
@GetMapping("/users")
@Operation(summary = "Search users", description = "Search users by keyword")
@ApiResponse(responseCode = "200", description = "List of users",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class))))
public List<UserDto> searchUsers(@RequestParam String keyword) {
return searchService.searchUsers(keyword).stream()
.map(userMapper::toDto)
@@ -31,6 +39,9 @@ public class SearchController {
}
@GetMapping("/posts")
@Operation(summary = "Search posts", description = "Search posts by keyword")
@ApiResponse(responseCode = "200", description = "List of posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
public List<PostSummaryDto> searchPosts(@RequestParam String keyword) {
return searchService.searchPosts(keyword).stream()
.map(postMapper::toSummaryDto)
@@ -38,6 +49,9 @@ public class SearchController {
}
@GetMapping("/posts/content")
@Operation(summary = "Search posts by content", description = "Search posts by content keyword")
@ApiResponse(responseCode = "200", description = "List of posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
public List<PostSummaryDto> searchPostsByContent(@RequestParam String keyword) {
return searchService.searchPostsByContent(keyword).stream()
.map(postMapper::toSummaryDto)
@@ -45,6 +59,9 @@ public class SearchController {
}
@GetMapping("/posts/title")
@Operation(summary = "Search posts by title", description = "Search posts by title keyword")
@ApiResponse(responseCode = "200", description = "List of posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
public List<PostSummaryDto> searchPostsByTitle(@RequestParam String keyword) {
return searchService.searchPostsByTitle(keyword).stream()
.map(postMapper::toSummaryDto)
@@ -52,6 +69,9 @@ public class SearchController {
}
@GetMapping("/global")
@Operation(summary = "Global search", description = "Search users and posts globally")
@ApiResponse(responseCode = "200", description = "Search results",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = SearchResultDto.class))))
public List<SearchResultDto> global(@RequestParam String keyword) {
return searchService.globalSearch(keyword).stream()
.map(r -> {

View File

@@ -10,6 +10,10 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.util.List;
@@ -26,6 +30,9 @@ public class SitemapController {
private String websiteUrl;
@GetMapping(value = "/sitemap.xml", produces = MediaType.APPLICATION_XML_VALUE)
@Operation(summary = "Sitemap", description = "Generate sitemap xml")
@ApiResponse(responseCode = "200", description = "Sitemap xml",
content = @Content(schema = @Schema(implementation = String.class)))
public ResponseEntity<String> sitemap() {
List<Post> posts = postRepository.findByStatus(PostStatus.PUBLISHED);

View File

@@ -8,6 +8,11 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.time.LocalDate;
import java.util.List;
@@ -21,6 +26,9 @@ public class StatController {
private final StatService statService;
@GetMapping("/dau")
@Operation(summary = "Daily active users", description = "Get daily active user count")
@ApiResponse(responseCode = "200", description = "DAU count",
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
public Map<String, Long> dau(@RequestParam(value = "date", required = false)
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
long count = userVisitService.countDau(date);
@@ -28,6 +36,9 @@ public class StatController {
}
@GetMapping("/dau-range")
@Operation(summary = "DAU range", description = "Get daily active users over range of days")
@ApiResponse(responseCode = "200", description = "DAU data",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class))))
public List<Map<String, Object>> dauRange(@RequestParam(value = "days", defaultValue = "30") int days) {
if (days < 1) days = 1;
LocalDate end = LocalDate.now();
@@ -42,6 +53,9 @@ public class StatController {
}
@GetMapping("/new-users-range")
@Operation(summary = "New users range", description = "Get new users over range of days")
@ApiResponse(responseCode = "200", description = "New user data",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class))))
public List<Map<String, Object>> newUsersRange(@RequestParam(value = "days", defaultValue = "30") int days) {
if (days < 1) days = 1;
LocalDate end = LocalDate.now();
@@ -56,6 +70,9 @@ public class StatController {
}
@GetMapping("/posts-range")
@Operation(summary = "Posts range", description = "Get posts count over range of days")
@ApiResponse(responseCode = "200", description = "Post data",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class))))
public List<Map<String, Object>> postsRange(@RequestParam(value = "days", defaultValue = "30") int days) {
if (days < 1) days = 1;
LocalDate end = LocalDate.now();
@@ -70,6 +87,9 @@ public class StatController {
}
@GetMapping("/comments-range")
@Operation(summary = "Comments range", description = "Get comments count over range of days")
@ApiResponse(responseCode = "200", description = "Comment data",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class))))
public List<Map<String, Object>> commentsRange(@RequestParam(value = "days", defaultValue = "30") int days) {
if (days < 1) days = 1;
LocalDate end = LocalDate.now();

View File

@@ -4,6 +4,9 @@ import com.openisle.service.SubscriptionService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
/** Endpoints for subscribing to posts, comments and users. */
@RestController
@@ -13,31 +16,49 @@ public class SubscriptionController {
private final SubscriptionService subscriptionService;
@PostMapping("/posts/{postId}")
@Operation(summary = "Subscribe post", description = "Subscribe to a post")
@ApiResponse(responseCode = "200", description = "Subscribed")
@SecurityRequirement(name = "JWT")
public void subscribePost(@PathVariable Long postId, Authentication auth) {
subscriptionService.subscribePost(auth.getName(), postId);
}
@DeleteMapping("/posts/{postId}")
@Operation(summary = "Unsubscribe post", description = "Unsubscribe from a post")
@ApiResponse(responseCode = "200", description = "Unsubscribed")
@SecurityRequirement(name = "JWT")
public void unsubscribePost(@PathVariable Long postId, Authentication auth) {
subscriptionService.unsubscribePost(auth.getName(), postId);
}
@PostMapping("/comments/{commentId}")
@Operation(summary = "Subscribe comment", description = "Subscribe to a comment")
@ApiResponse(responseCode = "200", description = "Subscribed")
@SecurityRequirement(name = "JWT")
public void subscribeComment(@PathVariable Long commentId, Authentication auth) {
subscriptionService.subscribeComment(auth.getName(), commentId);
}
@DeleteMapping("/comments/{commentId}")
@Operation(summary = "Unsubscribe comment", description = "Unsubscribe from a comment")
@ApiResponse(responseCode = "200", description = "Unsubscribed")
@SecurityRequirement(name = "JWT")
public void unsubscribeComment(@PathVariable Long commentId, Authentication auth) {
subscriptionService.unsubscribeComment(auth.getName(), commentId);
}
@PostMapping("/users/{username}")
@Operation(summary = "Subscribe user", description = "Subscribe to a user")
@ApiResponse(responseCode = "200", description = "Subscribed")
@SecurityRequirement(name = "JWT")
public void subscribeUser(@PathVariable String username, Authentication auth) {
subscriptionService.subscribeUser(auth.getName(), username);
}
@DeleteMapping("/users/{username}")
@Operation(summary = "Unsubscribe user", description = "Unsubscribe from a user")
@ApiResponse(responseCode = "200", description = "Unsubscribed")
@SecurityRequirement(name = "JWT")
public void unsubscribeUser(@PathVariable String username, Authentication auth) {
subscriptionService.unsubscribeUser(auth.getName(), username);
}

View File

@@ -13,6 +13,12 @@ import com.openisle.service.PostService;
import com.openisle.service.TagService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.List;
import java.util.Map;
@@ -29,6 +35,10 @@ public class TagController {
private final TagMapper tagMapper;
@PostMapping
@Operation(summary = "Create tag", description = "Create a new tag")
@ApiResponse(responseCode = "200", description = "Created tag",
content = @Content(schema = @Schema(implementation = TagDto.class)))
@SecurityRequirement(name = "JWT")
public TagDto create(@RequestBody TagRequest req, org.springframework.security.core.Authentication auth) {
boolean approved = true;
if (postService.getPublishMode() == PublishMode.REVIEW && auth != null) {
@@ -49,6 +59,9 @@ public class TagController {
}
@PutMapping("/{id}")
@Operation(summary = "Update tag", description = "Update an existing tag")
@ApiResponse(responseCode = "200", description = "Updated tag",
content = @Content(schema = @Schema(implementation = TagDto.class)))
public TagDto update(@PathVariable Long id, @RequestBody TagRequest req) {
Tag tag = tagService.updateTag(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
long count = postService.countPostsByTag(tag.getId());
@@ -56,11 +69,16 @@ public class TagController {
}
@DeleteMapping("/{id}")
@Operation(summary = "Delete tag", description = "Delete a tag by id")
@ApiResponse(responseCode = "200", description = "Tag deleted")
public void delete(@PathVariable Long id) {
tagService.deleteTag(id);
}
@GetMapping
@Operation(summary = "List tags", description = "List tags with optional keyword")
@ApiResponse(responseCode = "200", description = "List of tags",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class))))
public List<TagDto> list(@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "limit", required = false) Integer limit) {
List<Tag> tags = tagService.searchTags(keyword);
@@ -77,6 +95,9 @@ public class TagController {
}
@GetMapping("/{id}")
@Operation(summary = "Get tag", description = "Get tag by id")
@ApiResponse(responseCode = "200", description = "Tag detail",
content = @Content(schema = @Schema(implementation = TagDto.class)))
public TagDto get(@PathVariable Long id) {
Tag tag = tagService.getTag(id);
long count = postService.countPostsByTag(tag.getId());
@@ -84,6 +105,9 @@ public class TagController {
}
@GetMapping("/{id}/posts")
@Operation(summary = "List posts by tag", description = "Get posts with specific tag")
@ApiResponse(responseCode = "200", description = "List of posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
public List<PostSummaryDto> listPostsByTag(@PathVariable Long id,
@RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "pageSize", required = false) Integer pageSize) {

View File

@@ -6,6 +6,10 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@@ -27,6 +31,9 @@ public class UploadController {
private long maxUploadSize;
@PostMapping
@Operation(summary = "Upload file", description = "Upload image file")
@ApiResponse(responseCode = "200", description = "Upload result",
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
public ResponseEntity<?> upload(@RequestParam("file") MultipartFile file) {
if (checkImageType && (file.getContentType() == null || !file.getContentType().startsWith("image/"))) {
return ResponseEntity.badRequest().body(Map.of("code", 1, "msg", "File is not an image"));
@@ -48,6 +55,9 @@ public class UploadController {
}
@PostMapping("/url")
@Operation(summary = "Upload from URL", description = "Upload image from remote URL")
@ApiResponse(responseCode = "200", description = "Upload result",
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
public ResponseEntity<?> uploadUrl(@RequestBody Map<String, String> body) {
String link = body.get("url");
if (link == null || link.isBlank()) {
@@ -76,6 +86,9 @@ public class UploadController {
}
@GetMapping("/presign")
@Operation(summary = "Presign upload", description = "Get presigned upload URL")
@ApiResponse(responseCode = "200", description = "Presigned URL",
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
public java.util.Map<String, String> presign(@RequestParam("filename") String filename) {
return imageUploader.presignUpload(filename);
}

View File

@@ -6,6 +6,12 @@ import com.openisle.mapper.TagMapper;
import com.openisle.mapper.UserMapper;
import com.openisle.model.User;
import com.openisle.service.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
@@ -48,12 +54,20 @@ public class UserController {
private int defaultTagsLimit;
@GetMapping("/me")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Current user", description = "Get current authenticated user information")
@ApiResponse(responseCode = "200", description = "User detail",
content = @Content(schema = @Schema(implementation = UserDto.class)))
public ResponseEntity<UserDto> me(Authentication auth) {
User user = userService.findByUsername(auth.getName()).orElseThrow();
return ResponseEntity.ok(userMapper.toDto(user, auth));
}
@PostMapping("/me/avatar")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Upload avatar", description = "Upload avatar for current user")
@ApiResponse(responseCode = "200", description = "Upload result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> uploadAvatar(@RequestParam("file") MultipartFile file,
Authentication auth) {
if (checkImageType && (file.getContentType() == null || !file.getContentType().startsWith("image/"))) {
@@ -73,6 +87,10 @@ public class UserController {
}
@PutMapping("/me")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Update profile", description = "Update current user's profile")
@ApiResponse(responseCode = "200", description = "Updated profile",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> updateProfile(@RequestBody UpdateProfileDto dto,
Authentication auth) {
User user = userService.updateProfile(auth.getName(), dto.getUsername(), dto.getIntroduction());
@@ -83,12 +101,19 @@ public class UserController {
}
@PostMapping("/me/signin")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Daily sign in", description = "Sign in to receive rewards")
@ApiResponse(responseCode = "200", description = "Sign in reward",
content = @Content(schema = @Schema(implementation = Map.class)))
public Map<String, Integer> signIn(Authentication auth) {
int reward = levelService.awardForSignin(auth.getName());
return Map.of("reward", reward);
}
@GetMapping("/{identifier}")
@Operation(summary = "Get user", description = "Get user by identifier")
@ApiResponse(responseCode = "200", description = "User detail",
content = @Content(schema = @Schema(implementation = UserDto.class)))
public ResponseEntity<UserDto> getUser(@PathVariable("identifier") String identifier,
Authentication auth) {
User user = userService.findByIdentifier(identifier).orElseThrow(() -> new NotFoundException("User not found"));
@@ -96,6 +121,9 @@ public class UserController {
}
@GetMapping("/{identifier}/posts")
@Operation(summary = "User posts", description = "Get recent posts by user")
@ApiResponse(responseCode = "200", description = "User posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostMetaDto.class))))
public java.util.List<PostMetaDto> userPosts(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : defaultPostsLimit;
@@ -106,6 +134,9 @@ public class UserController {
}
@GetMapping("/{identifier}/subscribed-posts")
@Operation(summary = "Subscribed posts", description = "Get posts the user subscribed to")
@ApiResponse(responseCode = "200", description = "Subscribed posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostMetaDto.class))))
public java.util.List<PostMetaDto> subscribedPosts(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : defaultPostsLimit;
@@ -117,6 +148,9 @@ public class UserController {
}
@GetMapping("/{identifier}/replies")
@Operation(summary = "User replies", description = "Get recent replies by user")
@ApiResponse(responseCode = "200", description = "User replies",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = CommentInfoDto.class))))
public java.util.List<CommentInfoDto> userReplies(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : defaultRepliesLimit;
@@ -127,6 +161,9 @@ public class UserController {
}
@GetMapping("/{identifier}/hot-posts")
@Operation(summary = "User hot posts", description = "Get most reacted posts by user")
@ApiResponse(responseCode = "200", description = "Hot posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostMetaDto.class))))
public java.util.List<PostMetaDto> hotPosts(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : 10;
@@ -138,6 +175,9 @@ public class UserController {
}
@GetMapping("/{identifier}/hot-replies")
@Operation(summary = "User hot replies", description = "Get most reacted replies by user")
@ApiResponse(responseCode = "200", description = "Hot replies",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = CommentInfoDto.class))))
public java.util.List<CommentInfoDto> hotReplies(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : 10;
@@ -149,6 +189,9 @@ public class UserController {
}
@GetMapping("/{identifier}/hot-tags")
@Operation(summary = "User hot tags", description = "Get tags frequently used by user")
@ApiResponse(responseCode = "200", description = "Hot tags",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class))))
public java.util.List<TagDto> hotTags(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : 10;
@@ -161,6 +204,9 @@ public class UserController {
}
@GetMapping("/{identifier}/tags")
@Operation(summary = "User tags", description = "Get recent tags used by user")
@ApiResponse(responseCode = "200", description = "User tags",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class))))
public java.util.List<TagDto> userTags(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : defaultTagsLimit;
@@ -171,6 +217,9 @@ public class UserController {
}
@GetMapping("/{identifier}/following")
@Operation(summary = "Following users", description = "Get users that this user is following")
@ApiResponse(responseCode = "200", description = "Following list",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class))))
public java.util.List<UserDto> following(@PathVariable("identifier") String identifier) {
User user = userService.findByIdentifier(identifier).orElseThrow();
return subscriptionService.getSubscribedUsers(user.getUsername()).stream()
@@ -179,6 +228,9 @@ public class UserController {
}
@GetMapping("/{identifier}/followers")
@Operation(summary = "Followers", description = "Get followers of this user")
@ApiResponse(responseCode = "200", description = "Followers list",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class))))
public java.util.List<UserDto> followers(@PathVariable("identifier") String identifier) {
User user = userService.findByIdentifier(identifier).orElseThrow();
return subscriptionService.getSubscribers(user.getUsername()).stream()
@@ -187,6 +239,9 @@ public class UserController {
}
@GetMapping("/admins")
@Operation(summary = "Admin users", description = "List administrator users")
@ApiResponse(responseCode = "200", description = "Admin users",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class))))
public java.util.List<UserDto> admins() {
return userService.getAdmins().stream()
.map(userMapper::toDto)
@@ -194,6 +249,9 @@ public class UserController {
}
@GetMapping("/{identifier}/all")
@Operation(summary = "User aggregate", description = "Get aggregate information for user")
@ApiResponse(responseCode = "200", description = "User aggregate",
content = @Content(schema = @Schema(implementation = UserAggregateDto.class)))
public ResponseEntity<UserAggregateDto> userAggregate(@PathVariable("identifier") String identifier,
@RequestParam(value = "postsLimit", required = false) Integer postsLimit,
@RequestParam(value = "repliesLimit", required = false) Integer repliesLimit,

View File

@@ -1,5 +1,6 @@
package com.openisle.service;
import com.openisle.config.CachingConfig;
import com.openisle.model.Post;
import com.openisle.model.PostStatus;
import com.openisle.model.PostType;
@@ -28,12 +29,15 @@ import com.openisle.repository.PollVoteRepository;
import com.openisle.model.Role;
import com.openisle.exception.RateLimitException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.scheduling.TaskScheduler;
import com.openisle.service.EmailSender;
import java.time.Duration;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.*;
@@ -80,6 +84,8 @@ public class PostService {
@Value("${app.website-url:https://www.open-isle.com}")
private String websiteUrl;
private final RedisTemplate redisTemplate;
@org.springframework.beans.factory.annotation.Autowired
public PostService(PostRepository postRepository,
UserRepository userRepository,
@@ -102,7 +108,8 @@ public class PostService {
ApplicationContext applicationContext,
PointService pointService,
PostChangeLogService postChangeLogService,
@Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode) {
@Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode,
RedisTemplate redisTemplate) {
this.postRepository = postRepository;
this.userRepository = userRepository;
this.categoryRepository = categoryRepository;
@@ -125,6 +132,8 @@ public class PostService {
this.pointService = pointService;
this.postChangeLogService = postChangeLogService;
this.publishMode = publishMode;
this.redisTemplate = redisTemplate;
}
@EventListener(ApplicationReadyEvent.class)
@@ -201,9 +210,9 @@ public class PostService {
LocalDateTime endTime,
java.util.List<String> options,
Boolean multiple) {
long recent = postRepository.countByAuthorAfter(username,
java.time.LocalDateTime.now().minusMinutes(5));
if (recent >= 1) {
// 限制访问次数
boolean limitResult = postRateLimit(username);
if (!limitResult) {
throw new RateLimitException("Too many posts");
}
if (tagIds == null || tagIds.isEmpty()) {
@@ -300,6 +309,23 @@ public class PostService {
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) {
LotteryPost post = lotteryPostRepository.findById(postId)
.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
springdoc.api-docs.path=/api/v3/api-docs
springdoc.api-docs.enabled=true
springdoc.api-docs.server-url=${WEBSITE_URL:https://www.open-isle.com}
springdoc.api-docs.servers[0].url=https://www.open-isle.com
springdoc.api-docs.servers[0].description=Production Environment
springdoc.api-docs.servers[1].url=https://www.staging.open-isle.com
springdoc.api-docs.servers[1].description=Staging Environment
springdoc.info.title=OpenIsle
springdoc.info.description=OpenIsle Open API Documentation
springdoc.info.version=0.0.1

View File

@@ -6,6 +6,7 @@ import com.openisle.exception.RateLimitException;
import org.junit.jupiter.api.Test;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
import static org.junit.jupiter.api.Assertions.*;
@@ -38,11 +39,12 @@ class PostServiceTest {
ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
RedisTemplate redisTemplate = mock(RedisTemplate.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService,
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT);
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT, redisTemplate);
when(context.getBean(PostService.class)).thenReturn(service);
Post post = new Post();
@@ -88,11 +90,12 @@ class PostServiceTest {
ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
RedisTemplate redisTemplate = mock(RedisTemplate.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService,
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT);
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT, redisTemplate);
when(context.getBean(PostService.class)).thenReturn(service);
Post post = new Post();
@@ -144,11 +147,12 @@ class PostServiceTest {
ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
RedisTemplate redisTemplate = mock(RedisTemplate.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService,
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT);
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT, redisTemplate);
when(context.getBean(PostService.class)).thenReturn(service);
when(postRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(1L);
@@ -181,11 +185,12 @@ class PostServiceTest {
ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
RedisTemplate redisTemplate = mock(RedisTemplate.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService,
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT);
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT, redisTemplate);
when(context.getBean(PostService.class)).thenReturn(service);
User author = new User();

View File

@@ -16,6 +16,6 @@ bun dev
使用以下路由:
- `docs/frontend/` 前端技术文档
- `docs/backend/` 后端技术文档
- `docs/openapi/` 后端 API 文档
- `frontend/` 前端技术文档
- `backend/` 后端技术文档
- `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 page = source.getPage(params.slug);
if (!page) notFound();
@@ -48,7 +48,7 @@ export async function generateStaticParams() {
}
export async function generateMetadata(
props: PageProps<'/docs/[[...slug]]'>
props: PageProps<'/[[...slug]]'>
): Promise<Metadata> {
const params = await props.params;
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>;
}
export default function Layout({ children }: LayoutProps<'/docs'>) {
export default function Layout({ children }: LayoutProps<'/'>) {
return (
// @ts-ignore
<DocsLayout
@@ -40,7 +40,7 @@ export default function Layout({ children }: LayoutProps<'/docs'>) {
{
title: 'OpenIsle 前端',
description: <TabTitle></TabTitle>,
url: '/docs/frontend',
url: '/frontend',
icon: (
<TabIcon color="#4ca154">
<CompassIcon />
@@ -50,7 +50,7 @@ export default function Layout({ children }: LayoutProps<'/docs'>) {
{
title: 'OpenIsle 后端',
description: <TabTitle></TabTitle>,
url: '/docs/backend',
url: '/backend',
icon: (
<TabIcon color="#1f66f4">
<ServerIcon />
@@ -60,7 +60,7 @@ export default function Layout({ children }: LayoutProps<'/docs'>) {
{
title: 'OpenIsle API',
description: <TabTitle> API </TabTitle>,
url: '/docs/openapi',
url: '/openapi',
icon: (
<TabIcon color="#677489">
<CodeXmlIcon />

View File

@@ -6,7 +6,7 @@ const inter = Inter({
subsets: ['latin'],
});
export default function Layout({ children }: LayoutProps<'/docs'>) {
export default function Layout({ children }: LayoutProps<'/'>) {
return (
<html lang="zh" className={inter.className} suppressHydrationWarning>
<body className="flex flex-col min-h-screen">

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ export function baseOptions(): BaseLayoutProps {
githubUrl: 'https://github.com/nagisa77/OpenIsle',
nav: {
title: 'OpenIsle Docs',
url: '/docs',
url: '/',
},
searchToggle: {
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
export const source = loader({
// it assigns a URL to your pages
baseUrl: '/docs',
baseUrl: '/',
source: docs.toFumadocsSource(),
pageTree: {
transformers: [transformerOpenAPI()],