Compare commits

..

1 Commits

Author SHA1 Message Date
Tim
615832f112 feat: add floating new message container 2025-09-07 23:49:28 +08:00
113 changed files with 3569 additions and 3840 deletions

View File

@@ -1,11 +1,7 @@
name: Deploy Documentation name: Deploy Documentation
on: on:
workflow_call: push:
inputs:
build-id:
required: false
type: string
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@@ -20,9 +16,6 @@ jobs:
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Log build
run: echo "Running documentation deployment from build ${{ inputs.build-id }}"
- name: Setup Bun - name: Setup Bun
uses: oven-sh/setup-bun@v1 uses: oven-sh/setup-bun@v1
with: with:

View File

@@ -5,14 +5,10 @@ on:
branches: [main] branches: [main]
workflow_dispatch: workflow_dispatch:
permissions:
contents: write
jobs: jobs:
build-and-deploy: build-and-deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: Deploy environment: Deploy
if: ${{ !github.event.repository.fork }} # 只有非 fork 才执行
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -25,11 +21,3 @@ jobs:
key: ${{ secrets.SSH_KEY }} key: ${{ secrets.SSH_KEY }}
script: bash /opt/openisle/deploy-staging.sh script: bash /opt/openisle/deploy-staging.sh
deploy-docs:
needs: build-and-deploy
if: ${{ success() }}
uses: ./.github/workflows/deploy-docs.yml
secrets: inherit
with:
build-id: ${{ github.run_id }}

View File

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

View File

@@ -4,8 +4,6 @@
高效的开源社区前后端平台 高效的开源社区前后端平台
<br><br><br> <br><br><br>
<img alt="Image" src="https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/22752cfac5a04a9c90c41995b9f55fed.png" width="1200"> <img alt="Image" src="https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/22752cfac5a04a9c90c41995b9f55fed.png" width="1200">
<br><br><br>
<a href="https://hellogithub.com/repository/nagisa77/OpenIsle" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=8605546658d94cbab45182af2a02e4c8&claim_uid=p5GNFTtZl6HBAYQ" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</p> </p>
## 💡 简介 ## 💡 简介

View File

@@ -40,14 +40,6 @@ public class CachingConfig {
public static final String CATEGORY_CACHE_NAME="openisle_categories"; public static final String CATEGORY_CACHE_NAME="openisle_categories";
// 在线人数缓存名 // 在线人数缓存名
public static final String ONLINE_CACHE_NAME="openisle_online"; public static final String ONLINE_CACHE_NAME="openisle_online";
// 注册验证码
public static final String VERIFY_CACHE_NAME="openisle_verify";
// 发帖频率限制
public static final String LIMIT_CACHE_NAME="openisle_limit";
// 用户访问统计
public static final String VISIT_CACHE_NAME="openisle_visit";
// 文章缓存
public static final String POST_CACHE_NAME="openisle_posts";
/** /**
* 自定义Redis的序列化器 * 自定义Redis的序列化器
@@ -67,10 +59,7 @@ public class CachingConfig {
// Hibernate6Module 可以自动处理懒加载代理对象。 // Hibernate6Module 可以自动处理懒加载代理对象。
// Tag对象的creator是FetchType.LAZY // Tag对象的creator是FetchType.LAZY
objectMapper.registerModule(new Hibernate6Module() objectMapper.registerModule(new Hibernate6Module()
.disable(Hibernate6Module.Feature.USE_TRANSIENT_ANNOTATION) .disable(Hibernate6Module.Feature.USE_TRANSIENT_ANNOTATION));
// 将 Hibernate 特有的集合类型转换为标准 Java 集合类型
// 避免序列化时出现 org.hibernate.collection.spi.PersistentSet 这样的类型信息
.configure(Hibernate6Module.Feature.REPLACE_PERSISTENT_COLLECTIONS, true));
// service的时候带上类型信息 // service的时候带上类型信息
// 启用类型信息,避免 LinkedHashMap 问题 // 启用类型信息,避免 LinkedHashMap 问题
objectMapper.activateDefaultTyping( objectMapper.activateDefaultTyping(
@@ -97,10 +86,8 @@ public class CachingConfig {
// 个别缓存单独设置 TTL 时间 // 个别缓存单独设置 TTL 时间
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>(); Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
RedisCacheConfiguration oneHourConfig = config.entryTtl(Duration.ofHours(1)); RedisCacheConfiguration oneHourConfig = config.entryTtl(Duration.ofHours(1));
RedisCacheConfiguration tenMinutesConfig = config.entryTtl(Duration.ofMinutes(10));
cacheConfigs.put(TAG_CACHE_NAME, oneHourConfig); cacheConfigs.put(TAG_CACHE_NAME, oneHourConfig);
cacheConfigs.put(CATEGORY_CACHE_NAME, oneHourConfig); cacheConfigs.put(CATEGORY_CACHE_NAME, oneHourConfig);
cacheConfigs.put(POST_CACHE_NAME, tenMinutesConfig);
return RedisCacheManager.builder(connectionFactory) return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config) .cacheDefaults(config)

View File

@@ -5,21 +5,13 @@ import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
@RequiredArgsConstructor
public class OpenApiConfig { public class OpenApiConfig {
private final SpringDocProperties springDocProperties;
@Value("${springdoc.info.title}") @Value("${springdoc.info.title}")
private String title; private String title;
@@ -38,23 +30,19 @@ public class OpenApiConfig {
@Bean @Bean
public OpenAPI openAPI() { public OpenAPI openAPI() {
SecurityScheme securityScheme = new SecurityScheme() SecurityScheme securityScheme = new SecurityScheme()
.type(SecurityScheme.Type.HTTP) .type(SecurityScheme.Type.HTTP)
.scheme(scheme.toLowerCase()) .scheme(scheme.toLowerCase())
.bearerFormat("JWT") .bearerFormat("JWT")
.in(SecurityScheme.In.HEADER) .in(SecurityScheme.In.HEADER)
.name(header); .name(header);
List<Server> servers = springDocProperties.getServers().stream()
.map(s -> new Server().url(s.getUrl()).description(s.getDescription()))
.collect(Collectors.toList());
return new OpenAPI() return new OpenAPI()
.servers(servers)
.info(new Info() .info(new Info()
.title(title) .title(title)
.description(description) .description(description)
.version(version)) .version(version))
.components(new Components().addSecuritySchemes("JWT", securityScheme)) .components(new Components()
.addSecuritySchemes("JWT", securityScheme))
.addSecurityItem(new SecurityRequirement().addList("JWT")); .addSecurityItem(new SecurityRequirement().addList("JWT"));
} }
} }

View File

@@ -1,7 +1,6 @@
package com.openisle.config; package com.openisle.config;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.Queue;
@@ -24,7 +23,6 @@ import java.util.List;
@Configuration @Configuration
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
public class RabbitMQConfig { public class RabbitMQConfig {
public static final String EXCHANGE_NAME = "openisle-exchange"; public static final String EXCHANGE_NAME = "openisle-exchange";
@@ -40,7 +38,7 @@ public class RabbitMQConfig {
@PostConstruct @PostConstruct
public void init() { public void init() {
log.info("RabbitMQ配置初始化: 队列数量={}, 持久化={}", queueCount, queueDurable); System.out.println("RabbitMQ配置初始化: 队列数量=" + queueCount + ", 持久化=" + queueDurable);
} }
@Bean @Bean
@@ -53,7 +51,7 @@ public class RabbitMQConfig {
*/ */
@Bean @Bean
public List<Queue> shardedQueues() { public List<Queue> shardedQueues() {
log.info("开始创建分片队列 Bean..."); System.out.println("开始创建分片队列 Bean...");
List<Queue> queues = new ArrayList<>(); List<Queue> queues = new ArrayList<>();
for (int i = 0; i < queueCount; i++) { for (int i = 0; i < queueCount; i++) {
@@ -63,7 +61,7 @@ public class RabbitMQConfig {
queues.add(queue); queues.add(queue);
} }
log.info("分片队列 Bean 创建完成,总数: {}", queues.size()); System.out.println("分片队列 Bean 创建完成,总数: " + queues.size());
return queues; return queues;
} }
@@ -72,7 +70,7 @@ public class RabbitMQConfig {
*/ */
@Bean @Bean
public List<Binding> shardedBindings(TopicExchange exchange, @Qualifier("shardedQueues") List<Queue> shardedQueues) { public List<Binding> shardedBindings(TopicExchange exchange, @Qualifier("shardedQueues") List<Queue> shardedQueues) {
log.info("开始创建分片绑定 Bean..."); System.out.println("开始创建分片绑定 Bean...");
List<Binding> bindings = new ArrayList<>(); List<Binding> bindings = new ArrayList<>();
if (shardedQueues != null) { if (shardedQueues != null) {
for (Queue queue : shardedQueues) { for (Queue queue : shardedQueues) {
@@ -84,7 +82,7 @@ public class RabbitMQConfig {
} }
} }
log.info("分片绑定 Bean 创建完成,总数: {}", bindings.size()); System.out.println("分片绑定 Bean 创建完成,总数: " + bindings.size());
return bindings; return bindings;
} }
@@ -137,14 +135,14 @@ public class RabbitMQConfig {
@Qualifier("shardedBindings") List<Binding> shardedBindings, @Qualifier("shardedBindings") List<Binding> shardedBindings,
Binding legacyBinding) { Binding legacyBinding) {
return args -> { return args -> {
log.info("=== 开始主动声明 RabbitMQ 组件 ==="); System.out.println("=== 开始主动声明 RabbitMQ 组件 ===");
try { try {
// 声明交换 // 声明交换
rabbitAdmin.declareExchange(exchange); rabbitAdmin.declareExchange(exchange);
// 声明分片队列 - 检查存在性 // 声明分片队列 - 检查存在性
log.info("开始检查并声明 {} 个分片队列...", shardedQueues.size()); System.out.println("开始检查并声明 " + shardedQueues.size() + " 个分片队列...");
int successCount = 0; int successCount = 0;
int skippedCount = 0; int skippedCount = 0;
@@ -161,44 +159,45 @@ public class RabbitMQConfig {
skippedCount++; skippedCount++;
} }
} catch (Exception e) { } catch (Exception e) {
log.error("队列声明失败: {}, 错误: {}", queueName, e.getMessage()); System.err.println("队列声明失败: " + queueName + ", 错误: " + e.getMessage());
} }
} }
log.info("分片队列处理完成: 成功 {}, 跳过 {}, 总数 {}", successCount, skippedCount, shardedQueues.size()); System.out.println("分片队列处理完成: 成功 " + successCount + ", 跳过 " + skippedCount + ", 总数 " + shardedQueues.size());
// 声明分片绑定 // 声明分片绑定
log.info("开始声明 {} 个分片绑定...", shardedBindings.size()); System.out.println("开始声明 " + shardedBindings.size() + " 个分片绑定...");
int bindingSuccessCount = 0; int bindingSuccessCount = 0;
for (Binding binding : shardedBindings) { for (Binding binding : shardedBindings) {
try { try {
rabbitAdmin.declareBinding(binding); rabbitAdmin.declareBinding(binding);
bindingSuccessCount++; bindingSuccessCount++;
} catch (Exception e) { } catch (Exception e) {
log.error("绑定声明失败: {}", e.getMessage()); System.err.println("绑定声明失败: " + e.getMessage());
} }
} }
log.info("分片绑定声明完成: 成功 {}/{}", bindingSuccessCount, shardedBindings.size()); System.out.println("分片绑定声明完成: 成功 " + bindingSuccessCount + "/" + shardedBindings.size());
// 声明遗留队列和绑定 - 检查存在性 // 声明遗留队列和绑定 - 检查存在性
try { try {
rabbitAdmin.declareQueue(legacyQueue); rabbitAdmin.declareQueue(legacyQueue);
rabbitAdmin.declareBinding(legacyBinding); rabbitAdmin.declareBinding(legacyBinding);
log.info("遗留队列和绑定就绪: {} (已存在或新创建)", QUEUE_NAME); System.out.println("遗留队列和绑定就绪: " + QUEUE_NAME + " (已存在或新创建)");
} catch (org.springframework.amqp.AmqpIOException e) { } catch (org.springframework.amqp.AmqpIOException e) {
if (e.getMessage().contains("PRECONDITION_FAILED") && e.getMessage().contains("durable")) { if (e.getMessage().contains("PRECONDITION_FAILED") && e.getMessage().contains("durable")) {
log.warn("遗留队列已存在但 durable 设置不匹配: {}, 保持现有队列", QUEUE_NAME); System.out.println("遗留队列已存在但 durable 设置不匹配: " + QUEUE_NAME + ", 保持现有队列");
} else { } else {
log.error("遗留队列声明失败: {}, 错误: {}", QUEUE_NAME, e.getMessage()); System.err.println("遗留队列声明失败: " + QUEUE_NAME + ", 错误: " + e.getMessage());
} }
} catch (Exception e) { } catch (Exception e) {
log.error("遗留队列声明失败: {}, 错误: {}", QUEUE_NAME, e.getMessage()); System.err.println("遗留队列声明失败: " + QUEUE_NAME + ", 错误: " + e.getMessage());
} }
log.info("=== RabbitMQ 组件声明完成 ==="); System.out.println("=== RabbitMQ 组件声明完成 ===");
log.info("请检查 RabbitMQ 管理界面确认队列已正确创建"); System.out.println("请检查 RabbitMQ 管理界面确认队列已正确创建");
} catch (Exception e) { } catch (Exception e) {
log.error("RabbitMQ 组件声明过程中发生严重错误", e); System.err.println("RabbitMQ 组件声明过程中发生严重错误:");
e.printStackTrace();
} }
}; };
} }

View File

@@ -6,7 +6,6 @@ import com.openisle.repository.UserRepository;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -27,8 +26,6 @@ import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import java.time.LocalDate;
import java.util.List; import java.util.List;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
@@ -47,8 +44,6 @@ public class SecurityConfig {
@Value("${app.website-url}") @Value("${app.website-url}")
private String websiteUrl; private String websiteUrl;
private final RedisTemplate redisTemplate;
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); return new BCryptPasswordEncoder();
@@ -95,9 +90,6 @@ public class SecurityConfig {
"http://192.168.7.98", "http://192.168.7.98",
"http://192.168.7.98:3000", "http://192.168.7.98:3000",
"https://petstore.swagger.io", "https://petstore.swagger.io",
// 允许自建OpenAPI地址
"https://docs.open-isle.com",
"https://www.docs.open-isle.com",
websiteUrl, websiteUrl,
websiteUrl.replace("://www.", "://") websiteUrl.replace("://www.", "://")
)); ));
@@ -213,8 +205,7 @@ public class SecurityConfig {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
var auth = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication(); var auth = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated() && !(auth instanceof org.springframework.security.authentication.AnonymousAuthenticationToken)) { if (auth != null && auth.isAuthenticated() && !(auth instanceof org.springframework.security.authentication.AnonymousAuthenticationToken)) {
String key = CachingConfig.VISIT_CACHE_NAME+":"+ LocalDate.now(); userVisitService.recordVisit(auth.getName());
redisTemplate.opsForSet().add(key, auth.getName());
} }
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }

View File

@@ -1,20 +0,0 @@
package com.openisle.config;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "springdoc.api-docs")
public class SpringDocProperties {
private List<ServerConfig> servers = new ArrayList<>();
@Data
public static class ServerConfig {
private String url;
private String description;
}
}

View File

@@ -1,36 +0,0 @@
package com.openisle.config;
import com.openisle.model.Role;
import com.openisle.model.User;
import com.openisle.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* Ensure a dedicated "system" user exists for internal operations.
*/
@Component
@RequiredArgsConstructor
public class SystemUserInitializer implements CommandLineRunner {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Override
public void run(String... args) {
userRepository.findByUsername("system").orElseGet(() -> {
User system = new User();
system.setUsername("system");
system.setEmail("system@openisle.local");
// todo(tim): raw password 采用环境变量
system.setPassword(passwordEncoder.encode("system"));
system.setRole(Role.USER);
system.setVerified(true);
system.setApproved(true);
system.setAvatar("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png");
return userRepository.save(system);
});
}
}

View File

@@ -12,12 +12,6 @@ import com.openisle.service.UserService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; 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.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -31,9 +25,6 @@ public class ActivityController {
private final ActivityMapper activityMapper; private final ActivityMapper activityMapper;
@GetMapping @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() { public List<ActivityDto> list() {
return activityService.list().stream() return activityService.list().stream()
.map(activityMapper::toDto) .map(activityMapper::toDto)
@@ -41,9 +32,6 @@ public class ActivityController {
} }
@GetMapping("/milk-tea") @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() { public MilkTeaInfoDto milkTea() {
Activity a = activityService.getByType(ActivityType.MILK_TEA); Activity a = activityService.getByType(ActivityType.MILK_TEA);
long count = activityService.countParticipants(a); long count = activityService.countParticipants(a);
@@ -57,10 +45,6 @@ public class ActivityController {
} }
@PostMapping("/milk-tea/redeem") @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) { public java.util.Map<String, String> redeemMilkTea(@RequestBody MilkTeaRedeemRequest req, Authentication auth) {
User user = userService.findByIdentifier(auth.getName()).orElseThrow(); User user = userService.findByIdentifier(auth.getName()).orElseThrow();
Activity a = activityService.getByType(ActivityType.MILK_TEA); Activity a = activityService.getByType(ActivityType.MILK_TEA);

View File

@@ -3,11 +3,6 @@ package com.openisle.controller;
import com.openisle.dto.CommentDto; import com.openisle.dto.CommentDto;
import com.openisle.mapper.CommentMapper; import com.openisle.mapper.CommentMapper;
import com.openisle.service.CommentService; 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 lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -23,19 +18,11 @@ public class AdminCommentController {
private final CommentMapper commentMapper; private final CommentMapper commentMapper;
@PostMapping("/{id}/pin") @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) { public CommentDto pin(@PathVariable Long id, Authentication auth) {
return commentMapper.toDto(commentService.pinComment(auth.getName(), id)); return commentMapper.toDto(commentService.pinComment(auth.getName(), id));
} }
@PostMapping("/{id}/unpin") @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) { public CommentDto unpin(@PathVariable Long id, Authentication auth) {
return commentMapper.toDto(commentService.unpinComment(auth.getName(), id)); return commentMapper.toDto(commentService.unpinComment(auth.getName(), id));
} }

View File

@@ -5,11 +5,6 @@ import com.openisle.service.AiUsageService;
import com.openisle.service.PasswordValidator; import com.openisle.service.PasswordValidator;
import com.openisle.service.PostService; import com.openisle.service.PostService;
import com.openisle.service.RegisterModeService; 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 lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -23,10 +18,6 @@ public class AdminConfigController {
private final RegisterModeService registerModeService; private final RegisterModeService registerModeService;
@GetMapping @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() { public ConfigDto getConfig() {
ConfigDto dto = new ConfigDto(); ConfigDto dto = new ConfigDto();
dto.setPublishMode(postService.getPublishMode()); dto.setPublishMode(postService.getPublishMode());
@@ -37,10 +28,6 @@ public class AdminConfigController {
} }
@PostMapping @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) { public ConfigDto updateConfig(@RequestBody ConfigDto dto) {
if (dto.getPublishMode() != null) { if (dto.getPublishMode() != null) {
postService.setPublishMode(dto.getPublishMode()); postService.setPublishMode(dto.getPublishMode());

View File

@@ -1,10 +1,5 @@
package com.openisle.controller; 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.GetMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.Map; import java.util.Map;
@@ -15,10 +10,6 @@ import java.util.Map;
@RestController @RestController
public class AdminController { public class AdminController {
@GetMapping("/api/admin/hello") @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() { public Map<String, String> adminHello() {
return Map.of("message", "Hello, Admin User"); return Map.of("message", "Hello, Admin User");
} }

View File

@@ -3,12 +3,6 @@ package com.openisle.controller;
import com.openisle.dto.PostSummaryDto; import com.openisle.dto.PostSummaryDto;
import com.openisle.mapper.PostMapper; import com.openisle.mapper.PostMapper;
import com.openisle.service.PostService; 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 lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -26,10 +20,6 @@ public class AdminPostController {
private final PostMapper postMapper; private final PostMapper postMapper;
@GetMapping("/pending") @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() { public List<PostSummaryDto> pendingPosts() {
return postService.listPendingPosts().stream() return postService.listPendingPosts().stream()
.map(postMapper::toSummaryDto) .map(postMapper::toSummaryDto)
@@ -37,56 +27,32 @@ public class AdminPostController {
} }
@PostMapping("/{id}/approve") @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) { public PostSummaryDto approve(@PathVariable Long id) {
return postMapper.toSummaryDto(postService.approvePost(id)); return postMapper.toSummaryDto(postService.approvePost(id));
} }
@PostMapping("/{id}/reject") @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) { public PostSummaryDto reject(@PathVariable Long id) {
return postMapper.toSummaryDto(postService.rejectPost(id)); return postMapper.toSummaryDto(postService.rejectPost(id));
} }
@PostMapping("/{id}/pin") @PostMapping("/{id}/pin")
@SecurityRequirement(name = "JWT") public PostSummaryDto pin(@PathVariable Long id) {
@Operation(summary = "Pin post", description = "Pin a post to the top") return postMapper.toSummaryDto(postService.pinPost(id));
@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") @PostMapping("/{id}/unpin")
@SecurityRequirement(name = "JWT") public PostSummaryDto unpin(@PathVariable Long id) {
@Operation(summary = "Unpin post", description = "Remove a post from the top") return postMapper.toSummaryDto(postService.unpinPost(id));
@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") @PostMapping("/{id}/rss-exclude")
@SecurityRequirement(name = "JWT") public PostSummaryDto excludeFromRss(@PathVariable Long id) {
@Operation(summary = "Exclude from RSS", description = "Exclude a post from RSS feed") return postMapper.toSummaryDto(postService.excludeFromRss(id));
@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") @PostMapping("/{id}/rss-include")
@SecurityRequirement(name = "JWT") public PostSummaryDto includeInRss(@PathVariable Long id) {
@Operation(summary = "Include in RSS", description = "Include a post in the RSS feed") return postMapper.toSummaryDto(postService.includeInRss(id));
@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,12 +5,6 @@ import com.openisle.mapper.TagMapper;
import com.openisle.model.Tag; import com.openisle.model.Tag;
import com.openisle.service.PostService; import com.openisle.service.PostService;
import com.openisle.service.TagService; 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 lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -26,10 +20,6 @@ public class AdminTagController {
private final TagMapper tagMapper; private final TagMapper tagMapper;
@GetMapping("/pending") @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() { public List<TagDto> pendingTags() {
return tagService.listPendingTags().stream() return tagService.listPendingTags().stream()
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId()))) .map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
@@ -37,10 +27,6 @@ public class AdminTagController {
} }
@PostMapping("/{id}/approve") @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) { public TagDto approve(@PathVariable Long id) {
Tag tag = tagService.approveTag(id); Tag tag = tagService.approveTag(id);
long count = postService.countPostsByTag(tag.getId()); long count = postService.countPostsByTag(tag.getId());

View File

@@ -6,9 +6,6 @@ import com.openisle.model.User;
import com.openisle.service.EmailSender; import com.openisle.service.EmailSender;
import com.openisle.repository.NotificationRepository; import com.openisle.repository.NotificationRepository;
import com.openisle.repository.UserRepository; 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 lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -25,9 +22,6 @@ public class AdminUserController {
private String websiteUrl; private String websiteUrl;
@PostMapping("/{id}/approve") @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) { public ResponseEntity<?> approve(@PathVariable Long id) {
User user = userRepository.findById(id).orElseThrow(); User user = userRepository.findById(id).orElseThrow();
user.setApproved(true); user.setApproved(true);
@@ -39,9 +33,6 @@ public class AdminUserController {
} }
@PostMapping("/{id}/reject") @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) { public ResponseEntity<?> reject(@PathVariable Long id) {
User user = userRepository.findById(id).orElseThrow(); User user = userRepository.findById(id).orElseThrow();
user.setApproved(false); user.setApproved(false);

View File

@@ -9,11 +9,6 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; 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; import java.util.Map;
@@ -26,10 +21,6 @@ public class AiController {
private final AiUsageService aiUsageService; private final AiUsageService aiUsageService;
@PostMapping("/format") @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, public ResponseEntity<Map<String, String>> format(@RequestBody Map<String, String> req,
Authentication auth) { Authentication auth) {
String text = req.get("text"); String text = req.get("text");

View File

@@ -1,27 +1,18 @@
package com.openisle.controller; package com.openisle.controller;
import com.openisle.config.CachingConfig;
import com.openisle.dto.*; import com.openisle.dto.*;
import com.openisle.exception.FieldException; import com.openisle.exception.FieldException;
import com.openisle.model.RegisterMode; import com.openisle.model.RegisterMode;
import com.openisle.model.User; import com.openisle.model.User;
import com.openisle.repository.UserRepository; import com.openisle.repository.UserRepository;
import com.openisle.service.*; 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 lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit;
@RestController @RestController
@RequestMapping("/api/auth") @RequestMapping("/api/auth")
@@ -52,9 +43,6 @@ public class AuthController {
private boolean loginCaptchaEnabled; private boolean loginCaptchaEnabled;
@PostMapping("/register") @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) { public ResponseEntity<?> register(@RequestBody RegisterRequest req) {
if (captchaEnabled && registerCaptchaEnabled && !captchaService.verify(req.getCaptcha())) { if (captchaEnabled && registerCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha")); return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha"));
@@ -68,8 +56,7 @@ public class AuthController {
User user = userService.registerWithInvite( User user = userService.registerWithInvite(
req.getUsername(), req.getEmail(), req.getPassword()); req.getUsername(), req.getEmail(), req.getPassword());
inviteService.consume(req.getInviteToken(), user.getUsername()); inviteService.consume(req.getInviteToken(), user.getUsername());
// 发送确认邮件 emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
userService.sendVerifyMail(user, VerifyType.REGISTER);
return ResponseEntity.ok(Map.of( return ResponseEntity.ok(Map.of(
"token", jwtService.generateToken(user.getUsername()), "token", jwtService.generateToken(user.getUsername()),
"reason_code", "INVITE_APPROVED" "reason_code", "INVITE_APPROVED"
@@ -83,8 +70,7 @@ public class AuthController {
} }
User user = userService.register( User user = userService.register(
req.getUsername(), req.getEmail(), req.getPassword(), "", registerModeService.getRegisterMode()); req.getUsername(), req.getEmail(), req.getPassword(), "", registerModeService.getRegisterMode());
// 发送确认邮件 emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
userService.sendVerifyMail(user, VerifyType.REGISTER);
if (!user.isApproved()) { if (!user.isApproved()) {
notificationService.createRegisterRequestNotifications(user, user.getRegisterReason()); notificationService.createRegisterRequestNotifications(user, user.getRegisterReason());
} }
@@ -92,16 +78,14 @@ public class AuthController {
} }
@PostMapping("/verify") @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) { public ResponseEntity<?> verify(@RequestBody VerifyRequest req) {
Optional<User> userOpt = userService.findByUsername(req.getUsername()); boolean ok = userService.verifyCode(req.getUsername(), req.getCode());
if (userOpt.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid credentials"));
}
boolean ok = userService.verifyCode(userOpt.get(), req.getCode(), VerifyType.REGISTER);
if (ok) { if (ok) {
Optional<User> userOpt = userService.findByUsername(req.getUsername());
if (userOpt.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid credentials"));
}
User user = userOpt.get(); User user = userOpt.get();
if (user.isApproved()) { if (user.isApproved()) {
@@ -122,9 +106,6 @@ public class AuthController {
} }
@PostMapping("/login") @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) { public ResponseEntity<?> login(@RequestBody LoginRequest req) {
if (captchaEnabled && loginCaptchaEnabled && !captchaService.verify(req.getCaptcha())) { if (captchaEnabled && loginCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha")); return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha"));
@@ -141,7 +122,7 @@ public class AuthController {
User user = userOpt.get(); User user = userOpt.get();
if (!user.isVerified()) { if (!user.isVerified()) {
user = userService.register(user.getUsername(), user.getEmail(), user.getPassword(), user.getRegisterReason(), registerModeService.getRegisterMode()); user = userService.register(user.getUsername(), user.getEmail(), user.getPassword(), user.getRegisterReason(), registerModeService.getRegisterMode());
userService.sendVerifyMail(user, VerifyType.REGISTER); emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
return ResponseEntity.badRequest().body(Map.of( return ResponseEntity.badRequest().body(Map.of(
"error", "User not verified", "error", "User not verified",
"reason_code", "NOT_VERIFIED", "reason_code", "NOT_VERIFIED",
@@ -163,9 +144,6 @@ public class AuthController {
} }
@PostMapping("/google") @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) { public ResponseEntity<?> loginWithGoogle(@RequestBody GoogleLoginRequest req) {
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty(); boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken()); InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
@@ -213,9 +191,6 @@ public class AuthController {
@PostMapping("/reason") @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) { public ResponseEntity<?> reason(@RequestBody MakeReasonRequest req) {
String username = jwtService.validateAndGetSubjectForReason(req.getToken()); String username = jwtService.validateAndGetSubjectForReason(req.getToken());
Optional<User> userOpt = userService.findByUsername(username); Optional<User> userOpt = userService.findByUsername(username);
@@ -244,9 +219,6 @@ public class AuthController {
} }
@PostMapping("/github") @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) { public ResponseEntity<?> loginWithGithub(@RequestBody GithubLoginRequest req) {
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty(); boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken()); InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
@@ -295,9 +267,6 @@ public class AuthController {
} }
@PostMapping("/discord") @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) { public ResponseEntity<?> loginWithDiscord(@RequestBody DiscordLoginRequest req) {
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty(); boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken()); InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
@@ -345,9 +314,6 @@ public class AuthController {
} }
@PostMapping("/twitter") @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) { public ResponseEntity<?> loginWithTwitter(@RequestBody TwitterLoginRequest req) {
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty(); boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken()); InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
@@ -396,9 +362,6 @@ public class AuthController {
} }
@PostMapping("/telegram") @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) { public ResponseEntity<?> loginWithTelegram(@RequestBody TelegramLoginRequest req) {
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty(); boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken()); InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
@@ -444,37 +407,24 @@ public class AuthController {
} }
@GetMapping("/check") @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() { public ResponseEntity<?> checkToken() {
return ResponseEntity.ok(Map.of("valid", true)); return ResponseEntity.ok(Map.of("valid", true));
} }
@PostMapping("/forgot/send") @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) { public ResponseEntity<?> sendReset(@RequestBody ForgotPasswordRequest req) {
Optional<User> userOpt = userService.findByEmail(req.getEmail()); Optional<User> userOpt = userService.findByEmail(req.getEmail());
if (userOpt.isEmpty()) { if (userOpt.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "User not found")); return ResponseEntity.badRequest().body(Map.of("error", "User not found"));
} }
userService.sendVerifyMail(userOpt.get(), VerifyType.RESET_PASSWORD); String code = userService.generatePasswordResetCode(req.getEmail());
emailService.sendEmail(req.getEmail(), "请填写验证码以重置密码", "您的验证码是" + code);
return ResponseEntity.ok(Map.of("message", "Verification code sent")); return ResponseEntity.ok(Map.of("message", "Verification code sent"));
} }
@PostMapping("/forgot/verify") @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) { public ResponseEntity<?> verifyReset(@RequestBody VerifyForgotRequest req) {
Optional<User> userOpt = userService.findByEmail(req.getEmail()); boolean ok = userService.verifyPasswordResetCode(req.getEmail(), req.getCode());
if (userOpt.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "User not found"));
}
boolean ok = userService.verifyCode(userOpt.get(), req.getCode(), VerifyType.RESET_PASSWORD);
if (ok) { if (ok) {
String username = userService.findByEmail(req.getEmail()).get().getUsername(); String username = userService.findByEmail(req.getEmail()).get().getUsername();
return ResponseEntity.ok(Map.of("token", jwtService.generateResetToken(username))); return ResponseEntity.ok(Map.of("token", jwtService.generateResetToken(username)));
@@ -483,9 +433,6 @@ public class AuthController {
} }
@PostMapping("/forgot/reset") @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) { public ResponseEntity<?> resetPassword(@RequestBody ResetPasswordRequest req) {
String username = jwtService.validateAndGetSubjectForReset(req.getToken()); String username = jwtService.validateAndGetSubjectForReset(req.getToken());
try { try {

View File

@@ -10,11 +10,6 @@ import com.openisle.service.CategoryService;
import com.openisle.service.PostService; import com.openisle.service.PostService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; 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.List;
import java.util.Map; import java.util.Map;
@@ -30,9 +25,6 @@ public class CategoryController {
private final CategoryMapper categoryMapper; private final CategoryMapper categoryMapper;
@PostMapping @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) { public CategoryDto create(@RequestBody CategoryRequest req) {
Category c = categoryService.createCategory(req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon()); Category c = categoryService.createCategory(req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
long count = postService.countPostsByCategory(c.getId()); long count = postService.countPostsByCategory(c.getId());
@@ -40,9 +32,6 @@ public class CategoryController {
} }
@PutMapping("/{id}") @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) { public CategoryDto update(@PathVariable Long id, @RequestBody CategoryRequest req) {
Category c = categoryService.updateCategory(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon()); Category c = categoryService.updateCategory(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
long count = postService.countPostsByCategory(c.getId()); long count = postService.countPostsByCategory(c.getId());
@@ -50,16 +39,11 @@ public class CategoryController {
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@Operation(summary = "Delete category", description = "Remove a category by id")
@ApiResponse(responseCode = "200", description = "Category deleted")
public void delete(@PathVariable Long id) { public void delete(@PathVariable Long id) {
categoryService.deleteCategory(id); categoryService.deleteCategory(id);
} }
@GetMapping @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() { public List<CategoryDto> list() {
List<Category> all = categoryService.listCategories(); List<Category> all = categoryService.listCategories();
List<Long> ids = all.stream().map(Category::getId).toList(); List<Long> ids = all.stream().map(Category::getId).toList();
@@ -71,9 +55,6 @@ public class CategoryController {
} }
@GetMapping("/{id}") @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) { public CategoryDto get(@PathVariable Long id) {
Category c = categoryService.getCategory(id); Category c = categoryService.getCategory(id);
long count = postService.countPostsByCategory(c.getId()); long count = postService.countPostsByCategory(c.getId());
@@ -81,9 +62,6 @@ public class CategoryController {
} }
@GetMapping("/{id}/posts") @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, public List<PostSummaryDto> listPostsByCategory(@PathVariable Long id,
@RequestParam(value = "page", required = false) Integer page, @RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "pageSize", required = false) Integer pageSize) { @RequestParam(value = "pageSize", required = false) Integer pageSize) {

View File

@@ -8,12 +8,6 @@ import com.openisle.service.MessageService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; 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.List;
@@ -32,28 +26,16 @@ public class ChannelController {
} }
@GetMapping @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) { public List<ChannelDto> listChannels(Authentication auth) {
return channelService.listChannels(getCurrentUserId(auth)); return channelService.listChannels(getCurrentUserId(auth));
} }
@PostMapping("/{channelId}/join") @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) { public ChannelDto joinChannel(@PathVariable Long channelId, Authentication auth) {
return channelService.joinChannel(channelId, getCurrentUserId(auth)); return channelService.joinChannel(channelId, getCurrentUserId(auth));
} }
@GetMapping("/unread-count") @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) { public long unreadCount(Authentication auth) {
return messageService.getUnreadChannelCount(getCurrentUserId(auth)); return messageService.getUnreadChannelCount(getCurrentUserId(auth));
} }

View File

@@ -1,29 +1,20 @@
package com.openisle.controller; package com.openisle.controller;
import com.openisle.dto.PostChangeLogDto;
import com.openisle.dto.TimelineItemDto;
import com.openisle.mapper.PostChangeLogMapper;
import com.openisle.model.Comment; import com.openisle.model.Comment;
import com.openisle.dto.CommentDto; import com.openisle.dto.CommentDto;
import com.openisle.dto.CommentRequest; import com.openisle.dto.CommentRequest;
import com.openisle.mapper.CommentMapper; import com.openisle.mapper.CommentMapper;
import com.openisle.model.CommentSort; import com.openisle.service.CaptchaService;
import com.openisle.service.*; import com.openisle.service.CommentService;
import com.openisle.service.LevelService;
import com.openisle.service.PointService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; 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.ArrayList;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -37,8 +28,6 @@ public class CommentController {
private final CaptchaService captchaService; private final CaptchaService captchaService;
private final CommentMapper commentMapper; private final CommentMapper commentMapper;
private final PointService pointService; private final PointService pointService;
private final PostChangeLogService changeLogService;
private final PostChangeLogMapper postChangeLogMapper;
@Value("${app.captcha.enabled:false}") @Value("${app.captcha.enabled:false}")
private boolean captchaEnabled; private boolean captchaEnabled;
@@ -47,10 +36,6 @@ public class CommentController {
private boolean commentCaptchaEnabled; private boolean commentCaptchaEnabled;
@PostMapping("/posts/{postId}/comments") @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, public ResponseEntity<CommentDto> createComment(@PathVariable Long postId,
@RequestBody CommentRequest req, @RequestBody CommentRequest req,
Authentication auth) { Authentication auth) {
@@ -68,10 +53,6 @@ public class CommentController {
} }
@PostMapping("/comments/{commentId}/replies") @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, public ResponseEntity<CommentDto> replyComment(@PathVariable Long commentId,
@RequestBody CommentRequest req, @RequestBody CommentRequest req,
Authentication auth) { Authentication auth) {
@@ -88,51 +69,17 @@ public class CommentController {
} }
@GetMapping("/posts/{postId}/comments") @GetMapping("/posts/{postId}/comments")
@Operation(summary = "List comments", description = "List comments for a post") public List<CommentDto> listComments(@PathVariable Long postId,
@ApiResponse(responseCode = "200", description = "Comments", @RequestParam(value = "sort", required = false, defaultValue = "OLDEST") com.openisle.model.CommentSort sort) {
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TimelineItemDto.class))))
public List<TimelineItemDto<?>> listComments(@PathVariable Long postId,
@RequestParam(value = "sort", required = false, defaultValue = "OLDEST") CommentSort sort) {
log.debug("listComments called for post {} with sort {}", postId, sort); log.debug("listComments called for post {} with sort {}", postId, sort);
List<CommentDto> commentDtoList = commentService.getCommentsForPost(postId, sort).stream() List<CommentDto> list = commentService.getCommentsForPost(postId, sort).stream()
.map(commentMapper::toDtoWithReplies) .map(commentMapper::toDtoWithReplies)
.collect(Collectors.toList()); .collect(Collectors.toList());
List<PostChangeLogDto> postChangeLogDtoList = changeLogService.listLogs(postId).stream() log.debug("listComments returning {} comments", list.size());
.map(postChangeLogMapper::toDto) return list;
.collect(Collectors.toList());
List<TimelineItemDto<?>> itemDtoList = new ArrayList<>();
itemDtoList.addAll(commentDtoList.stream()
.map(c -> new TimelineItemDto<>(
c.getId(),
"comment",
c.getCreatedAt(),
c // payload 是 CommentDto
))
.toList());
itemDtoList.addAll(postChangeLogDtoList.stream()
.map(l -> new TimelineItemDto<>(
l.getId(),
"log",
l.getTime(), // 注意字段名不一样
l // payload 是 PostChangeLogDto
))
.toList());
// 排序
Comparator<TimelineItemDto<?>> comparator = Comparator.comparing(TimelineItemDto::getCreatedAt);
if (CommentSort.NEWEST.equals(sort)) {
comparator = comparator.reversed();
}
itemDtoList.sort(comparator);
log.debug("listComments returning {} comments", itemDtoList.size());
return itemDtoList;
} }
@DeleteMapping("/comments/{id}") @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) { public void deleteComment(@PathVariable Long id, Authentication auth) {
log.debug("deleteComment called by user {} for comment {}", auth.getName(), id); log.debug("deleteComment called by user {} for comment {}", auth.getName(), id);
commentService.deleteComment(auth.getName(), id); commentService.deleteComment(auth.getName(), id);
@@ -140,20 +87,12 @@ public class CommentController {
} }
@PostMapping("/comments/{id}/pin") @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) { public CommentDto pinComment(@PathVariable Long id, Authentication auth) {
log.debug("pinComment called by user {} for comment {}", auth.getName(), id); log.debug("pinComment called by user {} for comment {}", auth.getName(), id);
return commentMapper.toDto(commentService.pinComment(auth.getName(), id)); return commentMapper.toDto(commentService.pinComment(auth.getName(), id));
} }
@PostMapping("/comments/{id}/unpin") @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) { public CommentDto unpinComment(@PathVariable Long id, Authentication auth) {
log.debug("unpinComment called by user {} for comment {}", auth.getName(), id); log.debug("unpinComment called by user {} for comment {}", auth.getName(), id);
return commentMapper.toDto(commentService.unpinComment(auth.getName(), id)); return commentMapper.toDto(commentService.unpinComment(auth.getName(), id));

View File

@@ -6,10 +6,6 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; 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 @RestController
@RequestMapping("/api") @RequestMapping("/api")
@@ -37,9 +33,6 @@ public class ConfigController {
private final RegisterModeService registerModeService; private final RegisterModeService registerModeService;
@GetMapping("/config") @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() { public SiteConfigDto getConfig() {
SiteConfigDto resp = new SiteConfigDto(); SiteConfigDto resp = new SiteConfigDto();
resp.setCaptchaEnabled(captchaEnabled); resp.setCaptchaEnabled(captchaEnabled);

View File

@@ -9,11 +9,6 @@ import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; 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 @RestController
@RequestMapping("/api/drafts") @RequestMapping("/api/drafts")
@@ -23,20 +18,12 @@ public class DraftController {
private final DraftMapper draftMapper; private final DraftMapper draftMapper;
@PostMapping @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) { public ResponseEntity<DraftDto> saveDraft(@RequestBody DraftRequest req, Authentication auth) {
Draft draft = draftService.saveDraft(auth.getName(), req.getCategoryId(), req.getTitle(), req.getContent(), req.getTagIds()); Draft draft = draftService.saveDraft(auth.getName(), req.getCategoryId(), req.getTitle(), req.getContent(), req.getTagIds());
return ResponseEntity.ok(draftMapper.toDto(draft)); return ResponseEntity.ok(draftMapper.toDto(draft));
} }
@GetMapping("/me") @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) { public ResponseEntity<DraftDto> getMyDraft(Authentication auth) {
return draftService.getDraft(auth.getName()) return draftService.getDraft(auth.getName())
.map(d -> ResponseEntity.ok(draftMapper.toDto(d))) .map(d -> ResponseEntity.ok(draftMapper.toDto(d)))
@@ -44,9 +31,6 @@ public class DraftController {
} }
@DeleteMapping("/me") @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) { public ResponseEntity<?> deleteMyDraft(Authentication auth) {
draftService.deleteDraft(auth.getName()); draftService.deleteDraft(auth.getName());
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();

View File

@@ -1,10 +1,5 @@
package com.openisle.controller; 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.GetMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.Map; import java.util.Map;
@@ -12,10 +7,6 @@ import java.util.Map;
@RestController @RestController
public class HelloController { public class HelloController {
@GetMapping("/api/hello") @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() { public Map<String, String> hello() {
return Map.of("message", "Hello, Authenticated User"); return Map.of("message", "Hello, Authenticated User");
} }

View File

@@ -6,11 +6,6 @@ import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; 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; import java.util.Map;
@@ -21,10 +16,6 @@ public class InviteController {
private final InviteService inviteService; private final InviteService inviteService;
@PostMapping("/generate") @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) { public Map<String, String> generate(Authentication auth) {
String token = inviteService.generate(auth.getName()); String token = inviteService.generate(auth.getName());
return Map.of("token", token); return Map.of("token", token);

View File

@@ -7,12 +7,6 @@ import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; 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.List;
@@ -23,17 +17,11 @@ public class MedalController {
private final MedalService medalService; private final MedalService medalService;
@GetMapping @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) { public List<MedalDto> getMedals(@RequestParam(value = "userId", required = false) Long userId) {
return medalService.getMedals(userId); return medalService.getMedals(userId);
} }
@PostMapping("/select") @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) { public ResponseEntity<Void> selectMedal(@RequestBody MedalSelectRequest req, Authentication auth) {
try { try {
medalService.selectMedal(auth.getName(), req.getType()); medalService.selectMedal(auth.getName(), req.getType());

View File

@@ -18,12 +18,6 @@ import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; 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.List;
@@ -43,20 +37,12 @@ public class MessageController {
} }
@GetMapping("/conversations") @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) { public ResponseEntity<List<ConversationDto>> getConversations(Authentication auth) {
List<ConversationDto> conversations = messageService.getConversations(getCurrentUserId(auth)); List<ConversationDto> conversations = messageService.getConversations(getCurrentUserId(auth));
return ResponseEntity.ok(conversations); return ResponseEntity.ok(conversations);
} }
@GetMapping("/conversations/{conversationId}") @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, public ResponseEntity<ConversationDetailDto> getMessages(@PathVariable Long conversationId,
@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size, @RequestParam(defaultValue = "20") int size,
@@ -67,20 +53,12 @@ public class MessageController {
} }
@PostMapping @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) { public ResponseEntity<MessageDto> sendMessage(@RequestBody MessageRequest req, Authentication auth) {
Message message = messageService.sendMessage(getCurrentUserId(auth), req.getRecipientId(), req.getContent(), req.getReplyToId()); Message message = messageService.sendMessage(getCurrentUserId(auth), req.getRecipientId(), req.getContent(), req.getReplyToId());
return ResponseEntity.ok(messageService.toDto(message)); return ResponseEntity.ok(messageService.toDto(message));
} }
@PostMapping("/conversations/{conversationId}/messages") @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, public ResponseEntity<MessageDto> sendMessageToConversation(@PathVariable Long conversationId,
@RequestBody ChannelMessageRequest req, @RequestBody ChannelMessageRequest req,
Authentication auth) { Authentication auth) {
@@ -89,29 +67,18 @@ public class MessageController {
} }
@PostMapping("/conversations/{conversationId}/read") @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) { public ResponseEntity<Void> markAsRead(@PathVariable Long conversationId, Authentication auth) {
messageService.markConversationAsRead(conversationId, getCurrentUserId(auth)); messageService.markConversationAsRead(conversationId, getCurrentUserId(auth));
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@PostMapping("/conversations") @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) { public ResponseEntity<CreateConversationResponse> findOrCreateConversation(@RequestBody CreateConversationRequest req, Authentication auth) {
MessageConversation conversation = messageService.findOrCreateConversation(getCurrentUserId(auth), req.getRecipientId()); MessageConversation conversation = messageService.findOrCreateConversation(getCurrentUserId(auth), req.getRecipientId());
return ResponseEntity.ok(new CreateConversationResponse(conversation.getId())); return ResponseEntity.ok(new CreateConversationResponse(conversation.getId()));
} }
@GetMapping("/unread-count") @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) { public ResponseEntity<Long> getUnreadCount(Authentication auth) {
return ResponseEntity.ok(messageService.getUnreadMessageCount(getCurrentUserId(auth))); return ResponseEntity.ok(messageService.getUnreadMessageCount(getCurrentUserId(auth)));
} }

View File

@@ -10,12 +10,6 @@ import com.openisle.service.NotificationService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; 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.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -29,10 +23,6 @@ public class NotificationController {
private final NotificationMapper notificationMapper; private final NotificationMapper notificationMapper;
@GetMapping @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, public List<NotificationDto> list(@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "30") int size, @RequestParam(value = "size", defaultValue = "30") int size,
Authentication auth) { Authentication auth) {
@@ -42,10 +32,6 @@ public class NotificationController {
} }
@GetMapping("/unread") @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, public List<NotificationDto> listUnread(@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "30") int size, @RequestParam(value = "size", defaultValue = "30") int size,
Authentication auth) { Authentication auth) {
@@ -55,10 +41,6 @@ public class NotificationController {
} }
@GetMapping("/unread-count") @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) { public NotificationUnreadCountDto unreadCount(Authentication auth) {
long count = notificationService.countUnread(auth.getName()); long count = notificationService.countUnread(auth.getName());
NotificationUnreadCountDto uc = new NotificationUnreadCountDto(); NotificationUnreadCountDto uc = new NotificationUnreadCountDto();
@@ -67,43 +49,26 @@ public class NotificationController {
} }
@PostMapping("/read") @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) { public void markRead(@RequestBody NotificationMarkReadRequest req, Authentication auth) {
notificationService.markRead(auth.getName(), req.getIds()); notificationService.markRead(auth.getName(), req.getIds());
} }
@GetMapping("/prefs") @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) { public List<NotificationPreferenceDto> prefs(Authentication auth) {
return notificationService.listPreferences(auth.getName()); return notificationService.listPreferences(auth.getName());
} }
@PostMapping("/prefs") @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) { public void updatePref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) {
notificationService.updatePreference(auth.getName(), req.getType(), req.isEnabled()); notificationService.updatePreference(auth.getName(), req.getType(), req.isEnabled());
} }
@GetMapping("/email-prefs") @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) { public List<NotificationPreferenceDto> emailPrefs(Authentication auth) {
return notificationService.listEmailPreferences(auth.getName()); return notificationService.listEmailPreferences(auth.getName());
} }
@PostMapping("/email-prefs") @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) { public void updateEmailPref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) {
notificationService.updateEmailPreference(auth.getName(), req.getType(), req.isEnabled()); notificationService.updateEmailPreference(auth.getName(), req.getType(), req.isEnabled());
} }

View File

@@ -5,10 +5,6 @@ import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*; 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; import java.time.Duration;
@@ -26,16 +22,11 @@ public class OnlineController {
private static final String ONLINE_KEY = CachingConfig.ONLINE_CACHE_NAME +":"; private static final String ONLINE_KEY = CachingConfig.ONLINE_CACHE_NAME +":";
@PostMapping("/heartbeat") @PostMapping("/heartbeat")
@Operation(summary = "Heartbeat", description = "Record user heartbeat")
@ApiResponse(responseCode = "200", description = "Heartbeat recorded")
public void ping(@RequestParam String userId){ public void ping(@RequestParam String userId){
redisTemplate.opsForValue().set(ONLINE_KEY+userId,"1", Duration.ofSeconds(150)); redisTemplate.opsForValue().set(ONLINE_KEY+userId,"1", Duration.ofSeconds(150));
} }
@GetMapping("/count") @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(){ public long count(){
return redisTemplate.keys(ONLINE_KEY+"*").size(); return redisTemplate.keys(ONLINE_KEY+"*").size();
} }

View File

@@ -9,12 +9,6 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; 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.List;
import java.util.Map; import java.util.Map;
@@ -28,10 +22,6 @@ public class PointHistoryController {
private final PointHistoryMapper pointHistoryMapper; private final PointHistoryMapper pointHistoryMapper;
@GetMapping @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) { public List<PointHistoryDto> list(Authentication auth) {
return pointService.listHistory(auth.getName()).stream() return pointService.listHistory(auth.getName()).stream()
.map(pointHistoryMapper::toDto) .map(pointHistoryMapper::toDto)
@@ -39,10 +29,6 @@ public class PointHistoryController {
} }
@GetMapping("/trend") @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, public List<Map<String, Object>> trend(Authentication auth,
@RequestParam(value = "days", defaultValue = "30") int days) { @RequestParam(value = "days", defaultValue = "30") int days) {
return pointService.trend(auth.getName(), days); return pointService.trend(auth.getName(), days);

View File

@@ -9,12 +9,6 @@ import com.openisle.service.UserService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; 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.List;
import java.util.Map; import java.util.Map;
@@ -30,9 +24,6 @@ public class PointMallController {
private final PointGoodMapper pointGoodMapper; private final PointGoodMapper pointGoodMapper;
@GetMapping @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() { public List<PointGoodDto> list() {
return pointMallService.listGoods().stream() return pointMallService.listGoods().stream()
.map(pointGoodMapper::toDto) .map(pointGoodMapper::toDto)
@@ -40,10 +31,6 @@ public class PointMallController {
} }
@PostMapping("/redeem") @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) { public Map<String, Integer> redeem(@RequestBody PointRedeemRequest req, Authentication auth) {
User user = userService.findByIdentifier(auth.getName()).orElseThrow(); User user = userService.findByIdentifier(auth.getName()).orElseThrow();
int point = pointMallService.redeem(user, req.getGoodId(), req.getContact()); int point = pointMallService.redeem(user, req.getGoodId(), req.getContact());

View File

@@ -1,33 +0,0 @@
package com.openisle.controller;
import com.openisle.dto.PostChangeLogDto;
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;
@RestController
@RequestMapping("/api/posts")
@RequiredArgsConstructor
public class PostChangeLogController {
private final PostChangeLogService changeLogService;
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)
.collect(Collectors.toList());
}
}

View File

@@ -7,12 +7,6 @@ import com.openisle.dto.PollDto;
import com.openisle.mapper.PostMapper; import com.openisle.mapper.PostMapper;
import com.openisle.model.Post; import com.openisle.model.Post;
import com.openisle.service.*; 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 lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -27,8 +21,6 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor @RequiredArgsConstructor
public class PostController { public class PostController {
private final PostService postService; private final PostService postService;
private final CategoryService categoryService;
private final TagService tagService;
private final LevelService levelService; private final LevelService levelService;
private final CaptchaService captchaService; private final CaptchaService captchaService;
private final DraftService draftService; private final DraftService draftService;
@@ -43,10 +35,6 @@ public class PostController {
private boolean postCaptchaEnabled; private boolean postCaptchaEnabled;
@PostMapping @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) { public ResponseEntity<PostDetailDto> createPost(@RequestBody PostRequest req, Authentication auth) {
if (captchaEnabled && postCaptchaEnabled && !captchaService.verify(req.getCaptcha())) { if (captchaEnabled && postCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
@@ -65,10 +53,6 @@ public class PostController {
} }
@PutMapping("/{id}") @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, public ResponseEntity<PostDetailDto> updatePost(@PathVariable Long id, @RequestBody PostRequest req,
Authentication auth) { Authentication auth) {
Post post = postService.updatePost(id, auth.getName(), req.getCategoryId(), Post post = postService.updatePost(id, auth.getName(), req.getCategoryId(),
@@ -77,35 +61,21 @@ public class PostController {
} }
@DeleteMapping("/{id}") @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) { public void deletePost(@PathVariable Long id, Authentication auth) {
postService.deletePost(id, auth.getName()); postService.deletePost(id, auth.getName());
} }
@PostMapping("/{id}/close") @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) { public PostSummaryDto close(@PathVariable Long id, Authentication auth) {
return postMapper.toSummaryDto(postService.closePost(id, auth.getName())); return postMapper.toSummaryDto(postService.closePost(id, auth.getName()));
} }
@PostMapping("/{id}/reopen") @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) { public PostSummaryDto reopen(@PathVariable Long id, Authentication auth) {
return postMapper.toSummaryDto(postService.reopenPost(id, auth.getName())); return postMapper.toSummaryDto(postService.reopenPost(id, auth.getName()));
} }
@GetMapping("/{id}") @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) { public ResponseEntity<PostDetailDto> getPost(@PathVariable Long id, Authentication auth) {
String viewer = auth != null ? auth.getName() : null; String viewer = auth != null ? auth.getName() : null;
Post post = postService.viewPost(id, viewer); Post post = postService.viewPost(id, viewer);
@@ -113,35 +83,23 @@ public class PostController {
} }
@PostMapping("/{id}/lottery/join") @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) { public ResponseEntity<Void> joinLottery(@PathVariable Long id, Authentication auth) {
postService.joinLottery(id, auth.getName()); postService.joinLottery(id, auth.getName());
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@GetMapping("/{id}/poll/progress") @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) { public ResponseEntity<PollDto> pollProgress(@PathVariable Long id) {
return ResponseEntity.ok(postMapper.toSummaryDto(postService.getPoll(id)).getPoll()); return ResponseEntity.ok(postMapper.toSummaryDto(postService.getPoll(id)).getPoll());
} }
@PostMapping("/{id}/poll/vote") @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) { public ResponseEntity<Void> vote(@PathVariable Long id, @RequestParam("option") List<Integer> option, Authentication auth) {
postService.votePoll(id, auth.getName(), option); postService.votePoll(id, auth.getName(), option);
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@GetMapping @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, public List<PostSummaryDto> listPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds, @RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
@RequestParam(value = "tagId", required = false) Long tagId, @RequestParam(value = "tagId", required = false) Long tagId,
@@ -149,22 +107,36 @@ public class PostController {
@RequestParam(value = "page", required = false) Integer page, @RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "pageSize", required = false) Integer pageSize, @RequestParam(value = "pageSize", required = false) Integer pageSize,
Authentication auth) { Authentication auth) {
List<Long> ids = categoryIds;
if (categoryId != null) {
ids = java.util.List.of(categoryId);
}
List<Long> tids = tagIds;
if (tagId != null) {
tids = java.util.List.of(tagId);
}
List<Long> ids = categoryService.getSearchCategoryIds(categoryIds, categoryId); if (auth != null) {
List<Long> tids = tagService.getSearchTagIds(tagIds, tagId); userVisitService.recordVisit(auth.getName());
// 只需要在请求的一开始统计一次 }
// if (auth != null) {
// userVisitService.recordVisit(auth.getName());
// }
return postService.defaultListPosts(ids,tids,page, pageSize).stream() boolean hasCategories = ids != null && !ids.isEmpty();
.map(postMapper::toSummaryDto).collect(Collectors.toList()); boolean hasTags = tids != null && !tids.isEmpty();
if (hasCategories && hasTags) {
return postService.listPostsByCategoriesAndTags(ids, tids, page, pageSize)
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
}
if (hasTags) {
return postService.listPostsByTags(tids, page, pageSize)
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
}
return postService.listPostsByCategories(ids, page, pageSize)
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
} }
@GetMapping("/ranking") @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, public List<PostSummaryDto> rankingPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds, @RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
@RequestParam(value = "tagId", required = false) Long tagId, @RequestParam(value = "tagId", required = false) Long tagId,
@@ -172,22 +144,24 @@ public class PostController {
@RequestParam(value = "page", required = false) Integer page, @RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "pageSize", required = false) Integer pageSize, @RequestParam(value = "pageSize", required = false) Integer pageSize,
Authentication auth) { Authentication auth) {
List<Long> ids = categoryIds;
if (categoryId != null) {
ids = java.util.List.of(categoryId);
}
List<Long> tids = tagIds;
if (tagId != null) {
tids = java.util.List.of(tagId);
}
List<Long> ids = categoryService.getSearchCategoryIds(categoryIds, categoryId); if (auth != null) {
List<Long> tids = tagService.getSearchTagIds(tagIds, tagId); userVisitService.recordVisit(auth.getName());
// 只需要在请求的一开始统计一次 }
// if (auth != null) {
// userVisitService.recordVisit(auth.getName());
// }
return postService.listPostsByViews(ids, tids, page, pageSize) return postService.listPostsByViews(ids, tids, page, pageSize)
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList()); .stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
} }
@GetMapping("/latest-reply") @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, public List<PostSummaryDto> latestReplyPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds, @RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
@RequestParam(value = "tagId", required = false) Long tagId, @RequestParam(value = "tagId", required = false) Long tagId,
@@ -195,22 +169,24 @@ public class PostController {
@RequestParam(value = "page", required = false) Integer page, @RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "pageSize", required = false) Integer pageSize, @RequestParam(value = "pageSize", required = false) Integer pageSize,
Authentication auth) { Authentication auth) {
List<Long> ids = categoryIds;
if (categoryId != null) {
ids = java.util.List.of(categoryId);
}
List<Long> tids = tagIds;
if (tagId != null) {
tids = java.util.List.of(tagId);
}
List<Long> ids = categoryService.getSearchCategoryIds(categoryIds, categoryId); if (auth != null) {
List<Long> tids = tagService.getSearchTagIds(tagIds, tagId); userVisitService.recordVisit(auth.getName());
// 只需要在请求的一开始统计一次 }
// if (auth != null) {
// userVisitService.recordVisit(auth.getName());
// }
List<Post> posts = postService.listPostsByLatestReply(ids, tids, page, pageSize); return postService.listPostsByLatestReply(ids, tids, page, pageSize)
return posts.stream().map(postMapper::toSummaryDto).collect(Collectors.toList()); .stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
} }
@GetMapping("/featured") @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, public List<PostSummaryDto> featuredPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds, @RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
@RequestParam(value = "tagId", required = false) Long tagId, @RequestParam(value = "tagId", required = false) Long tagId,
@@ -218,13 +194,17 @@ public class PostController {
@RequestParam(value = "page", required = false) Integer page, @RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "pageSize", required = false) Integer pageSize, @RequestParam(value = "pageSize", required = false) Integer pageSize,
Authentication auth) { Authentication auth) {
List<Long> ids = categoryIds;
List<Long> ids = categoryService.getSearchCategoryIds(categoryIds, categoryId); if (categoryId != null) {
List<Long> tids = tagService.getSearchTagIds(tagIds, tagId); ids = java.util.List.of(categoryId);
// 只需要在请求的一开始统计一次 }
// if (auth != null) { List<Long> tids = tagIds;
// userVisitService.recordVisit(auth.getName()); if (tagId != null) {
// } tids = java.util.List.of(tagId);
}
if (auth != null) {
userVisitService.recordVisit(auth.getName());
}
return postService.listFeaturedPosts(ids, tids, page, pageSize) return postService.listFeaturedPosts(ids, tids, page, pageSize)
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList()); .stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
} }

View File

@@ -7,11 +7,6 @@ import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; 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 @RestController
@RequestMapping("/api/push") @RequestMapping("/api/push")
@@ -22,9 +17,6 @@ public class PushSubscriptionController {
private String publicKey; private String publicKey;
@GetMapping("/public-key") @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() { public PushPublicKeyDto getPublicKey() {
PushPublicKeyDto r = new PushPublicKeyDto(); PushPublicKeyDto r = new PushPublicKeyDto();
r.setKey(publicKey); r.setKey(publicKey);
@@ -32,9 +24,6 @@ public class PushSubscriptionController {
} }
@PostMapping("/subscribe") @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) { public void subscribe(@RequestBody PushSubscriptionRequest req, Authentication auth) {
pushSubscriptionService.saveSubscription(auth.getName(), req.getEndpoint(), req.getP256dh(), req.getAuth()); pushSubscriptionService.saveSubscription(auth.getName(), req.getEndpoint(), req.getP256dh(), req.getAuth());
} }

View File

@@ -12,11 +12,6 @@ import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; 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 @RestController
@RequestMapping("/api") @RequestMapping("/api")
@@ -31,18 +26,11 @@ public class ReactionController {
* Get all available reaction types. * Get all available reaction types.
*/ */
@GetMapping("/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() { public ReactionType[] listReactionTypes() {
return ReactionType.values(); return ReactionType.values();
} }
@PostMapping("/posts/{postId}/reactions") @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, public ResponseEntity<ReactionDto> reactToPost(@PathVariable Long postId,
@RequestBody ReactionRequest req, @RequestBody ReactionRequest req,
Authentication auth) { Authentication auth) {
@@ -58,10 +46,6 @@ public class ReactionController {
} }
@PostMapping("/comments/{commentId}/reactions") @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, public ResponseEntity<ReactionDto> reactToComment(@PathVariable Long commentId,
@RequestBody ReactionRequest req, @RequestBody ReactionRequest req,
Authentication auth) { Authentication auth) {
@@ -77,10 +61,6 @@ public class ReactionController {
} }
@PostMapping("/messages/{messageId}/reactions") @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, public ResponseEntity<ReactionDto> reactToMessage(@PathVariable Long messageId,
@RequestBody ReactionRequest req, @RequestBody ReactionRequest req,
Authentication auth) { Authentication auth) {

View File

@@ -13,10 +13,6 @@ import org.jsoup.safety.Safelist;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; 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.autolink.AutolinkExtension;
import com.vladsch.flexmark.ext.tables.TablesExtension; import com.vladsch.flexmark.ext.tables.TablesExtension;
@@ -67,8 +63,6 @@ public class RssController {
} }
@GetMapping(value = "/api/rss", produces = "application/rss+xml;charset=UTF-8") @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() { public String feed() {
// 建议 20你现在是 10这里保留你的 10 // 建议 20你现在是 10这里保留你的 10
List<Post> posts = postService.listLatestRssPosts(10); List<Post> posts = postService.listLatestRssPosts(10);

View File

@@ -11,11 +11,6 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; 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.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -29,9 +24,6 @@ public class SearchController {
private final PostMapper postMapper; private final PostMapper postMapper;
@GetMapping("/users") @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) { public List<UserDto> searchUsers(@RequestParam String keyword) {
return searchService.searchUsers(keyword).stream() return searchService.searchUsers(keyword).stream()
.map(userMapper::toDto) .map(userMapper::toDto)
@@ -39,9 +31,6 @@ public class SearchController {
} }
@GetMapping("/posts") @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) { public List<PostSummaryDto> searchPosts(@RequestParam String keyword) {
return searchService.searchPosts(keyword).stream() return searchService.searchPosts(keyword).stream()
.map(postMapper::toSummaryDto) .map(postMapper::toSummaryDto)
@@ -49,9 +38,6 @@ public class SearchController {
} }
@GetMapping("/posts/content") @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) { public List<PostSummaryDto> searchPostsByContent(@RequestParam String keyword) {
return searchService.searchPostsByContent(keyword).stream() return searchService.searchPostsByContent(keyword).stream()
.map(postMapper::toSummaryDto) .map(postMapper::toSummaryDto)
@@ -59,9 +45,6 @@ public class SearchController {
} }
@GetMapping("/posts/title") @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) { public List<PostSummaryDto> searchPostsByTitle(@RequestParam String keyword) {
return searchService.searchPostsByTitle(keyword).stream() return searchService.searchPostsByTitle(keyword).stream()
.map(postMapper::toSummaryDto) .map(postMapper::toSummaryDto)
@@ -69,9 +52,6 @@ public class SearchController {
} }
@GetMapping("/global") @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) { public List<SearchResultDto> global(@RequestParam String keyword) {
return searchService.globalSearch(keyword).stream() return searchService.globalSearch(keyword).stream()
.map(r -> { .map(r -> {

View File

@@ -10,10 +10,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; 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; import java.util.List;
@@ -30,9 +26,6 @@ public class SitemapController {
private String websiteUrl; private String websiteUrl;
@GetMapping(value = "/sitemap.xml", produces = MediaType.APPLICATION_XML_VALUE) @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() { public ResponseEntity<String> sitemap() {
List<Post> posts = postRepository.findByStatus(PostStatus.PUBLISHED); List<Post> posts = postRepository.findByStatus(PostStatus.PUBLISHED);

View File

@@ -8,11 +8,6 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; 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.time.LocalDate;
import java.util.List; import java.util.List;
@@ -26,9 +21,6 @@ public class StatController {
private final StatService statService; private final StatService statService;
@GetMapping("/dau") @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) public Map<String, Long> dau(@RequestParam(value = "date", required = false)
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) { @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
long count = userVisitService.countDau(date); long count = userVisitService.countDau(date);
@@ -36,9 +28,6 @@ public class StatController {
} }
@GetMapping("/dau-range") @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) { public List<Map<String, Object>> dauRange(@RequestParam(value = "days", defaultValue = "30") int days) {
if (days < 1) days = 1; if (days < 1) days = 1;
LocalDate end = LocalDate.now(); LocalDate end = LocalDate.now();
@@ -53,9 +42,6 @@ public class StatController {
} }
@GetMapping("/new-users-range") @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) { public List<Map<String, Object>> newUsersRange(@RequestParam(value = "days", defaultValue = "30") int days) {
if (days < 1) days = 1; if (days < 1) days = 1;
LocalDate end = LocalDate.now(); LocalDate end = LocalDate.now();
@@ -70,9 +56,6 @@ public class StatController {
} }
@GetMapping("/posts-range") @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) { public List<Map<String, Object>> postsRange(@RequestParam(value = "days", defaultValue = "30") int days) {
if (days < 1) days = 1; if (days < 1) days = 1;
LocalDate end = LocalDate.now(); LocalDate end = LocalDate.now();
@@ -87,9 +70,6 @@ public class StatController {
} }
@GetMapping("/comments-range") @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) { public List<Map<String, Object>> commentsRange(@RequestParam(value = "days", defaultValue = "30") int days) {
if (days < 1) days = 1; if (days < 1) days = 1;
LocalDate end = LocalDate.now(); LocalDate end = LocalDate.now();

View File

@@ -4,9 +4,6 @@ import com.openisle.service.SubscriptionService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; 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. */ /** Endpoints for subscribing to posts, comments and users. */
@RestController @RestController
@@ -16,49 +13,31 @@ public class SubscriptionController {
private final SubscriptionService subscriptionService; private final SubscriptionService subscriptionService;
@PostMapping("/posts/{postId}") @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) { public void subscribePost(@PathVariable Long postId, Authentication auth) {
subscriptionService.subscribePost(auth.getName(), postId); subscriptionService.subscribePost(auth.getName(), postId);
} }
@DeleteMapping("/posts/{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) { public void unsubscribePost(@PathVariable Long postId, Authentication auth) {
subscriptionService.unsubscribePost(auth.getName(), postId); subscriptionService.unsubscribePost(auth.getName(), postId);
} }
@PostMapping("/comments/{commentId}") @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) { public void subscribeComment(@PathVariable Long commentId, Authentication auth) {
subscriptionService.subscribeComment(auth.getName(), commentId); subscriptionService.subscribeComment(auth.getName(), commentId);
} }
@DeleteMapping("/comments/{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) { public void unsubscribeComment(@PathVariable Long commentId, Authentication auth) {
subscriptionService.unsubscribeComment(auth.getName(), commentId); subscriptionService.unsubscribeComment(auth.getName(), commentId);
} }
@PostMapping("/users/{username}") @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) { public void subscribeUser(@PathVariable String username, Authentication auth) {
subscriptionService.subscribeUser(auth.getName(), username); subscriptionService.subscribeUser(auth.getName(), username);
} }
@DeleteMapping("/users/{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) { public void unsubscribeUser(@PathVariable String username, Authentication auth) {
subscriptionService.unsubscribeUser(auth.getName(), username); subscriptionService.unsubscribeUser(auth.getName(), username);
} }

View File

@@ -13,12 +13,6 @@ import com.openisle.service.PostService;
import com.openisle.service.TagService; import com.openisle.service.TagService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; 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.List;
import java.util.Map; import java.util.Map;
@@ -35,10 +29,6 @@ public class TagController {
private final TagMapper tagMapper; private final TagMapper tagMapper;
@PostMapping @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) { public TagDto create(@RequestBody TagRequest req, org.springframework.security.core.Authentication auth) {
boolean approved = true; boolean approved = true;
if (postService.getPublishMode() == PublishMode.REVIEW && auth != null) { if (postService.getPublishMode() == PublishMode.REVIEW && auth != null) {
@@ -59,9 +49,6 @@ public class TagController {
} }
@PutMapping("/{id}") @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) { public TagDto update(@PathVariable Long id, @RequestBody TagRequest req) {
Tag tag = tagService.updateTag(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon()); Tag tag = tagService.updateTag(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
long count = postService.countPostsByTag(tag.getId()); long count = postService.countPostsByTag(tag.getId());
@@ -69,16 +56,11 @@ public class TagController {
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@Operation(summary = "Delete tag", description = "Delete a tag by id")
@ApiResponse(responseCode = "200", description = "Tag deleted")
public void delete(@PathVariable Long id) { public void delete(@PathVariable Long id) {
tagService.deleteTag(id); tagService.deleteTag(id);
} }
@GetMapping @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, public List<TagDto> list(@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "limit", required = false) Integer limit) { @RequestParam(value = "limit", required = false) Integer limit) {
List<Tag> tags = tagService.searchTags(keyword); List<Tag> tags = tagService.searchTags(keyword);
@@ -95,9 +77,6 @@ public class TagController {
} }
@GetMapping("/{id}") @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) { public TagDto get(@PathVariable Long id) {
Tag tag = tagService.getTag(id); Tag tag = tagService.getTag(id);
long count = postService.countPostsByTag(tag.getId()); long count = postService.countPostsByTag(tag.getId());
@@ -105,9 +84,6 @@ public class TagController {
} }
@GetMapping("/{id}/posts") @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, public List<PostSummaryDto> listPostsByTag(@PathVariable Long id,
@RequestParam(value = "page", required = false) Integer page, @RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "pageSize", required = false) Integer pageSize) { @RequestParam(value = "pageSize", required = false) Integer pageSize) {

View File

@@ -6,10 +6,6 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; 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.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
@@ -31,9 +27,6 @@ public class UploadController {
private long maxUploadSize; private long maxUploadSize;
@PostMapping @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) { public ResponseEntity<?> upload(@RequestParam("file") MultipartFile file) {
if (checkImageType && (file.getContentType() == null || !file.getContentType().startsWith("image/"))) { if (checkImageType && (file.getContentType() == null || !file.getContentType().startsWith("image/"))) {
return ResponseEntity.badRequest().body(Map.of("code", 1, "msg", "File is not an image")); return ResponseEntity.badRequest().body(Map.of("code", 1, "msg", "File is not an image"));
@@ -55,9 +48,6 @@ public class UploadController {
} }
@PostMapping("/url") @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) { public ResponseEntity<?> uploadUrl(@RequestBody Map<String, String> body) {
String link = body.get("url"); String link = body.get("url");
if (link == null || link.isBlank()) { if (link == null || link.isBlank()) {
@@ -86,9 +76,6 @@ public class UploadController {
} }
@GetMapping("/presign") @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) { public java.util.Map<String, String> presign(@RequestParam("filename") String filename) {
return imageUploader.presignUpload(filename); return imageUploader.presignUpload(filename);
} }

View File

@@ -6,12 +6,6 @@ import com.openisle.mapper.TagMapper;
import com.openisle.mapper.UserMapper; import com.openisle.mapper.UserMapper;
import com.openisle.model.User; import com.openisle.model.User;
import com.openisle.service.*; 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 lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -54,20 +48,12 @@ public class UserController {
private int defaultTagsLimit; private int defaultTagsLimit;
@GetMapping("/me") @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) { public ResponseEntity<UserDto> me(Authentication auth) {
User user = userService.findByUsername(auth.getName()).orElseThrow(); User user = userService.findByUsername(auth.getName()).orElseThrow();
return ResponseEntity.ok(userMapper.toDto(user, auth)); return ResponseEntity.ok(userMapper.toDto(user, auth));
} }
@PostMapping("/me/avatar") @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, public ResponseEntity<?> uploadAvatar(@RequestParam("file") MultipartFile file,
Authentication auth) { Authentication auth) {
if (checkImageType && (file.getContentType() == null || !file.getContentType().startsWith("image/"))) { if (checkImageType && (file.getContentType() == null || !file.getContentType().startsWith("image/"))) {
@@ -87,10 +73,6 @@ public class UserController {
} }
@PutMapping("/me") @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, public ResponseEntity<?> updateProfile(@RequestBody UpdateProfileDto dto,
Authentication auth) { Authentication auth) {
User user = userService.updateProfile(auth.getName(), dto.getUsername(), dto.getIntroduction()); User user = userService.updateProfile(auth.getName(), dto.getUsername(), dto.getIntroduction());
@@ -100,21 +82,13 @@ public class UserController {
)); ));
} }
// 这个方法似乎没有使用?
@PostMapping("/me/signin") @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) { public Map<String, Integer> signIn(Authentication auth) {
int reward = levelService.awardForSignin(auth.getName()); int reward = levelService.awardForSignin(auth.getName());
return Map.of("reward", reward); return Map.of("reward", reward);
} }
@GetMapping("/{identifier}") @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, public ResponseEntity<UserDto> getUser(@PathVariable("identifier") String identifier,
Authentication auth) { Authentication auth) {
User user = userService.findByIdentifier(identifier).orElseThrow(() -> new NotFoundException("User not found")); User user = userService.findByIdentifier(identifier).orElseThrow(() -> new NotFoundException("User not found"));
@@ -122,9 +96,6 @@ public class UserController {
} }
@GetMapping("/{identifier}/posts") @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, public java.util.List<PostMetaDto> userPosts(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) { @RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : defaultPostsLimit; int l = limit != null ? limit : defaultPostsLimit;
@@ -135,9 +106,6 @@ public class UserController {
} }
@GetMapping("/{identifier}/subscribed-posts") @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, public java.util.List<PostMetaDto> subscribedPosts(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) { @RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : defaultPostsLimit; int l = limit != null ? limit : defaultPostsLimit;
@@ -149,9 +117,6 @@ public class UserController {
} }
@GetMapping("/{identifier}/replies") @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, public java.util.List<CommentInfoDto> userReplies(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) { @RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : defaultRepliesLimit; int l = limit != null ? limit : defaultRepliesLimit;
@@ -162,9 +127,6 @@ public class UserController {
} }
@GetMapping("/{identifier}/hot-posts") @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, public java.util.List<PostMetaDto> hotPosts(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) { @RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : 10; int l = limit != null ? limit : 10;
@@ -176,9 +138,6 @@ public class UserController {
} }
@GetMapping("/{identifier}/hot-replies") @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, public java.util.List<CommentInfoDto> hotReplies(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) { @RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : 10; int l = limit != null ? limit : 10;
@@ -190,9 +149,6 @@ public class UserController {
} }
@GetMapping("/{identifier}/hot-tags") @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, public java.util.List<TagDto> hotTags(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) { @RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : 10; int l = limit != null ? limit : 10;
@@ -205,9 +161,6 @@ public class UserController {
} }
@GetMapping("/{identifier}/tags") @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, public java.util.List<TagDto> userTags(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) { @RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : defaultTagsLimit; int l = limit != null ? limit : defaultTagsLimit;
@@ -218,9 +171,6 @@ public class UserController {
} }
@GetMapping("/{identifier}/following") @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) { public java.util.List<UserDto> following(@PathVariable("identifier") String identifier) {
User user = userService.findByIdentifier(identifier).orElseThrow(); User user = userService.findByIdentifier(identifier).orElseThrow();
return subscriptionService.getSubscribedUsers(user.getUsername()).stream() return subscriptionService.getSubscribedUsers(user.getUsername()).stream()
@@ -229,9 +179,6 @@ public class UserController {
} }
@GetMapping("/{identifier}/followers") @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) { public java.util.List<UserDto> followers(@PathVariable("identifier") String identifier) {
User user = userService.findByIdentifier(identifier).orElseThrow(); User user = userService.findByIdentifier(identifier).orElseThrow();
return subscriptionService.getSubscribers(user.getUsername()).stream() return subscriptionService.getSubscribers(user.getUsername()).stream()
@@ -240,9 +187,6 @@ public class UserController {
} }
@GetMapping("/admins") @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() { public java.util.List<UserDto> admins() {
return userService.getAdmins().stream() return userService.getAdmins().stream()
.map(userMapper::toDto) .map(userMapper::toDto)
@@ -250,9 +194,6 @@ public class UserController {
} }
@GetMapping("/{identifier}/all") @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, public ResponseEntity<UserAggregateDto> userAggregate(@PathVariable("identifier") String identifier,
@RequestParam(value = "postsLimit", required = false) Integer postsLimit, @RequestParam(value = "postsLimit", required = false) Integer postsLimit,
@RequestParam(value = "repliesLimit", required = false) Integer repliesLimit, @RequestParam(value = "repliesLimit", required = false) Integer repliesLimit,

View File

@@ -1,32 +0,0 @@
package com.openisle.dto;
import com.openisle.model.PostChangeType;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.List;
@Getter
@Setter
public class PostChangeLogDto {
private Long id;
private String username;
private String userAvatar;
private PostChangeType type;
private LocalDateTime time;
private String oldTitle;
private String newTitle;
private String oldContent;
private String newContent;
private CategoryDto oldCategory;
private CategoryDto newCategory;
private List<TagDto> oldTags;
private List<TagDto> newTags;
private Boolean oldClosed;
private Boolean newClosed;
private LocalDateTime oldPinnedAt;
private LocalDateTime newPinnedAt;
private Boolean oldFeatured;
private Boolean newFeatured;
}

View File

@@ -1,20 +0,0 @@
package com.openisle.dto;
import lombok.*;
import java.time.LocalDateTime;
/**
* comment and change_log Dto
*/
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class TimelineItemDto<T> {
private Long id;
private String kind; // "comment" | "log"
private LocalDateTime createdAt;
private T payload; // 泛型,具体类型由外部决定
}

View File

@@ -1,92 +0,0 @@
package com.openisle.mapper;
import com.openisle.dto.CategoryDto;
import com.openisle.dto.PostChangeLogDto;
import com.openisle.dto.TagDto;
import com.openisle.model.*;
import com.openisle.repository.CategoryRepository;
import com.openisle.repository.TagRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
public class PostChangeLogMapper {
private final CategoryRepository categoryRepository;
private final TagRepository tagRepository;
private final CategoryMapper categoryMapper;
private final TagMapper tagMapper;
public PostChangeLogDto toDto(PostChangeLog log) {
PostChangeLogDto dto = new PostChangeLogDto();
dto.setId(log.getId());
if (log.getUser() != null) {
dto.setUsername(log.getUser().getUsername());
dto.setUserAvatar(log.getUser().getAvatar());
}
dto.setType(log.getType());
dto.setTime(log.getCreatedAt());
if (log instanceof PostTitleChangeLog t) {
dto.setOldTitle(t.getOldTitle());
dto.setNewTitle(t.getNewTitle());
} else if (log instanceof PostContentChangeLog c) {
dto.setOldContent(c.getOldContent());
dto.setNewContent(c.getNewContent());
} else if (log instanceof PostCategoryChangeLog cat) {
dto.setOldCategory(mapCategory(cat.getOldCategory()));
dto.setNewCategory(mapCategory(cat.getNewCategory()));
} else if (log instanceof PostTagChangeLog tag) {
dto.setOldTags(mapTags(tag.getOldTags()));
dto.setNewTags(mapTags(tag.getNewTags()));
} else if (log instanceof PostClosedChangeLog cl) {
dto.setOldClosed(cl.isOldClosed());
dto.setNewClosed(cl.isNewClosed());
} else if (log instanceof PostPinnedChangeLog p) {
dto.setOldPinnedAt(p.getOldPinnedAt());
dto.setNewPinnedAt(p.getNewPinnedAt());
} else if (log instanceof PostFeaturedChangeLog f) {
dto.setOldFeatured(f.isOldFeatured());
dto.setNewFeatured(f.isNewFeatured());
}
return dto;
}
private CategoryDto mapCategory(String name) {
if (name == null) {
return null;
}
return categoryRepository.findByName(name)
.map(categoryMapper::toDto)
.orElseGet(() -> {
CategoryDto dto = new CategoryDto();
dto.setName(name);
return dto;
});
}
private List<TagDto> mapTags(String tags) {
if (tags == null || tags.isBlank()) {
return Collections.emptyList();
}
return Arrays.stream(tags.split(","))
.map(String::trim)
.map(this::mapTag)
.collect(Collectors.toList());
}
private TagDto mapTag(String name) {
return tagRepository.findByName(name)
.map(tagMapper::toDto)
.orElseGet(() -> {
TagDto dto = new TagDto();
dto.setName(name);
return dto;
});
}
}

View File

@@ -67,6 +67,7 @@ public class PostMapper {
dto.setCategory(categoryMapper.toDto(post.getCategory())); dto.setCategory(categoryMapper.toDto(post.getCategory()));
dto.setTags(post.getTags().stream().map(tagMapper::toDto).collect(Collectors.toList())); dto.setTags(post.getTags().stream().map(tagMapper::toDto).collect(Collectors.toList()));
dto.setViews(post.getViews()); dto.setViews(post.getViews());
dto.setCommentCount(commentService.countComments(post.getId()));
dto.setStatus(post.getStatus()); dto.setStatus(post.getStatus());
dto.setPinnedAt(post.getPinnedAt()); dto.setPinnedAt(post.getPinnedAt());
dto.setRssExcluded(post.getRssExcluded() == null || post.getRssExcluded()); dto.setRssExcluded(post.getRssExcluded() == null || post.getRssExcluded());
@@ -81,12 +82,8 @@ public class PostMapper {
List<User> participants = commentService.getParticipants(post.getId(), 5); List<User> participants = commentService.getParticipants(post.getId(), 5);
dto.setParticipants(participants.stream().map(userMapper::toAuthorDto).collect(Collectors.toList())); dto.setParticipants(participants.stream().map(userMapper::toAuthorDto).collect(Collectors.toList()));
LocalDateTime last = post.getLastReplyAt(); LocalDateTime last = commentService.getLastCommentTime(post.getId());
if (last == null) { dto.setLastReplyAt(last != null ? last : post.getCreatedAt());
commentService.updatePostCommentStats(post);
}
dto.setCommentCount(post.getCommentCount());
dto.setLastReplyAt(post.getLastReplyAt());
dto.setReward(0); dto.setReward(0);
dto.setSubscribed(false); dto.setSubscribed(false);
dto.setType(post.getType()); dto.setType(post.getType());
@@ -99,6 +96,8 @@ public class PostMapper {
l.setPointCost(lp.getPointCost()); l.setPointCost(lp.getPointCost());
l.setStartTime(lp.getStartTime()); l.setStartTime(lp.getStartTime());
l.setEndTime(lp.getEndTime()); l.setEndTime(lp.getEndTime());
l.setParticipants(lp.getParticipants().stream().map(userMapper::toAuthorDto).collect(Collectors.toList()));
l.setWinners(lp.getWinners().stream().map(userMapper::toAuthorDto).collect(Collectors.toList()));
dto.setLottery(l); dto.setLottery(l);
} }
@@ -107,6 +106,7 @@ public class PostMapper {
p.setOptions(pp.getOptions()); p.setOptions(pp.getOptions());
p.setVotes(pp.getVotes()); p.setVotes(pp.getVotes());
p.setEndTime(pp.getEndTime()); p.setEndTime(pp.getEndTime());
p.setParticipants(pp.getParticipants().stream().map(userMapper::toAuthorDto).collect(Collectors.toList()));
Map<Integer, List<AuthorDto>> optionParticipants = pollVoteRepository.findByPostId(pp.getId()).stream() Map<Integer, List<AuthorDto>> optionParticipants = pollVoteRepository.findByPostId(pp.getId()).stream()
.collect(Collectors.groupingBy(PollVote::getOptionIndex, .collect(Collectors.groupingBy(PollVote::getOptionIndex,
Collectors.mapping(v -> userMapper.toAuthorDto(v.getUser()), Collectors.toList()))); Collectors.mapping(v -> userMapper.toAuthorDto(v.getUser()), Collectors.toList())));

View File

@@ -39,19 +39,19 @@ public class Post {
columnDefinition = "DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6)") columnDefinition = "DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6)")
private LocalDateTime createdAt; private LocalDateTime createdAt;
@ManyToOne(optional = false, fetch = FetchType.EAGER) @ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "author_id") @JoinColumn(name = "author_id")
private User author; private User author;
@ManyToOne(optional = false, fetch = FetchType.EAGER) @ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "category_id") @JoinColumn(name = "category_id")
private Category category; private Category category;
@ManyToMany(fetch = FetchType.EAGER) @ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "post_tags", @JoinTable(name = "post_tags",
joinColumns = @JoinColumn(name = "post_id"), joinColumns = @JoinColumn(name = "post_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")) inverseJoinColumns = @JoinColumn(name = "tag_id"))
private Set<Tag> tags = new HashSet<>(); private java.util.Set<Tag> tags = new java.util.HashSet<>();
@Column(nullable = false) @Column(nullable = false)
private long views = 0; private long views = 0;
@@ -72,10 +72,4 @@ public class Post {
@Column(nullable = true) @Column(nullable = true)
private Boolean rssExcluded = true; private Boolean rssExcluded = true;
@Column(nullable = false)
private long commentCount = 0;
@Column(nullable = true)
private LocalDateTime lastReplyAt;
} }

View File

@@ -1,17 +0,0 @@
package com.openisle.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_category_change_logs")
public class PostCategoryChangeLog extends PostChangeLog {
private String oldCategory;
private String newCategory;
}

View File

@@ -1,37 +0,0 @@
package com.openisle.model;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_change_logs")
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class PostChangeLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "post_id")
private Post post;
@ManyToOne(fetch = FetchType.LAZY, optional = true)
@JoinColumn(name = "user_id")
private User user;
@CreationTimestamp
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private PostChangeType type;
}

View File

@@ -1,13 +0,0 @@
package com.openisle.model;
public enum PostChangeType {
CONTENT,
TITLE,
CATEGORY,
TAG,
CLOSED,
PINNED,
FEATURED,
VOTE_RESULT,
LOTTERY_RESULT
}

View File

@@ -1,17 +0,0 @@
package com.openisle.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_closed_change_logs")
public class PostClosedChangeLog extends PostChangeLog {
private boolean oldClosed;
private boolean newClosed;
}

View File

@@ -1,21 +0,0 @@
package com.openisle.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_content_change_logs")
public class PostContentChangeLog extends PostChangeLog {
@Column(name = "old_content", columnDefinition = "LONGTEXT")
private String oldContent;
@Column(name = "new_content", columnDefinition = "LONGTEXT")
private String newContent;
}

View File

@@ -1,17 +0,0 @@
package com.openisle.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_featured_change_logs")
public class PostFeaturedChangeLog extends PostChangeLog {
private boolean oldFeatured;
private boolean newFeatured;
}

View File

@@ -1,16 +0,0 @@
package com.openisle.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_lottery_result_change_logs")
public class PostLotteryResultChangeLog extends PostChangeLog {
}

View File

@@ -1,19 +0,0 @@
package com.openisle.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_pinned_change_logs")
public class PostPinnedChangeLog extends PostChangeLog {
private LocalDateTime oldPinnedAt;
private LocalDateTime newPinnedAt;
}

View File

@@ -1,21 +0,0 @@
package com.openisle.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_tag_change_logs")
public class PostTagChangeLog extends PostChangeLog {
@Column(name = "old_tags")
private String oldTags;
@Column(name = "new_tags")
private String newTags;
}

View File

@@ -1,17 +0,0 @@
package com.openisle.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_title_change_logs")
public class PostTitleChangeLog extends PostChangeLog {
private String oldTitle;
private String newTitle;
}

View File

@@ -1,16 +0,0 @@
package com.openisle.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_vote_result_change_logs")
public class PostVoteResultChangeLog extends PostChangeLog {
}

View File

@@ -4,10 +4,7 @@ import com.openisle.model.Category;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List; import java.util.List;
import java.util.Optional;
public interface CategoryRepository extends JpaRepository<Category, Long> { public interface CategoryRepository extends JpaRepository<Category, Long> {
List<Category> findByNameContainingIgnoreCase(String keyword); List<Category> findByNameContainingIgnoreCase(String keyword);
Optional<Category> findByName(String name);
} }

View File

@@ -1,9 +1,8 @@
package com.openisle.repository; package com.openisle.repository;
import com.openisle.model.Comment;
import com.openisle.model.PointHistory; import com.openisle.model.PointHistory;
import com.openisle.model.Post;
import com.openisle.model.User; import com.openisle.model.User;
import com.openisle.model.Comment;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -15,8 +14,6 @@ public interface PointHistoryRepository extends JpaRepository<PointHistory, Long
long countByUser(User user); long countByUser(User user);
List<PointHistory> findByUserAndCreatedAtAfterOrderByCreatedAtDesc(User user, LocalDateTime createdAt); List<PointHistory> findByUserAndCreatedAtAfterOrderByCreatedAtDesc(User user, LocalDateTime createdAt);
List<PointHistory> findByComment(Comment comment); List<PointHistory> findByComment(Comment comment);
List<PointHistory> findByPost(Post post);
} }

View File

@@ -1,13 +0,0 @@
package com.openisle.repository;
import com.openisle.model.Post;
import com.openisle.model.PostChangeLog;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface PostChangeLogRepository extends JpaRepository<PostChangeLog, Long> {
List<PostChangeLog> findByPostOrderByCreatedAtAsc(Post post);
void deleteByPost(Post post);
}

View File

@@ -6,7 +6,6 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import java.util.List; import java.util.List;
import java.util.Optional;
public interface TagRepository extends JpaRepository<Tag, Long> { public interface TagRepository extends JpaRepository<Tag, Long> {
List<Tag> findByNameContainingIgnoreCase(String keyword); List<Tag> findByNameContainingIgnoreCase(String keyword);
@@ -16,6 +15,4 @@ public interface TagRepository extends JpaRepository<Tag, Long> {
List<Tag> findByCreatorOrderByCreatedAtDesc(User creator, Pageable pageable); List<Tag> findByCreatorOrderByCreatedAtDesc(User creator, Pageable pageable);
List<Tag> findByCreator(User creator); List<Tag> findByCreator(User creator);
Optional<Tag> findByName(String name);
} }

View File

@@ -1,48 +0,0 @@
package com.openisle.scheduler;
import com.openisle.config.CachingConfig;
import com.openisle.model.User;
import com.openisle.model.UserVisit;
import com.openisle.repository.UserRepository;
import com.openisle.repository.UserVisitRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.time.LocalDate;
import java.util.Set;
/**
* 执行计划
* 将每天用户访问落库
* @author smallclover
* @since 2025-09-09
*/
@Component
@RequiredArgsConstructor
public class UserVisitScheduler {
private final RedisTemplate redisTemplate;
private final UserRepository userRepository;
private final UserVisitRepository userVisitRepository;
@Scheduled(cron = "0 5 0 * * ?") // 每天 00:05 执行
public void persistDailyVisits(){
LocalDate yesterday = LocalDate.now().minusDays(1);
String key = CachingConfig.VISIT_CACHE_NAME + ":" + yesterday;
Set<String> usernames = redisTemplate.opsForSet().members(key);
if (!CollectionUtils.isEmpty(usernames)) {
for(String username: usernames){
User user = userRepository.findByUsername(username).orElse(null);
if(user != null){
UserVisit userVisit = new UserVisit();
userVisit.setUser(user);
userVisit.setVisitDate(yesterday);
userVisitRepository.save(userVisit);
}
}
redisTemplate.delete(key);
}
}
}

View File

@@ -62,18 +62,4 @@ public class CategoryService {
public List<Category> listCategories() { public List<Category> listCategories() {
return categoryRepository.findAll(); return categoryRepository.findAll();
} }
/**
* 获取检索用的分类Id列表
* @param categoryIds
* @param categoryId
* @return
*/
public List<Long> getSearchCategoryIds(List<Long> categoryIds, Long categoryId){
List<Long> ids = categoryIds;
if (categoryId != null) {
ids = List.of(categoryId);
}
return ids;
}
} }

View File

@@ -1,6 +1,5 @@
package com.openisle.service; package com.openisle.service;
import com.openisle.config.CachingConfig;
import com.openisle.model.Comment; import com.openisle.model.Comment;
import com.openisle.model.Post; import com.openisle.model.Post;
import com.openisle.model.User; import com.openisle.model.User;
@@ -21,8 +20,6 @@ import com.openisle.model.Role;
import com.openisle.exception.RateLimitException; import com.openisle.exception.RateLimitException;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.List; import java.util.List;
@@ -50,10 +47,6 @@ public class CommentService {
private final PointService pointService; private final PointService pointService;
private final ImageUploader imageUploader; private final ImageUploader imageUploader;
@CacheEvict(
value = CachingConfig.POST_CACHE_NAME,
allEntries = true
)
@Transactional @Transactional
public Comment addComment(String username, Long postId, String content) { public Comment addComment(String username, Long postId, String content) {
log.debug("addComment called by user {} for post {}", username, postId); log.debug("addComment called by user {} for post {}", username, postId);
@@ -76,10 +69,6 @@ public class CommentService {
comment.setContent(content); comment.setContent(content);
comment = commentRepository.save(comment); comment = commentRepository.save(comment);
log.debug("Comment {} saved for post {}", comment.getId(), postId); log.debug("Comment {} saved for post {}", comment.getId(), postId);
// Update post comment statistics
updatePostCommentStats(post);
imageUploader.addReferences(imageUploader.extractUrls(content)); imageUploader.addReferences(imageUploader.extractUrls(content));
if (!author.getId().equals(post.getAuthor().getId())) { if (!author.getId().equals(post.getAuthor().getId())) {
notificationService.createNotification(post.getAuthor(), NotificationType.COMMENT_REPLY, post, comment, notificationService.createNotification(post.getAuthor(), NotificationType.COMMENT_REPLY, post, comment,
@@ -106,10 +95,6 @@ public class CommentService {
return commentRepository.findLastCommentTimeOfUserByUserId(userId); return commentRepository.findLastCommentTimeOfUserByUserId(userId);
} }
@CacheEvict(
value = CachingConfig.POST_CACHE_NAME,
allEntries = true
)
@Transactional @Transactional
public Comment addReply(String username, Long parentId, String content) { public Comment addReply(String username, Long parentId, String content) {
log.debug("addReply called by user {} for parent comment {}", username, parentId); log.debug("addReply called by user {} for parent comment {}", username, parentId);
@@ -133,10 +118,6 @@ public class CommentService {
comment.setContent(content); comment.setContent(content);
comment = commentRepository.save(comment); comment = commentRepository.save(comment);
log.debug("Reply {} saved for parent {}", comment.getId(), parentId); log.debug("Reply {} saved for parent {}", comment.getId(), parentId);
// Update post comment statistics
updatePostCommentStats(parent.getPost());
imageUploader.addReferences(imageUploader.extractUrls(content)); imageUploader.addReferences(imageUploader.extractUrls(content));
if (!author.getId().equals(parent.getAuthor().getId())) { if (!author.getId().equals(parent.getAuthor().getId())) {
notificationService.createNotification(parent.getAuthor(), NotificationType.COMMENT_REPLY, parent.getPost(), notificationService.createNotification(parent.getAuthor(), NotificationType.COMMENT_REPLY, parent.getPost(),
@@ -247,10 +228,6 @@ public class CommentService {
return count; return count;
} }
@CacheEvict(
value = CachingConfig.POST_CACHE_NAME,
allEntries = true
)
@Transactional @Transactional
public void deleteComment(String username, Long id) { public void deleteComment(String username, Long id) {
log.debug("deleteComment called by user {} for comment {}", username, id); log.debug("deleteComment called by user {} for comment {}", username, id);
@@ -266,10 +243,6 @@ public class CommentService {
log.debug("deleteComment completed for comment {}", id); log.debug("deleteComment completed for comment {}", id);
} }
@CacheEvict(
value = CachingConfig.POST_CACHE_NAME,
allEntries = true
)
@Transactional @Transactional
public void deleteCommentCascade(Comment comment) { public void deleteCommentCascade(Comment comment) {
log.debug("deleteCommentCascade called for comment {}", comment.getId()); log.debug("deleteCommentCascade called for comment {}", comment.getId());
@@ -290,13 +263,9 @@ public class CommentService {
imageUploader.removeReferences(imageUploader.extractUrls(comment.getContent())); imageUploader.removeReferences(imageUploader.extractUrls(comment.getContent()));
// 逻辑删除评论 // 逻辑删除评论
Post post = comment.getPost();
commentRepository.delete(comment); commentRepository.delete(comment);
// 删除积分历史 // 删除积分历史
pointHistoryRepository.deleteAll(pointHistories); pointHistoryRepository.deleteAll(pointHistories);
// Update post comment statistics
updatePostCommentStats(post);
// 重新计算受影响用户的积分 // 重新计算受影响用户的积分
if (!usersToRecalculate.isEmpty()) { if (!usersToRecalculate.isEmpty()) {
@@ -342,23 +311,4 @@ public class CommentService {
int replies = commentRepository.findByParentOrderByCreatedAtAsc(comment).size(); int replies = commentRepository.findByParentOrderByCreatedAtAsc(comment).size();
return reactions + replies; return reactions + replies;
} }
/**
* Update post comment statistics (comment count and last reply time)
*/
public void updatePostCommentStats(Post post) {
long commentCount = commentRepository.countByPostId(post.getId());
post.setCommentCount(commentCount);
LocalDateTime lastReplyAt = commentRepository.findLastCommentTime(post);
if (lastReplyAt == null) {
post.setLastReplyAt(post.getCreatedAt());
} else {
post.setLastReplyAt(lastReplyAt);
}
postRepository.save(post);
log.debug("Updated post {} stats: commentCount={}, lastReplyAt={}",
post.getId(), commentCount, lastReplyAt);
}
} }

View File

@@ -1,121 +0,0 @@
package com.openisle.service;
import com.openisle.model.*;
import com.openisle.repository.PostChangeLogRepository;
import com.openisle.repository.PostRepository;
import com.openisle.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class PostChangeLogService {
private final PostChangeLogRepository logRepository;
private final PostRepository postRepository;
private final UserRepository userRepository;
private User getSystemUser() {
return userRepository.findByUsername("system")
.orElseThrow(() -> new IllegalStateException("System user not found"));
}
public void recordContentChange(Post post, User user, String oldContent, String newContent) {
PostContentChangeLog log = new PostContentChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.CONTENT);
log.setOldContent(oldContent);
log.setNewContent(newContent);
logRepository.save(log);
}
public void recordTitleChange(Post post, User user, String oldTitle, String newTitle) {
PostTitleChangeLog log = new PostTitleChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.TITLE);
log.setOldTitle(oldTitle);
log.setNewTitle(newTitle);
logRepository.save(log);
}
public void recordCategoryChange(Post post, User user, String oldCategory, String newCategory) {
PostCategoryChangeLog log = new PostCategoryChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.CATEGORY);
log.setOldCategory(oldCategory);
log.setNewCategory(newCategory);
logRepository.save(log);
}
public void recordTagChange(Post post, User user, Set<Tag> oldTags, Set<Tag> newTags) {
PostTagChangeLog log = new PostTagChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.TAG);
log.setOldTags(oldTags.stream().map(Tag::getName).collect(Collectors.joining(",")));
log.setNewTags(newTags.stream().map(Tag::getName).collect(Collectors.joining(",")));
logRepository.save(log);
}
public void recordClosedChange(Post post, User user, boolean oldClosed, boolean newClosed) {
PostClosedChangeLog log = new PostClosedChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.CLOSED);
log.setOldClosed(oldClosed);
log.setNewClosed(newClosed);
logRepository.save(log);
}
public void recordPinnedChange(Post post, User user, java.time.LocalDateTime oldPinnedAt, java.time.LocalDateTime newPinnedAt) {
PostPinnedChangeLog log = new PostPinnedChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.PINNED);
log.setOldPinnedAt(oldPinnedAt);
log.setNewPinnedAt(newPinnedAt);
logRepository.save(log);
}
public void recordFeaturedChange(Post post, User user, boolean oldFeatured, boolean newFeatured) {
PostFeaturedChangeLog log = new PostFeaturedChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.FEATURED);
log.setOldFeatured(oldFeatured);
log.setNewFeatured(newFeatured);
logRepository.save(log);
}
public void recordVoteResult(Post post) {
PostVoteResultChangeLog log = new PostVoteResultChangeLog();
log.setPost(post);
log.setUser(getSystemUser());
log.setType(PostChangeType.VOTE_RESULT);
logRepository.save(log);
}
public void recordLotteryResult(Post post) {
PostLotteryResultChangeLog log = new PostLotteryResultChangeLog();
log.setPost(post);
log.setUser(getSystemUser());
log.setType(PostChangeType.LOTTERY_RESULT);
logRepository.save(log);
}
public void deleteLogsForPost(Post post) {
logRepository.deleteByPost(post);
}
public List<PostChangeLog> listLogs(Long postId) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
return logRepository.findByPostOrderByCreatedAtAsc(post);
}
}

View File

@@ -1,33 +1,38 @@
package com.openisle.service; package com.openisle.service;
import com.openisle.config.CachingConfig; import com.openisle.model.Post;
import com.openisle.mapper.PostMapper; import com.openisle.model.PostStatus;
import com.openisle.model.*; import com.openisle.model.PostType;
import com.openisle.model.PublishMode;
import com.openisle.model.User;
import com.openisle.model.Category;
import com.openisle.model.Comment;
import com.openisle.model.NotificationType;
import com.openisle.model.LotteryPost;
import com.openisle.model.PollPost;
import com.openisle.model.PollVote;
import com.openisle.repository.PostRepository; import com.openisle.repository.PostRepository;
import com.openisle.repository.LotteryPostRepository; import com.openisle.repository.LotteryPostRepository;
import com.openisle.repository.PollPostRepository; import com.openisle.repository.PollPostRepository;
import com.openisle.repository.UserRepository; import com.openisle.repository.UserRepository;
import com.openisle.repository.CategoryRepository; import com.openisle.repository.CategoryRepository;
import com.openisle.repository.TagRepository; import com.openisle.repository.TagRepository;
import com.openisle.service.SubscriptionService;
import com.openisle.service.CommentService;
import com.openisle.repository.CommentRepository; import com.openisle.repository.CommentRepository;
import com.openisle.repository.ReactionRepository; import com.openisle.repository.ReactionRepository;
import com.openisle.repository.PostSubscriptionRepository; import com.openisle.repository.PostSubscriptionRepository;
import com.openisle.repository.NotificationRepository; import com.openisle.repository.NotificationRepository;
import com.openisle.repository.PollVoteRepository; import com.openisle.repository.PollVoteRepository;
import com.openisle.repository.PointHistoryRepository; import com.openisle.model.Role;
import com.openisle.exception.RateLimitException; import com.openisle.exception.RateLimitException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.TaskScheduler;
import com.openisle.service.EmailSender; import com.openisle.service.EmailSender;
import java.time.Duration;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.util.*; import java.util.*;
@@ -42,8 +47,6 @@ import java.time.LocalDateTime;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.stream.Collectors;
import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
@@ -71,14 +74,10 @@ public class PostService {
private final EmailSender emailSender; private final EmailSender emailSender;
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
private final PointService pointService; private final PointService pointService;
private final PostChangeLogService postChangeLogService;
private final PointHistoryRepository pointHistoryRepository;
private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations = new ConcurrentHashMap<>(); private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations = new ConcurrentHashMap<>();
@Value("${app.website-url:https://www.open-isle.com}") @Value("${app.website-url:https://www.open-isle.com}")
private String websiteUrl; private String websiteUrl;
private final RedisTemplate redisTemplate;
@org.springframework.beans.factory.annotation.Autowired @org.springframework.beans.factory.annotation.Autowired
public PostService(PostRepository postRepository, public PostService(PostRepository postRepository,
UserRepository userRepository, UserRepository userRepository,
@@ -100,10 +99,7 @@ public class PostService {
EmailSender emailSender, EmailSender emailSender,
ApplicationContext applicationContext, ApplicationContext applicationContext,
PointService pointService, PointService pointService,
PostChangeLogService postChangeLogService, @Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode) {
PointHistoryRepository pointHistoryRepository,
@Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode,
RedisTemplate redisTemplate) {
this.postRepository = postRepository; this.postRepository = postRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.categoryRepository = categoryRepository; this.categoryRepository = categoryRepository;
@@ -124,11 +120,7 @@ public class PostService {
this.emailSender = emailSender; this.emailSender = emailSender;
this.applicationContext = applicationContext; this.applicationContext = applicationContext;
this.pointService = pointService; this.pointService = pointService;
this.postChangeLogService = postChangeLogService;
this.pointHistoryRepository = pointHistoryRepository;
this.publishMode = publishMode; this.publishMode = publishMode;
this.redisTemplate = redisTemplate;
} }
@EventListener(ApplicationReadyEvent.class) @EventListener(ApplicationReadyEvent.class)
@@ -167,37 +159,26 @@ public class PostService {
return postRepository.findByStatusAndRssExcludedFalseOrderByCreatedAtDesc(PostStatus.PUBLISHED, pageable); return postRepository.findByStatusAndRssExcludedFalseOrderByCreatedAtDesc(PostStatus.PUBLISHED, pageable);
} }
public Post excludeFromRss(Long id, String username) { public Post excludeFromRss(Long id) {
Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
boolean oldFeatured = !Boolean.TRUE.equals(post.getRssExcluded());
post.setRssExcluded(true); post.setRssExcluded(true);
Post saved = postRepository.save(post); return postRepository.save(post);
postChangeLogService.recordFeaturedChange(saved, user, oldFeatured, false);
return saved;
} }
public Post includeInRss(Long id, String username) { public Post includeInRss(Long id) {
Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
boolean oldFeatured = !Boolean.TRUE.equals(post.getRssExcluded());
post.setRssExcluded(false); post.setRssExcluded(false);
Post saved = postRepository.save(post); post = postRepository.save(post);
postChangeLogService.recordFeaturedChange(saved, user, oldFeatured, true); notificationService.createNotification(post.getAuthor(), NotificationType.POST_FEATURED, post, null, null, null, null, null);
notificationService.createNotification(saved.getAuthor(), NotificationType.POST_FEATURED, saved, null, null, null, null, null); pointService.awardForFeatured(post.getAuthor().getUsername(), post.getId());
pointService.awardForFeatured(saved.getAuthor().getUsername(), saved.getId()); return post;
return saved;
} }
@CacheEvict(
value = CachingConfig.POST_CACHE_NAME, allEntries = true
)
public Post createPost(String username, public Post createPost(String username,
Long categoryId, Long categoryId,
String title, String title,
String content, String content,
List<Long> tagIds, java.util.List<Long> tagIds,
PostType type, PostType type,
String prizeDescription, String prizeDescription,
String prizeIcon, String prizeIcon,
@@ -207,9 +188,9 @@ public class PostService {
LocalDateTime endTime, LocalDateTime endTime,
java.util.List<String> options, java.util.List<String> options,
Boolean multiple) { Boolean multiple) {
// 限制访问次数 long recent = postRepository.countByAuthorAfter(username,
boolean limitResult = postRateLimit(username); java.time.LocalDateTime.now().minusMinutes(5));
if (!limitResult) { if (recent >= 1) {
throw new RateLimitException("Too many posts"); throw new RateLimitException("Too many posts");
} }
if (tagIds == null || tagIds.isEmpty()) { if (tagIds == null || tagIds.isEmpty()) {
@@ -306,23 +287,6 @@ public class PostService {
return post; return post;
} }
/**
* 限制发帖频率
* @param username
* @return
*/
private boolean postRateLimit(String username){
String key = CachingConfig.LIMIT_CACHE_NAME +":posts:"+username;
String result = (String)redisTemplate.opsForValue().get(key);
//最近没有创建过文章
if(StringUtils.isEmpty(result)){
// 限制频率为5分钟
redisTemplate.opsForValue().set(key,"1", Duration.ofMinutes(5));
return true;
}
return false;
}
public void joinLottery(Long postId, String username) { public void joinLottery(Long postId, String username) {
LotteryPost post = lotteryPostRepository.findById(postId) LotteryPost post = lotteryPostRepository.findById(postId)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
@@ -391,7 +355,6 @@ public class PostService {
for (User participant : pp.getParticipants()) { for (User participant : pp.getParticipants()) {
notificationService.createNotification(participant, NotificationType.POLL_RESULT_PARTICIPANT, pp, null, null, null, null, null); notificationService.createNotification(participant, NotificationType.POLL_RESULT_PARTICIPANT, pp, null, null, null, null, null);
} }
postChangeLogService.recordVoteResult(pp);
}); });
} }
@@ -426,7 +389,6 @@ public class PostService {
notificationService.createNotification(lp.getAuthor(), NotificationType.LOTTERY_DRAW, lp, null, null, null, null, null); notificationService.createNotification(lp.getAuthor(), NotificationType.LOTTERY_DRAW, lp, null, null, null, null, null);
notificationService.sendCustomPush(lp.getAuthor(), "抽奖已开奖", String.format("%s/posts/%d", websiteUrl, lp.getId())); notificationService.sendCustomPush(lp.getAuthor(), "抽奖已开奖", String.format("%s/posts/%d", websiteUrl, lp.getId()));
} }
postChangeLogService.recordLotteryResult(lp);
}); });
} }
@@ -508,10 +470,6 @@ public class PostService {
return listPostsByLatestReply(null, null, page, pageSize); return listPostsByLatestReply(null, null, page, pageSize);
} }
@Cacheable(
value = CachingConfig.POST_CACHE_NAME,
key = "new org.springframework.cache.interceptor.SimpleKey('latest_reply', #categoryIds, #tagIds, #page, #pageSize)"
)
public List<Post> listPostsByLatestReply(java.util.List<Long> categoryIds, public List<Post> listPostsByLatestReply(java.util.List<Long> categoryIds,
java.util.List<Long> tagIds, java.util.List<Long> tagIds,
Integer page, Integer page,
@@ -539,9 +497,9 @@ public class PostService {
posts = postRepository.findByCategoryInAndStatusOrderByCreatedAtDesc(categories, PostStatus.PUBLISHED); posts = postRepository.findByCategoryInAndStatusOrderByCreatedAtDesc(categories, PostStatus.PUBLISHED);
} }
} else { } else {
List<Tag> tags = tagRepository.findAllById(tagIds); java.util.List<com.openisle.model.Tag> tags = tagRepository.findAllById(tagIds);
if (tags.isEmpty()) { if (tags.isEmpty()) {
return new ArrayList<>(); return java.util.List.of();
} }
posts = postRepository.findByAllTagsOrderByCreatedAtDesc(tags, PostStatus.PUBLISHED, tags.size()); posts = postRepository.findByAllTagsOrderByCreatedAtDesc(tags, PostStatus.PUBLISHED, tags.size());
} }
@@ -639,43 +597,11 @@ public class PostService {
return paginate(sortByPinnedAndCreated(posts), page, pageSize); return paginate(sortByPinnedAndCreated(posts), page, pageSize);
} }
/**
* 默认的文章列表
* @param ids
* @param tids
* @param page
* @param pageSize
* @return
*/
@Cacheable(
value = CachingConfig.POST_CACHE_NAME,
key = "new org.springframework.cache.interceptor.SimpleKey('default', #ids, #tids, #page, #pageSize)"
)
public List<Post> defaultListPosts(List<Long> ids, List<Long> tids, Integer page, Integer pageSize){
boolean hasCategories = !CollectionUtils.isEmpty(ids);
boolean hasTags = !CollectionUtils.isEmpty(tids);
if (hasCategories && hasTags) {
return listPostsByCategoriesAndTags(ids, tids, page, pageSize)
.stream().collect(Collectors.toList());
}
if (hasTags) {
return listPostsByTags(tids, page, pageSize)
.stream().collect(Collectors.toList());
}
return listPostsByCategories(ids, page, pageSize)
.stream().collect(Collectors.toList());
}
public List<Post> listPendingPosts() { public List<Post> listPendingPosts() {
return postRepository.findByStatus(PostStatus.PENDING); return postRepository.findByStatus(PostStatus.PENDING);
} }
@CacheEvict(
value = CachingConfig.POST_CACHE_NAME,
allEntries = true
)
public Post approvePost(Long id) { public Post approvePost(Long id) {
Post post = postRepository.findById(id) Post post = postRepository.findById(id)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
@@ -712,42 +638,20 @@ public class PostService {
return post; return post;
} }
@CacheEvict( public Post pinPost(Long id) {
value = CachingConfig.POST_CACHE_NAME,
allEntries = true
)
public Post pinPost(Long id, String username) {
Post post = postRepository.findById(id) Post post = postRepository.findById(id)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
java.time.LocalDateTime oldPinned = post.getPinnedAt();
post.setPinnedAt(java.time.LocalDateTime.now()); post.setPinnedAt(java.time.LocalDateTime.now());
Post saved = postRepository.save(post); return postRepository.save(post);
postChangeLogService.recordPinnedChange(saved, user, oldPinned, saved.getPinnedAt());
return saved;
} }
@CacheEvict( public Post unpinPost(Long id) {
value = CachingConfig.POST_CACHE_NAME,
allEntries = true
)
public Post unpinPost(Long id, String username) {
Post post = postRepository.findById(id) Post post = postRepository.findById(id)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
java.time.LocalDateTime oldPinned = post.getPinnedAt();
post.setPinnedAt(null); post.setPinnedAt(null);
Post saved = postRepository.save(post); return postRepository.save(post);
postChangeLogService.recordPinnedChange(saved, user, oldPinned, null);
return saved;
} }
@CacheEvict(
value = CachingConfig.POST_CACHE_NAME,
allEntries = true
)
public Post closePost(Long id, String username) { public Post closePost(Long id, String username) {
Post post = postRepository.findById(id) Post post = postRepository.findById(id)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
@@ -756,17 +660,10 @@ public class PostService {
if (!user.getId().equals(post.getAuthor().getId()) && user.getRole() != Role.ADMIN) { if (!user.getId().equals(post.getAuthor().getId()) && user.getRole() != Role.ADMIN) {
throw new IllegalArgumentException("Unauthorized"); throw new IllegalArgumentException("Unauthorized");
} }
boolean oldClosed = post.isClosed();
post.setClosed(true); post.setClosed(true);
Post saved = postRepository.save(post); return postRepository.save(post);
postChangeLogService.recordClosedChange(saved, user, oldClosed, true);
return saved;
} }
@CacheEvict(
value = CachingConfig.POST_CACHE_NAME,
allEntries = true
)
public Post reopenPost(Long id, String username) { public Post reopenPost(Long id, String username) {
Post post = postRepository.findById(id) Post post = postRepository.findById(id)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
@@ -775,18 +672,11 @@ public class PostService {
if (!user.getId().equals(post.getAuthor().getId()) && user.getRole() != Role.ADMIN) { if (!user.getId().equals(post.getAuthor().getId()) && user.getRole() != Role.ADMIN) {
throw new IllegalArgumentException("Unauthorized"); throw new IllegalArgumentException("Unauthorized");
} }
boolean oldClosed = post.isClosed();
post.setClosed(false); post.setClosed(false);
Post saved = postRepository.save(post); return postRepository.save(post);
postChangeLogService.recordClosedChange(saved, user, oldClosed, false);
return saved;
} }
@CacheEvict( @org.springframework.transaction.annotation.Transactional
value = CachingConfig.POST_CACHE_NAME,
allEntries = true
)
@Transactional
public Post updatePost(Long id, public Post updatePost(Long id,
String username, String username,
Long categoryId, Long categoryId,
@@ -812,38 +702,18 @@ public class PostService {
if (tags.isEmpty()) { if (tags.isEmpty()) {
throw new IllegalArgumentException("Tag not found"); throw new IllegalArgumentException("Tag not found");
} }
String oldTitle = post.getTitle();
String oldContent = post.getContent();
Category oldCategory = post.getCategory();
java.util.Set<com.openisle.model.Tag> oldTags = new java.util.HashSet<>(post.getTags());
post.setTitle(title); post.setTitle(title);
String oldContent = post.getContent();
post.setContent(content); post.setContent(content);
post.setCategory(category); post.setCategory(category);
post.setTags(new java.util.HashSet<>(tags)); post.setTags(new java.util.HashSet<>(tags));
Post updated = postRepository.save(post); Post updated = postRepository.save(post);
imageUploader.adjustReferences(oldContent, content); imageUploader.adjustReferences(oldContent, content);
notificationService.notifyMentions(content, user, updated, null); notificationService.notifyMentions(content, user, updated, null);
if (!java.util.Objects.equals(oldTitle, title)) {
postChangeLogService.recordTitleChange(updated, user, oldTitle, title);
}
if (!java.util.Objects.equals(oldContent, content)) {
postChangeLogService.recordContentChange(updated, user, oldContent, content);
}
if (!java.util.Objects.equals(oldCategory.getId(), category.getId())) {
postChangeLogService.recordCategoryChange(updated, user, oldCategory.getName(), category.getName());
}
java.util.Set<com.openisle.model.Tag> newTags = new java.util.HashSet<>(tags);
if (!oldTags.equals(newTags)) {
postChangeLogService.recordTagChange(updated, user, oldTags, newTags);
}
return updated; return updated;
} }
@CacheEvict( @org.springframework.transaction.annotation.Transactional
value = CachingConfig.POST_CACHE_NAME,
allEntries = true
)
@Transactional
public void deletePost(Long id, String username) { public void deletePost(Long id, String username) {
Post post = postRepository.findById(id) Post post = postRepository.findById(id)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
@@ -862,25 +732,6 @@ public class PostService {
notificationRepository.deleteAll(notificationRepository.findByPost(post)); notificationRepository.deleteAll(notificationRepository.findByPost(post));
postReadService.deleteByPost(post); postReadService.deleteByPost(post);
imageUploader.removeReferences(imageUploader.extractUrls(post.getContent())); imageUploader.removeReferences(imageUploader.extractUrls(post.getContent()));
List<PointHistory> pointHistories = pointHistoryRepository.findByPost(post);
Set<User> usersToRecalculate = pointHistories.stream()
.map(PointHistory::getUser)
.collect(Collectors.toSet());
if (!pointHistories.isEmpty()) {
LocalDateTime deletedAt = LocalDateTime.now();
for (PointHistory history : pointHistories) {
history.setDeletedAt(deletedAt);
history.setPost(null);
}
pointHistoryRepository.saveAll(pointHistories);
}
if (!usersToRecalculate.isEmpty()) {
for (User affected : usersToRecalculate) {
int newPoints = pointService.recalculateUserPoints(affected);
affected.setPoint(newPoints);
}
userRepository.saveAll(usersToRecalculate);
}
if (post instanceof LotteryPost lp) { if (post instanceof LotteryPost lp) {
ScheduledFuture<?> future = scheduledFinalizations.remove(lp.getId()); ScheduledFuture<?> future = scheduledFinalizations.remove(lp.getId());
if (future != null) { if (future != null) {
@@ -888,7 +739,6 @@ public class PostService {
} }
} }
String title = post.getTitle(); String title = post.getTitle();
postChangeLogService.deleteLogsForPost(post);
postRepository.delete(post); postRepository.delete(post);
if (adminDeleting) { if (adminDeleting) {
notificationService.createNotification(author, NotificationType.POST_DELETED, notificationService.createNotification(author, NotificationType.POST_DELETED,
@@ -956,17 +806,15 @@ public class PostService {
.toList(); .toList();
} }
private List<Post> paginate(List<Post> posts, Integer page, Integer pageSize) { private java.util.List<Post> paginate(java.util.List<Post> posts, Integer page, Integer pageSize) {
if (page == null || pageSize == null) { if (page == null || pageSize == null) {
return posts; return posts;
} }
int from = page * pageSize; int from = page * pageSize;
if (from >= posts.size()) { if (from >= posts.size()) {
return new ArrayList<>(); return java.util.List.of();
} }
int to = Math.min(from + pageSize, posts.size()); int to = Math.min(from + pageSize, posts.size());
// 这里必须将list包装为arrayList类型否则序列化会有问题 return posts.subList(from, to);
// list.sublist返回的是内部类
return new ArrayList<>(posts.subList(from, to));
} }
} }

View File

@@ -120,18 +120,4 @@ public class TagService {
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found")); .orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
return tagRepository.findByCreator(user); return tagRepository.findByCreator(user);
} }
/**
* 获取检索用的标签Id列表
* @param tagIds
* @param tagId
* @return
*/
public List<Long> getSearchTagIds(List<Long> tagIds, Long tagId){
List<Long> ids = tagIds;
if (tagId != null) {
ids = List.of(tagId);
}
return ids;
}
} }

View File

@@ -1,6 +1,5 @@
package com.openisle.service; package com.openisle.service;
import com.openisle.config.CachingConfig;
import com.openisle.model.User; import com.openisle.model.User;
import com.openisle.model.Role; import com.openisle.model.Role;
import com.openisle.service.PasswordValidator; import com.openisle.service.PasswordValidator;
@@ -8,18 +7,13 @@ import com.openisle.service.UsernameValidator;
import com.openisle.service.AvatarGenerator; import com.openisle.service.AvatarGenerator;
import com.openisle.exception.FieldException; import com.openisle.exception.FieldException;
import com.openisle.repository.UserRepository; import com.openisle.repository.UserRepository;
import com.openisle.util.VerifyType;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Random; import java.util.Random;
import java.util.concurrent.TimeUnit;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -31,10 +25,6 @@ public class UserService {
private final ImageUploader imageUploader; private final ImageUploader imageUploader;
private final AvatarGenerator avatarGenerator; private final AvatarGenerator avatarGenerator;
private final RedisTemplate redisTemplate;
private final EmailSender emailService;
public User register(String username, String email, String password, String reason, com.openisle.model.RegisterMode mode) { public User register(String username, String email, String password, String reason, com.openisle.model.RegisterMode mode) {
usernameValidator.validate(username); usernameValidator.validate(username);
passwordValidator.validate(password); passwordValidator.validate(password);
@@ -48,7 +38,7 @@ public class UserService {
// 未验证 → 允许“重注册”:覆盖必要字段并重新发验证码 // 未验证 → 允许“重注册”:覆盖必要字段并重新发验证码
u.setEmail(email); // 若不允许改邮箱可去掉 u.setEmail(email); // 若不允许改邮箱可去掉
u.setPassword(passwordEncoder.encode(password)); u.setPassword(passwordEncoder.encode(password));
// u.setVerificationCode(genCode()); u.setVerificationCode(genCode());
u.setRegisterReason(reason); u.setRegisterReason(reason);
u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT); u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
return userRepository.save(u); return userRepository.save(u);
@@ -64,7 +54,7 @@ public class UserService {
// 未验证 → 允许“重注册” // 未验证 → 允许“重注册”
u.setUsername(username); // 若不允许改用户名可去掉 u.setUsername(username); // 若不允许改用户名可去掉
u.setPassword(passwordEncoder.encode(password)); u.setPassword(passwordEncoder.encode(password));
// u.setVerificationCode(genCode()); u.setVerificationCode(genCode());
u.setRegisterReason(reason); u.setRegisterReason(reason);
u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT); u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
return userRepository.save(u); return userRepository.save(u);
@@ -77,7 +67,7 @@ public class UserService {
user.setPassword(passwordEncoder.encode(password)); user.setPassword(passwordEncoder.encode(password));
user.setRole(Role.USER); user.setRole(Role.USER);
user.setVerified(false); user.setVerified(false);
// user.setVerificationCode(genCode()); user.setVerificationCode(genCode());
user.setAvatar(avatarGenerator.generate(username)); user.setAvatar(avatarGenerator.generate(username));
user.setRegisterReason(reason); user.setRegisterReason(reason);
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT); user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
@@ -87,7 +77,7 @@ public class UserService {
public User registerWithInvite(String username, String email, String password) { public User registerWithInvite(String username, String email, String password) {
User user = register(username, email, password, "", com.openisle.model.RegisterMode.DIRECT); User user = register(username, email, password, "", com.openisle.model.RegisterMode.DIRECT);
user.setVerified(true); user.setVerified(true);
// user.setVerificationCode(genCode()); user.setVerificationCode(genCode());
return userRepository.save(user); return userRepository.save(user);
} }
@@ -95,58 +85,16 @@ public class UserService {
return String.format("%06d", new Random().nextInt(1000000)); return String.format("%06d", new Random().nextInt(1000000));
} }
/** public boolean verifyCode(String username, String code) {
* 将验证码存入缓存,并发送邮件 Optional<User> userOpt = userRepository.findByUsername(username);
* @param user if (userOpt.isPresent() && code.equals(userOpt.get().getVerificationCode())) {
*/ User user = userOpt.get();
public void sendVerifyMail(User user, VerifyType verifyType){
// 缓存验证码
String code = genCode();
String key;
String subject;
String content = "您的验证码是:" + code;
// 注册类型
if(verifyType.equals(VerifyType.REGISTER)){
key = CachingConfig.VERIFY_CACHE_NAME + ":register:code:" + user.getUsername();
subject = "在网站填写验证码以验证(有效期为5分钟)";
}else {
// 重置密码
key = CachingConfig.VERIFY_CACHE_NAME + ":reset_password:code:" + user.getUsername();
subject = "请填写验证码以重置密码(有效期为5分钟)";
}
redisTemplate.opsForValue().set(key, code, 5, TimeUnit.MINUTES);// 五分钟后验证码过期
emailService.sendEmail(user.getEmail(), subject, content);
}
/**
* 验证code是否正确
* @param user
* @param code
* @param verifyType
* @return
*/
public boolean verifyCode(User user, String code, VerifyType verifyType) {
// 生成key
String key1 = VerifyType.REGISTER.equals(verifyType)?":register:code:":":reset_password:code:";
String key = CachingConfig.VERIFY_CACHE_NAME + key1 + user.getUsername();
// 这里不能使用getAndDelete,需要6.x版本
String cachedCode = (String)redisTemplate.opsForValue().get(key);
// 如果校验code过期或者不存在
// 或者校验code不一致
if(Objects.isNull(cachedCode)
|| !cachedCode.equals(code)){
return false;
}
// 注册模式需要设置已经确认
if(VerifyType.REGISTER.equals(verifyType)){
user.setVerified(true); user.setVerified(true);
user.setVerificationCode(null);
userRepository.save(user); userRepository.save(user);
return true;
} }
// 走到这里说明验证成功删除验证码 return false;
redisTemplate.delete(key);
return true;
} }
public Optional<User> authenticate(String username, String password) { public Optional<User> authenticate(String username, String password) {
@@ -217,6 +165,26 @@ public class UserService {
return userRepository.save(user); return userRepository.save(user);
} }
public String generatePasswordResetCode(String email) {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
String code = genCode();
user.setPasswordResetCode(code);
userRepository.save(user);
return code;
}
public boolean verifyPasswordResetCode(String email, String code) {
Optional<User> userOpt = userRepository.findByEmail(email);
if (userOpt.isPresent() && code.equals(userOpt.get().getPasswordResetCode())) {
User user = userOpt.get();
user.setPasswordResetCode(null);
userRepository.save(user);
return true;
}
return false;
}
public User updatePassword(String username, String newPassword) { public User updatePassword(String username, String newPassword) {
passwordValidator.validate(newPassword); passwordValidator.validate(newPassword);
User user = userRepository.findByUsername(username) User user = userRepository.findByUsername(username)

View File

@@ -1,22 +1,15 @@
package com.openisle.service; package com.openisle.service;
import com.openisle.config.CachingConfig;
import com.openisle.model.User; import com.openisle.model.User;
import com.openisle.model.UserVisit; import com.openisle.model.UserVisit;
import com.openisle.repository.UserRepository; import com.openisle.repository.UserRepository;
import com.openisle.repository.UserVisitRepository; import com.openisle.repository.UserVisitRepository;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.time.Duration;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.Set;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -24,8 +17,6 @@ public class UserVisitService {
private final UserVisitRepository userVisitRepository; private final UserVisitRepository userVisitRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private final RedisTemplate redisTemplate;
public boolean recordVisit(String username) { public boolean recordVisit(String username) {
User user = userRepository.findByUsername(username) User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found")); .orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
@@ -39,36 +30,10 @@ public class UserVisitService {
}); });
} }
/**
* 统计访问次数,改为从缓存获取/数据库获取
* @param username
* @return
*/
public long countVisits(String username) { public long countVisits(String username) {
User user = userRepository.findByUsername(username) User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found")); .orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
return userVisitRepository.countByUser(user);
// 如果缓存存在就返回
String key1 = CachingConfig.VISIT_CACHE_NAME + ":" +LocalDate.now() + ":count:" + username;
Integer cached = (Integer) redisTemplate.opsForValue().get(key1);
if (cached != null){
return cached.longValue();
}
// Redis Set 检查今天是否访问
String todayKey = CachingConfig.VISIT_CACHE_NAME + ":" + LocalDate.now();
boolean todayVisited = redisTemplate.opsForSet().isMember(todayKey, username);
Long visitCount = userVisitRepository.countByUser(user);
if (todayVisited) visitCount += 1;
LocalDateTime now = LocalDateTime.now();
LocalDateTime endOfDay = now.toLocalDate().atTime(23, 59, 59);
long secondsUntilEndOfDay = Duration.between(now, endOfDay).getSeconds();
// 写入缓存,设置 TTL当天剩余时间
redisTemplate.opsForValue().set(key1, visitCount, Duration.ofSeconds(secondsUntilEndOfDay));
return visitCount;
} }
public long countDau(LocalDate date) { public long countDau(LocalDate date) {

View File

@@ -1,20 +0,0 @@
package com.openisle.util;
/**
* 验证码类型
* @author smallclover
* @since 2025-09-08
*/
public enum VerifyType {
REGISTER(1),
RESET_PASSWORD(2);
private final int code;
VerifyType(int code) {
this.code = code;
}
public int getCode() {
return code;
}
}

View File

@@ -108,10 +108,6 @@ rabbitmq.sharding.enabled=true
# see https://springdoc.org/#springdoc-openapi-core-properties # see https://springdoc.org/#springdoc-openapi-core-properties
springdoc.api-docs.path=/api/v3/api-docs springdoc.api-docs.path=/api/v3/api-docs
springdoc.api-docs.enabled=true springdoc.api-docs.enabled=true
springdoc.api-docs.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.title=OpenIsle
springdoc.info.description=OpenIsle Open API Documentation springdoc.info.description=OpenIsle Open API Documentation
springdoc.info.version=0.0.1 springdoc.info.version=0.0.1

View File

@@ -1,19 +0,0 @@
-- Add comment count and last reply time fields to posts table for performance optimization
ALTER TABLE posts ADD COLUMN comment_count BIGINT NOT NULL DEFAULT 0;
ALTER TABLE posts ADD COLUMN last_reply_at DATETIME(6) NULL;
-- Add index on last_reply_at for sorting by latest reply
CREATE INDEX idx_posts_last_reply_at ON posts(last_reply_at);
-- Initialize comment_count and last_reply_at with existing data
UPDATE posts p SET
comment_count = (
SELECT COUNT(*)
FROM comments c
WHERE c.post_id = p.id AND c.deleted_at IS NULL
),
last_reply_at = (
SELECT MAX(c.created_at)
FROM comments c
WHERE c.post_id = p.id AND c.deleted_at IS NULL
);

View File

@@ -4,7 +4,6 @@ import com.openisle.model.User;
import com.openisle.service.*; import com.openisle.service.*;
import com.openisle.model.RegisterMode; import com.openisle.model.RegisterMode;
import com.openisle.repository.UserRepository; import com.openisle.repository.UserRepository;
import com.openisle.util.VerifyType;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -72,9 +71,7 @@ class AuthControllerTest {
@Test @Test
void verifyCodeEndpoint() throws Exception { void verifyCodeEndpoint() throws Exception {
User user = new User(); Mockito.when(userService.verifyCode("u", "123")).thenReturn(true);
user.setUsername("u");
Mockito.when(userService.verifyCode(user, "123", VerifyType.REGISTER)).thenReturn(true);
Mockito.when(jwtService.generateReasonToken("u")).thenReturn("reason_token"); Mockito.when(jwtService.generateReasonToken("u")).thenReturn("reason_token");
mockMvc.perform(post("/api/auth/verify") mockMvc.perform(post("/api/auth/verify")

View File

@@ -55,10 +55,6 @@ class PostControllerTest {
private UserVisitService userVisitService; private UserVisitService userVisitService;
@MockBean @MockBean
private PostReadService postReadService; private PostReadService postReadService;
@MockBean
private MedalService medalService;
@MockBean
private com.openisle.repository.PollVoteRepository pollVoteRepository;
@Test @Test
void createAndGetPost() throws Exception { void createAndGetPost() throws Exception {
@@ -67,13 +63,9 @@ class PostControllerTest {
Category cat = new Category(); Category cat = new Category();
cat.setId(1L); cat.setId(1L);
cat.setName("tech"); cat.setName("tech");
cat.setDescription("Technology category");
cat.setIcon("tech-icon");
Tag tag = new Tag(); Tag tag = new Tag();
tag.setId(1L); tag.setId(1L);
tag.setName("java"); tag.setName("java");
tag.setDescription("Java programming language");
tag.setIcon("java-icon");
Post post = new Post(); Post post = new Post();
post.setId(1L); post.setId(1L);
post.setTitle("t"); post.setTitle("t");
@@ -119,13 +111,9 @@ class PostControllerTest {
Category cat = new Category(); Category cat = new Category();
cat.setId(1L); cat.setId(1L);
cat.setName("tech"); cat.setName("tech");
cat.setDescription("Technology category");
cat.setIcon("tech-icon");
Tag tag = new Tag(); Tag tag = new Tag();
tag.setId(1L); tag.setId(1L);
tag.setName("java"); tag.setName("java");
tag.setDescription("Java programming language");
tag.setIcon("java-icon");
Post post = new Post(); Post post = new Post();
post.setId(1L); post.setId(1L);
post.setTitle("t2"); post.setTitle("t2");
@@ -159,13 +147,9 @@ class PostControllerTest {
Category cat = new Category(); Category cat = new Category();
cat.setId(1L); cat.setId(1L);
cat.setName("tech"); cat.setName("tech");
cat.setDescription("Technology category");
cat.setIcon("tech-icon");
Tag tag = new Tag(); Tag tag = new Tag();
tag.setId(1L); tag.setId(1L);
tag.setName("java"); tag.setName("java");
tag.setDescription("Java programming language");
tag.setIcon("java-icon");
Post post = new Post(); Post post = new Post();
post.setId(2L); post.setId(2L);
post.setTitle("hello"); post.setTitle("hello");
@@ -213,13 +197,9 @@ class PostControllerTest {
Category cat = new Category(); Category cat = new Category();
cat.setId(1L); cat.setId(1L);
cat.setName("tech"); cat.setName("tech");
cat.setDescription("Technology category");
cat.setIcon("tech-icon");
Tag tag = new Tag(); Tag tag = new Tag();
tag.setId(1L); tag.setId(1L);
tag.setName("java"); tag.setName("java");
tag.setDescription("Java programming language");
tag.setIcon("java-icon");
Post post = new Post(); Post post = new Post();
post.setId(1L); post.setId(1L);
post.setTitle("t"); post.setTitle("t");
@@ -282,8 +262,6 @@ class PostControllerTest {
Category cat = new Category(); Category cat = new Category();
cat.setId(1L); cat.setId(1L);
cat.setName("tech"); cat.setName("tech");
cat.setDescription("Technology category");
cat.setIcon("tech-icon");
Post post = new Post(); Post post = new Post();
post.setId(1L); post.setId(1L);
post.setTitle("t"); post.setTitle("t");

View File

@@ -1,90 +0,0 @@
package com.openisle.service;
import com.openisle.model.*;
import com.openisle.repository.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@TestPropertySource(locations = "classpath:application.properties")
@Transactional
public class PostCommentStatsTest {
@Autowired
private PostRepository postRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private CategoryRepository categoryRepository;
@Autowired
private TagRepository tagRepository;
@Autowired
private CommentService commentService;
@Test
public void testPostCommentStatsUpdate() {
// Create test user
User user = new User();
user.setUsername("testuser");
user.setEmail("test@example.com");
user.setPassword("hash");
user = userRepository.save(user);
// Create test category
Category category = new Category();
category.setName("Test Category");
category.setDescription("Test Category Description");
category.setIcon("test-icon");
category = categoryRepository.save(category);
// Create test tag
Tag tag = new Tag();
tag.setName("Test Tag");
tag.setDescription("Test Tag Description");
tag.setIcon("test-tag-icon");
tag = tagRepository.save(tag);
// Create test post
Post post = new Post();
post.setTitle("Test Post");
post.setContent("Test content");
post.setAuthor(user);
post.setCategory(category);
post.getTags().add(tag);
post.setStatus(PostStatus.PUBLISHED);
post.setCommentCount(0L);
post = postRepository.save(post);
// Verify initial state
assertEquals(0L, post.getCommentCount());
assertNull(post.getLastReplyAt());
// Add a comment
commentService.addComment("testuser", post.getId(), "Test comment");
// Refresh post from database
post = postRepository.findById(post.getId()).orElseThrow();
// Verify comment count and last reply time are updated
assertEquals(1L, post.getCommentCount());
assertNotNull(post.getLastReplyAt());
// Add another comment
commentService.addComment("testuser", post.getId(), "Another comment");
// Refresh post again
post = postRepository.findById(post.getId()).orElseThrow();
// Verify comment count is updated
assertEquals(2L, post.getCommentCount());
}
}

View File

@@ -6,15 +6,11 @@ import com.openisle.exception.RateLimitException;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.TaskScheduler;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.List;
import org.mockito.ArgumentCaptor;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@@ -41,15 +37,11 @@ class PostServiceTest {
EmailSender emailSender = mock(EmailSender.class); EmailSender emailSender = mock(EmailSender.class);
ApplicationContext context = mock(ApplicationContext.class); ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class); PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
RedisTemplate redisTemplate = mock(RedisTemplate.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService, reactionRepo, subRepo, notificationRepo, postReadService,
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
pointHistoryRepository, PublishMode.DIRECT, redisTemplate);
when(context.getBean(PostService.class)).thenReturn(service); when(context.getBean(PostService.class)).thenReturn(service);
Post post = new Post(); Post post = new Post();
@@ -65,13 +57,11 @@ class PostServiceTest {
when(reactionRepo.findByPost(post)).thenReturn(List.of()); when(reactionRepo.findByPost(post)).thenReturn(List.of());
when(subRepo.findByPost(post)).thenReturn(List.of()); when(subRepo.findByPost(post)).thenReturn(List.of());
when(notificationRepo.findByPost(post)).thenReturn(List.of()); when(notificationRepo.findByPost(post)).thenReturn(List.of());
when(pointHistoryRepository.findByPost(post)).thenReturn(List.of());
service.deletePost(1L, "alice"); service.deletePost(1L, "alice");
verify(postReadService).deleteByPost(post); verify(postReadService).deleteByPost(post);
verify(postRepo).delete(post); verify(postRepo).delete(post);
verify(postChangeLogService).deleteLogsForPost(post);
} }
@Test @Test
@@ -96,15 +86,11 @@ class PostServiceTest {
EmailSender emailSender = mock(EmailSender.class); EmailSender emailSender = mock(EmailSender.class);
ApplicationContext context = mock(ApplicationContext.class); ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class); PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
RedisTemplate redisTemplate = mock(RedisTemplate.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService, reactionRepo, subRepo, notificationRepo, postReadService,
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
pointHistoryRepository, PublishMode.DIRECT, redisTemplate);
when(context.getBean(PostService.class)).thenReturn(service); when(context.getBean(PostService.class)).thenReturn(service);
Post post = new Post(); Post post = new Post();
@@ -126,7 +112,6 @@ class PostServiceTest {
when(reactionRepo.findByPost(post)).thenReturn(List.of()); when(reactionRepo.findByPost(post)).thenReturn(List.of());
when(subRepo.findByPost(post)).thenReturn(List.of()); when(subRepo.findByPost(post)).thenReturn(List.of());
when(notificationRepo.findByPost(post)).thenReturn(List.of()); when(notificationRepo.findByPost(post)).thenReturn(List.of());
when(pointHistoryRepository.findByPost(post)).thenReturn(List.of());
service.deletePost(1L, "admin"); service.deletePost(1L, "admin");
@@ -156,15 +141,11 @@ class PostServiceTest {
EmailSender emailSender = mock(EmailSender.class); EmailSender emailSender = mock(EmailSender.class);
ApplicationContext context = mock(ApplicationContext.class); ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class); PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
RedisTemplate redisTemplate = mock(RedisTemplate.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService, reactionRepo, subRepo, notificationRepo, postReadService,
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
pointHistoryRepository, PublishMode.DIRECT, redisTemplate);
when(context.getBean(PostService.class)).thenReturn(service); when(context.getBean(PostService.class)).thenReturn(service);
when(postRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(1L); when(postRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(1L);
@@ -174,77 +155,6 @@ class PostServiceTest {
null, null, null, null, null, null, null, null, null)); null, null, null, null, null, null, null, null, null));
} }
@Test
void deletePostRemovesPointHistoriesAndRecalculatesPoints() {
PostRepository postRepo = mock(PostRepository.class);
UserRepository userRepo = mock(UserRepository.class);
CategoryRepository catRepo = mock(CategoryRepository.class);
TagRepository tagRepo = mock(TagRepository.class);
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
PollPostRepository pollPostRepo = mock(PollPostRepository.class);
PollVoteRepository pollVoteRepo = mock(PollVoteRepository.class);
NotificationService notifService = mock(NotificationService.class);
SubscriptionService subService = mock(SubscriptionService.class);
CommentService commentService = mock(CommentService.class);
CommentRepository commentRepo = mock(CommentRepository.class);
ReactionRepository reactionRepo = mock(ReactionRepository.class);
PostSubscriptionRepository subRepo = mock(PostSubscriptionRepository.class);
NotificationRepository notificationRepo = mock(NotificationRepository.class);
PostReadService postReadService = mock(PostReadService.class);
ImageUploader imageUploader = mock(ImageUploader.class);
TaskScheduler taskScheduler = mock(TaskScheduler.class);
EmailSender emailSender = mock(EmailSender.class);
ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.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,
pointHistoryRepository, PublishMode.DIRECT, redisTemplate);
when(context.getBean(PostService.class)).thenReturn(service);
Post post = new Post();
post.setId(10L);
User author = new User();
author.setId(20L);
author.setRole(Role.USER);
post.setAuthor(author);
User historyUser = new User();
historyUser.setId(30L);
PointHistory history = new PointHistory();
history.setUser(historyUser);
history.setPost(post);
when(postRepo.findById(10L)).thenReturn(Optional.of(post));
when(userRepo.findByUsername("author")).thenReturn(Optional.of(author));
when(commentRepo.findByPostAndParentIsNullOrderByCreatedAtAsc(post)).thenReturn(List.of());
when(reactionRepo.findByPost(post)).thenReturn(List.of());
when(subRepo.findByPost(post)).thenReturn(List.of());
when(notificationRepo.findByPost(post)).thenReturn(List.of());
when(pointHistoryRepository.findByPost(post)).thenReturn(List.of(history));
when(pointService.recalculateUserPoints(historyUser)).thenReturn(0);
service.deletePost(10L, "author");
ArgumentCaptor<List<PointHistory>> captor = ArgumentCaptor.forClass(List.class);
verify(pointHistoryRepository).saveAll(captor.capture());
List<PointHistory> savedHistories = captor.getValue();
assertEquals(1, savedHistories.size());
PointHistory savedHistory = savedHistories.get(0);
assertNull(savedHistory.getPost());
assertNotNull(savedHistory.getDeletedAt());
assertTrue(savedHistory.getDeletedAt().isBefore(LocalDateTime.now().plusSeconds(1)));
verify(pointService).recalculateUserPoints(historyUser);
verify(userRepo).saveAll(any());
}
@Test @Test
void finalizeLotteryNotifiesAuthor() { void finalizeLotteryNotifiesAuthor() {
PostRepository postRepo = mock(PostRepository.class); PostRepository postRepo = mock(PostRepository.class);
@@ -267,15 +177,11 @@ class PostServiceTest {
EmailSender emailSender = mock(EmailSender.class); EmailSender emailSender = mock(EmailSender.class);
ApplicationContext context = mock(ApplicationContext.class); ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class); PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
RedisTemplate redisTemplate = mock(RedisTemplate.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService, reactionRepo, subRepo, notificationRepo, postReadService,
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
pointHistoryRepository, PublishMode.DIRECT, redisTemplate);
when(context.getBean(PostService.class)).thenReturn(service); when(context.getBean(PostService.class)).thenReturn(service);
User author = new User(); User author = new User();

View File

@@ -4,18 +4,7 @@ spring.datasource.username=sa
spring.datasource.password= spring.datasource.password=
spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.hibernate.ddl-auto=create-drop
springdoc.info.title=openisle
springdoc.info.description=Test API documentation
springdoc.info.version=1.0.0
springdoc.info.scheme=Bearer
springdoc.info.header=Authorization
rabbitmq.queue.durable=true
rabbitmq.sharding.enabled=true
resend.api.key=dummy resend.api.key=dummy
resend.from.email=dummy@example.com
cos.base-url=http://test.example.com cos.base-url=http://test.example.com
cos.secret-id=dummy cos.secret-id=dummy
cos.secret-key=dummy cos.secret-key=dummy
@@ -29,7 +18,6 @@ app.upload.max-size=1048576
app.jwt.secret=TestSecret app.jwt.secret=TestSecret
app.jwt.reason-secret=TestReasonSecret app.jwt.reason-secret=TestReasonSecret
app.jwt.reset-secret=TestResetSecret app.jwt.reset-secret=TestResetSecret
app.jwt.invite-secret=TestInviteSecret
app.jwt.expiration=3600000 app.jwt.expiration=3600000
# Default publish mode for tests # Default publish mode for tests

View File

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

View File

@@ -19,7 +19,7 @@ function DocsCategory({ url }: { url: string }) {
); );
} }
export default async function Page(props: PageProps<'/[[...slug]]'>) { export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
const params = await props.params; const params = await props.params;
const page = source.getPage(params.slug); const page = source.getPage(params.slug);
if (!page) notFound(); if (!page) notFound();
@@ -48,7 +48,7 @@ export async function generateStaticParams() {
} }
export async function generateMetadata( export async function generateMetadata(
props: PageProps<'/[[...slug]]'> props: PageProps<'/docs/[[...slug]]'>
): Promise<Metadata> { ): Promise<Metadata> {
const params = await props.params; const params = await props.params;
const page = source.getPage(params.slug); const page = source.getPage(params.slug);

View File

@@ -28,7 +28,7 @@ function TabTitle({ children }: { children: React.ReactNode }) {
return <span className="text-[11px]">{children}</span>; return <span className="text-[11px]">{children}</span>;
} }
export default function Layout({ children }: LayoutProps<'/'>) { export default function Layout({ children }: LayoutProps<'/docs'>) {
return ( return (
// @ts-ignore // @ts-ignore
<DocsLayout <DocsLayout
@@ -40,7 +40,7 @@ export default function Layout({ children }: LayoutProps<'/'>) {
{ {
title: 'OpenIsle 前端', title: 'OpenIsle 前端',
description: <TabTitle></TabTitle>, description: <TabTitle></TabTitle>,
url: '/frontend', url: '/docs/frontend',
icon: ( icon: (
<TabIcon color="#4ca154"> <TabIcon color="#4ca154">
<CompassIcon /> <CompassIcon />
@@ -50,7 +50,7 @@ export default function Layout({ children }: LayoutProps<'/'>) {
{ {
title: 'OpenIsle 后端', title: 'OpenIsle 后端',
description: <TabTitle></TabTitle>, description: <TabTitle></TabTitle>,
url: '/backend', url: '/docs/backend',
icon: ( icon: (
<TabIcon color="#1f66f4"> <TabIcon color="#1f66f4">
<ServerIcon /> <ServerIcon />
@@ -60,7 +60,7 @@ export default function Layout({ children }: LayoutProps<'/'>) {
{ {
title: 'OpenIsle API', title: 'OpenIsle API',
description: <TabTitle> API </TabTitle>, description: <TabTitle> API </TabTitle>,
url: '/openapi', url: '/docs/openapi',
icon: ( icon: (
<TabIcon color="#677489"> <TabIcon color="#677489">
<CodeXmlIcon /> <CodeXmlIcon />

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ export function baseOptions(): BaseLayoutProps {
githubUrl: 'https://github.com/nagisa77/OpenIsle', githubUrl: 'https://github.com/nagisa77/OpenIsle',
nav: { nav: {
title: 'OpenIsle Docs', title: 'OpenIsle Docs',
url: '/', url: '/docs',
}, },
searchToggle: { searchToggle: {
enabled: false, enabled: false,

View File

@@ -10,7 +10,7 @@ import * as ClientAdapters from './media-adapter.client';
// See https://fumadocs.vercel.app/docs/headless/source-api for more info // See https://fumadocs.vercel.app/docs/headless/source-api for more info
export const source = loader({ export const source = loader({
// it assigns a URL to your pages // it assigns a URL to your pages
baseUrl: '/', baseUrl: '/docs',
source: docs.toFumadocsSource(), source: docs.toFumadocsSource(),
pageTree: { pageTree: {
transformers: [transformerOpenAPI()], transformers: [transformerOpenAPI()],

View File

@@ -4,9 +4,7 @@ NUXT_PUBLIC_WEBSOCKET_URL=https://127.0.0.1:8082
NUXT_PUBLIC_WEBSITE_BASE_URL=http://localhost:3000 NUXT_PUBLIC_WEBSITE_BASE_URL=http://localhost:3000
NUXT_PUBLIC_GOOGLE_CLIENT_ID=777830451304-nt8afkkap18gui4f9entcha99unal744.apps.googleusercontent.com NUXT_PUBLIC_GOOGLE_CLIENT_ID=777830451304-nt8afkkap18gui4f9entcha99unal744.apps.googleusercontent.com
# NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liVkO1NPAX5JyWxJ NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liVkO1NPAX5JyWxJ
; 本地
NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liOlrZnPKRF7s7NN
NUXT_PUBLIC_DISCORD_CLIENT_ID=1394985417044000779 NUXT_PUBLIC_DISCORD_CLIENT_ID=1394985417044000779
NUXT_PUBLIC_TWITTER_CLIENT_ID=ZTRTU05KSk9KTTJrTTdrVC1tc1E6MTpjaQ NUXT_PUBLIC_TWITTER_CLIENT_ID=ZTRTU05KSk9KTTJrTTdrVC1tc1E6MTpjaQ
NUXT_PUBLIC_TELEGRAM_BOT_ID=8450237135 NUXT_PUBLIC_TELEGRAM_BOT_ID=8450237135

View File

@@ -2,8 +2,6 @@
--primary-color-hover: rgb(9, 95, 105); --primary-color-hover: rgb(9, 95, 105);
--primary-color: rgb(10, 110, 120); --primary-color: rgb(10, 110, 120);
--primary-color-disabled: rgba(93, 152, 156, 0.5); --primary-color-disabled: rgba(93, 152, 156, 0.5);
--secondary-color: rgb(255, 255, 255);
--secondary-color-hover: rgba(10, 111, 120, 0.184);
--new-post-icon-color: rgba(10, 111, 120, 0.598); --new-post-icon-color: rgba(10, 111, 120, 0.598);
--header-height: 60px; --header-height: 60px;
--header-background-color: white; --header-background-color: white;
@@ -19,7 +17,7 @@
--background-color: white; --background-color: white;
--background-color-blur: rgba(255, 255, 255, 0.57); --background-color-blur: rgba(255, 255, 255, 0.57);
--menu-border-color: lightgray; --menu-border-color: lightgray;
--normal-border-color: rgba(211, 211, 211, 0.63); --normal-border-color: lightgray;
--menu-selected-background-color: rgba(88, 241, 255, 0.166); --menu-selected-background-color: rgba(88, 241, 255, 0.166);
--normal-light-background-color: rgba(242, 242, 242, 0.884); --normal-light-background-color: rgba(242, 242, 242, 0.884);
--menu-selected-background-color-hover: rgba(242, 242, 242, 0.884); --menu-selected-background-color-hover: rgba(242, 242, 242, 0.884);
@@ -241,16 +239,8 @@ body {
} }
.info-content-text img { .info-content-text img {
max-width: min(800px, 100%); max-width: 100%;
max-height: 600px;
height: auto; height: auto;
cursor: pointer;
box-shadow: 4px 12px 48px 0 rgba(0, 0, 0, 0.11);
transition: box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.info-content-text img:hover {
box-shadow: 4px 12px 48px 0 rgba(0, 0, 0, 0.35);
} }
.info-content-text table { .info-content-text table {
@@ -356,41 +346,9 @@ body {
position: relative; position: relative;
min-width: 0; min-width: 0;
} }
.d2h-file-name {
font-size: 14px !important;
}
.d2h-file-header {
height: auto !important;
}
.d2h-code-linenumber {
display: none !important;
}
.d2h-code-line {
padding-left: 10px !important;
}
/* .d2h-diff-table {
font-size: 6px !important;
}
.d2h-code-line ins {
height: 100%;
font-size: 13px !important;
} */
/* .d2h-code-line {
height: 12px;
}
.d2h-code-line-ctn {
font-size: 12px !important;
} */
} }
/* Transition API */
::view-transition-old(root), ::view-transition-old(root),
::view-transition-new(root) { ::view-transition-new(root) {
animation: none; animation: none;

View File

@@ -35,7 +35,6 @@ const isImageIcon = (icon) => {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 10px; gap: 10px;
min-height: 25px;
} }
.article-info-item { .article-info-item {
@@ -64,9 +63,5 @@ const isImageIcon = (icon) => {
.article-info-item { .article-info-item {
font-size: 10px; font-size: 10px;
} }
.article-category-container {
min-height: 20px;
}
} }
</style> </style>

View File

@@ -44,7 +44,6 @@ const isImageIcon = (icon) => {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 10px; gap: 10px;
min-height: 25px;
} }
.article-info-item { .article-info-item {
@@ -73,9 +72,5 @@ const isImageIcon = (icon) => {
.article-info-item { .article-info-item {
font-size: 10px; font-size: 10px;
} }
.article-tags-container {
min-height: 20px;
}
} }
</style> </style>

View File

@@ -100,7 +100,7 @@ export default {
.timeline-content { .timeline-content {
flex: 1; flex: 1;
width: calc(100% - 42px); width: calc(100% - 32px);
} }
@media (max-width: 768px) { @media (max-width: 768px) {

View File

@@ -6,15 +6,8 @@
</div> </div>
<div class="comment-bottom-container"> <div class="comment-bottom-container">
<div class="comment-submit" :class="{ disabled: isDisabled }" @click="submit"> <div class="comment-submit" :class="{ disabled: isDisabled }" @click="submit">
<template v-if="!loading"> <template v-if="!loading"> 发布评论 </template>
发布评论 <template v-else> <loading-four /> 发布中... </template>
<span class="shortcut-icon" v-if="!isMobile">
{{ isMac ? '' : 'Ctrl' }}
</span>
</template>
<template v-else>
<loading-four /> 发布中...
</template>
</div> </div>
</div> </div>
</div> </div>
@@ -31,7 +24,6 @@ import {
} from '~/utils/vditor' } from '~/utils/vditor'
import '~/assets/global.css' import '~/assets/global.css'
import LoginOverlay from '~/components/LoginOverlay.vue' import LoginOverlay from '~/components/LoginOverlay.vue'
import { useIsMobile } from '~/utils/screen'
export default { export default {
name: 'CommentEditor', name: 'CommentEditor',
@@ -60,22 +52,12 @@ export default {
}, },
components: { LoginOverlay }, components: { LoginOverlay },
setup(props, { emit }) { setup(props, { emit }) {
const isMobile = useIsMobile()
const vditorInstance = ref(null) const vditorInstance = ref(null)
const text = ref('') const text = ref('')
const editorId = ref(props.editorId) const editorId = ref(props.editorId)
if (!editorId.value) { if (!editorId.value) {
editorId.value = 'editor-' + useId() editorId.value = 'editor-' + useId()
} }
const isMac = ref(false)
if (navigator.userAgentData) {
isMac.value = navigator.userAgentData.platform === 'macOS'
} else {
isMac.value = /Mac|iPhone|iPad|iPod/.test(navigator.userAgent)
}
const getEditorTheme = getEditorThemeUtil const getEditorTheme = getEditorThemeUtil
const getPreviewTheme = getPreviewThemeUtil const getPreviewTheme = getPreviewThemeUtil
const applyTheme = () => { const applyTheme = () => {
@@ -114,27 +96,7 @@ export default {
applyTheme() applyTheme()
}, },
}) })
// 不是手机的情况下不添加快捷键 // applyTheme()
if(!isMobile.value){
// 添加快捷键监听 (Ctrl+Enter 或 Cmd+Enter)
const handleKeydown = (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault()
submit()
}
}
const el = document.getElementById(editorId.value)
if (el) {
el.addEventListener('keydown', handleKeydown)
}
onUnmounted(() => {
if (el) {
el.removeEventListener('keydown', handleKeydown)
}
})
}
}) })
onUnmounted(() => { onUnmounted(() => {
@@ -172,7 +134,7 @@ export default {
}, },
) )
return { submit, isDisabled, editorId, isMac, isMobile} return { submit, isDisabled, editorId }
}, },
} }
</script> </script>
@@ -212,16 +174,10 @@ export default {
.comment-submit:hover { .comment-submit:hover {
background-color: var(--primary-color-hover); background-color: var(--primary-color-hover);
} }
/** 评论按钮快捷键样式 */
.shortcut-icon { @media (max-width: 768px) {
padding: 2px 6px; .comment-editor-container {
border-radius: 6px; margin-bottom: 10px;
font-size: 12px; }
font-weight: 500;
line-height: 1.2;
background-color: rgba(0, 0, 0, 0.25);
}
.comment-submit.disabled .shortcut-icon {
background-color: rgba(0, 0, 0, 0);
} }
</style> </style>

View File

@@ -342,7 +342,7 @@ const copyCommentLink = () => {
const handleContentClick = (e) => { const handleContentClick = (e) => {
handleMarkdownClick(e) handleMarkdownClick(e)
if (e.target.tagName === 'IMG' && !e.target.classList.contains('emoji')) { if (e.target.tagName === 'IMG') {
const container = e.target.parentNode const container = e.target.parentNode
const imgs = [...container.querySelectorAll('img')].map((i) => i.src) const imgs = [...container.querySelectorAll('img')].map((i) => i.src)
lightboxImgs.value = imgs lightboxImgs.value = imgs

View File

@@ -314,7 +314,6 @@ const gotoTag = (t) => {
border-radius: 10px; border-radius: 10px;
display: flex; display: flex;
align-items: center; align-items: center;
transition: background-color 0.5s ease;
} }
.menu-item:hover { .menu-item:hover {
@@ -409,7 +408,6 @@ const gotoTag = (t) => {
gap: 5px; gap: 5px;
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
transition: background-color 0.5s ease;
} }
.section-item:hover { .section-item:hover {

View File

@@ -5,10 +5,7 @@
</div> </div>
<div class="message-bottom-container"> <div class="message-bottom-container">
<div class="message-submit" :class="{ disabled: isDisabled }" @click="submit"> <div class="message-submit" :class="{ disabled: isDisabled }" @click="submit">
<template v-if="!loading"> <template v-if="!loading"> 发送 </template>
发送
<span class="shortcut-icon" v-if="!isMobile"> {{ isMac ? '' : 'Ctrl' }} </span>
</template>
<template v-else> <loading-four /> 发送中... </template> <template v-else> <loading-four /> 发送中... </template>
</div> </div>
</div> </div>
@@ -24,8 +21,6 @@ import {
getEditorTheme as getEditorThemeUtil, getEditorTheme as getEditorThemeUtil,
getPreviewTheme as getPreviewThemeUtil, getPreviewTheme as getPreviewThemeUtil,
} from '~/utils/vditor' } from '~/utils/vditor'
import { useIsMobile } from '~/utils/screen'
import { isMac } from '~/utils/device'
import '~/assets/global.css' import '~/assets/global.css'
export default { export default {
@@ -49,7 +44,6 @@ export default {
const vditorInstance = ref(null) const vditorInstance = ref(null)
const text = ref('') const text = ref('')
const editorId = ref(props.editorId) const editorId = ref(props.editorId)
const isMobile = useIsMobile()
if (!editorId.value) { if (!editorId.value) {
editorId.value = 'editor-' + useId() editorId.value = 'editor-' + useId()
} }
@@ -76,6 +70,23 @@ export default {
onMounted(() => { onMounted(() => {
vditorInstance.value = createVditor(editorId.value, { vditorInstance.value = createVditor(editorId.value, {
placeholder: '输入消息...', placeholder: '输入消息...',
height: 150,
toolbar: [
'emoji',
'bold',
'italic',
'strike',
'link',
'|',
'list',
'|',
'line',
'quote',
'code',
'inline-code',
'|',
'upload',
],
preview: { preview: {
actions: [], actions: [],
markdown: { toc: false }, markdown: { toc: false },
@@ -90,28 +101,6 @@ export default {
applyTheme() applyTheme()
}, },
}) })
// 不是手机的情况下不添加快捷键
if (!isMobile.value) {
// 添加快捷键监听 (Ctrl+Enter 或 Cmd+Enter)
const handleKeydown = (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault()
submit()
}
}
const el = document.getElementById(editorId.value)
if (el) {
el.addEventListener('keydown', handleKeydown)
}
onUnmounted(() => {
if (el) {
el.removeEventListener('keydown', handleKeydown)
}
})
}
}) })
onUnmounted(() => { onUnmounted(() => {
@@ -149,7 +138,7 @@ export default {
}, },
) )
return { submit, isDisabled, editorId, isMac, isMobile } return { submit, isDisabled, editorId }
}, },
} }
</script> </script>
@@ -160,17 +149,11 @@ export default {
border-radius: 8px; border-radius: 8px;
} }
.vditor {
min-height: 50px;
max-height: 150px;
}
.message-bottom-container { .message-bottom-container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-end; justify-content: flex-end;
margin-top: 10px; padding: 10px;
margin-bottom: 10px;
background-color: var(--bg-color-soft); background-color: var(--bg-color-soft);
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
border-bottom-left-radius: 8px; border-bottom-left-radius: 8px;
@@ -196,17 +179,4 @@ export default {
.message-submit:not(.disabled):hover { .message-submit:not(.disabled):hover {
background-color: var(--primary-color-hover); background-color: var(--primary-color-hover);
} }
/** 评论按钮快捷键样式 */
.shortcut-icon {
padding: 2px 6px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
line-height: 1.2;
background-color: rgba(0, 0, 0, 0.25);
}
.comment-submit.disabled .shortcut-icon {
background-color: rgba(0, 0, 0, 0);
}
</style> </style>

View File

@@ -0,0 +1,33 @@
<template>
<div class="new-message-container" :style="{ bottom: bottom + 'px' }" @click="$emit('click')">
{{ count }} 条新消息点击查看
</div>
</template>
<script setup>
const props = defineProps({
count: {
type: Number,
default: 0,
},
bottom: {
type: Number,
default: 0,
},
})
</script>
<style scoped>
.new-message-container {
position: absolute;
left: 50%;
transform: translateX(-50%);
background-color: var(--primary-color);
color: #fff;
padding: 6px 16px;
border-radius: 20px;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 50;
}
</style>

View File

@@ -1,154 +0,0 @@
<template>
<div :id="`change-log-${log.id}`" class="change-log-container">
<div class="change-log-text">
<BaseImage
v-if="log.userAvatar"
class="change-log-avatar"
:src="log.userAvatar"
alt="avatar"
@click="() => navigateTo(`/users/${log.username}`)"
/>
<span v-if="log.username" class="change-log-user">{{ log.username }}</span>
<span v-if="log.type === 'CONTENT'" class="change-log-content">变更了文章内容</span>
<span v-else-if="log.type === 'TITLE'" class="change-log-content">变更了文章标题</span>
<template v-else-if="log.type === 'CATEGORY'">
<div class="change-log-category-text">变更了文章分类, </div>
<ArticleCategory :category="log.oldCategory" />
<div class="change-log-category-text">修改为</div>
<ArticleCategory :category="log.newCategory" />
</template>
<template v-else-if="log.type === 'TAG'">
<div class="change-log-category-text">变更了文章标签, </div>
<ArticleTags :tags="log.oldTags" />
<div class="change-log-category-text">修改为</div>
<ArticleTags :tags="log.newTags" />
</template>
<span v-else-if="log.type === 'CLOSED'" class="change-log-content">
<template v-if="log.newClosed">关闭了文章</template>
<template v-else>重新打开了文章</template>
</span>
<span v-else-if="log.type === 'PINNED'" class="change-log-content">
<template v-if="log.newPinnedAt">置顶了文章</template>
<template v-else>取消置顶文章</template>
</span>
<span v-else-if="log.type === 'FEATURED'" class="change-log-content">
<template v-if="log.newFeatured">将文章设为精选</template>
<template v-else>取消精选文章</template>
</span>
<span v-else-if="log.type === 'VOTE_RESULT'" class="change-log-content"
>系统已计算投票结果</span
>
<span v-else-if="log.type === 'LOTTERY_RESULT'" class="change-log-content"
>系统已精密计算抽奖结果 (=゚ω゚)</span
>
</div>
<div class="change-log-time">{{ log.time }}</div>
<div
v-if="log.type === 'CONTENT' || log.type === 'TITLE'"
class="content-diff"
v-html="diffHtml"
></div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { html } from 'diff2html'
import { createTwoFilesPatch } from 'diff'
import { useIsMobile } from '~/utils/screen'
import 'diff2html/bundles/css/diff2html.min.css'
import BaseImage from '~/components/BaseImage.vue'
import { navigateTo } from 'nuxt/app'
import { themeState } from '~/utils/theme'
import ArticleCategory from '~/components/ArticleCategory.vue'
import ArticleTags from '~/components/ArticleTags.vue'
const props = defineProps({
log: Object,
title: String,
})
const diffHtml = computed(() => {
// Track theme changes
const isDark = import.meta.client && document.documentElement.dataset.theme === 'dark'
themeState.mode
const colorScheme = isDark ? 'dark' : 'light'
if (props.log.type === 'CONTENT') {
const oldContent = props.log.oldContent ?? ''
const newContent = props.log.newContent ?? ''
const diff = createTwoFilesPatch(props.title, props.title, oldContent, newContent)
return html(diff, {
inputFormat: 'diff',
showFiles: false,
matching: 'lines',
drawFileList: false,
colorScheme,
})
} else if (props.log.type === 'TITLE') {
const oldTitle = props.log.oldTitle ?? ''
const newTitle = props.log.newTitle ?? ''
const diff = createTwoFilesPatch(oldTitle, newTitle, '', '')
return html(diff, {
inputFormat: 'diff',
showFiles: false,
matching: 'lines',
drawFileList: false,
colorScheme,
})
}
return ''
})
</script>
<style scoped>
.change-log-container {
display: flex;
flex-direction: column;
/* padding-top: 5px; */
/* padding-bottom: 30px; */
font-size: 14px;
border-bottom: 1px solid var(--normal-border-color);
padding-bottom: 10px;
}
.change-log-text {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.change-log-user {
font-weight: bold;
margin-right: 4px;
}
.change-log-user,
.change-log-content {
opacity: 0.7;
}
.change-log-avatar {
width: 20px;
height: 20px;
border-radius: 50%;
margin-right: 4px;
cursor: pointer;
}
.change-log-time {
font-size: 12px;
opacity: 0.6;
}
.content-diff {
margin-top: 8px;
}
.change-log-category {
display: flex;
flex-direction: row;
gap: 4px;
align-items: center;
flex-wrap: wrap;
}
</style>

View File

@@ -107,33 +107,11 @@ const likeCount = computed(() => counts.value['LIKE'] || 0)
const userReacted = (type) => const userReacted = (type) =>
reactions.value.some((r) => r.type === type && r.user === authState.username) reactions.value.some((r) => r.type === type && r.user === authState.username)
const defaultOrder = computed(() => {
if (reactionTypes.value && reactionTypes.value.length) {
return reactionTypes.value
}
const seen = new Set()
const order = []
for (const reaction of reactions.value) {
if (!seen.has(reaction.type)) {
seen.add(reaction.type)
order.push(reaction.type)
}
}
return order
})
const displayedReactions = computed(() => { const displayedReactions = computed(() => {
const orderIndex = new Map(defaultOrder.value.map((type, index) => [type, index]))
return Object.entries(counts.value) return Object.entries(counts.value)
.map(([type, count]) => ({ type, count })) .sort((a, b) => b[1] - a[1])
.sort((a, b) => {
if (b.count !== a.count) return b.count - a.count
const indexA = orderIndex.has(a.type) ? orderIndex.get(a.type) : Number.MAX_SAFE_INTEGER
const indexB = orderIndex.has(b.type) ? orderIndex.get(b.type) : Number.MAX_SAFE_INTEGER
return indexA - indexB
})
.slice(0, 3) .slice(0, 3)
.map(({ type }) => ({ type })) .map(([type]) => ({ type }))
}) })
const panelTypes = computed(() => reactionTypes.value.filter((t) => t !== 'LIKE')) const panelTypes = computed(() => reactionTypes.value.filter((t) => t !== 'LIKE'))

Some files were not shown because too many files have changed in this diff Show More