mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-05-08 11:47:28 +08:00
fix: 后端代码格式化
This commit is contained in:
@@ -4,53 +4,53 @@ import com.openisle.exception.NotFoundException;
|
||||
import com.openisle.model.*;
|
||||
import com.openisle.repository.ActivityRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ActivityService {
|
||||
private final ActivityRepository activityRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final LevelService levelService;
|
||||
private final NotificationService notificationService;
|
||||
|
||||
public List<Activity> list() {
|
||||
return activityRepository.findAll();
|
||||
}
|
||||
private final ActivityRepository activityRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final LevelService levelService;
|
||||
private final NotificationService notificationService;
|
||||
|
||||
public Activity getByType(ActivityType type) {
|
||||
Activity a = activityRepository.findByType(type);
|
||||
if (a == null) throw new NotFoundException("Activity not found");
|
||||
return a;
|
||||
}
|
||||
public List<Activity> list() {
|
||||
return activityRepository.findAll();
|
||||
}
|
||||
|
||||
public long countLevel1Users() {
|
||||
int threshold = levelService.nextLevelExp(0);
|
||||
return userRepository.countByExperienceGreaterThanEqual(threshold);
|
||||
}
|
||||
public Activity getByType(ActivityType type) {
|
||||
Activity a = activityRepository.findByType(type);
|
||||
if (a == null) throw new NotFoundException("Activity not found");
|
||||
return a;
|
||||
}
|
||||
|
||||
public void end(Activity activity) {
|
||||
activity.setEnded(true);
|
||||
activityRepository.save(activity);
|
||||
}
|
||||
public long countLevel1Users() {
|
||||
int threshold = levelService.nextLevelExp(0);
|
||||
return userRepository.countByExperienceGreaterThanEqual(threshold);
|
||||
}
|
||||
|
||||
public long countParticipants(Activity activity) {
|
||||
return activity.getParticipants().size();
|
||||
}
|
||||
public void end(Activity activity) {
|
||||
activity.setEnded(true);
|
||||
activityRepository.save(activity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redeem an activity for the given user.
|
||||
*
|
||||
* @return true if the user redeemed for the first time, false if the
|
||||
* information was simply updated
|
||||
*/
|
||||
public boolean redeem(Activity activity, User user, String contact) {
|
||||
notificationService.createActivityRedeemNotifications(user, contact);
|
||||
boolean added = activity.getParticipants().add(user);
|
||||
activityRepository.save(activity);
|
||||
return added;
|
||||
}
|
||||
public long countParticipants(Activity activity) {
|
||||
return activity.getParticipants().size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redeem an activity for the given user.
|
||||
*
|
||||
* @return true if the user redeemed for the first time, false if the
|
||||
* information was simply updated
|
||||
*/
|
||||
public boolean redeem(Activity activity, User user, String contact) {
|
||||
notificationService.createActivityRedeemNotifications(user, contact);
|
||||
boolean added = activity.getParticipants().add(user);
|
||||
activityRepository.save(activity);
|
||||
return added;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,51 +4,55 @@ import com.openisle.model.AiFormatUsage;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.AiFormatUsageRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import java.time.LocalDate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AiUsageService {
|
||||
private final AiFormatUsageRepository usageRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Value("${app.ai.format-limit:3}")
|
||||
private int formatLimit;
|
||||
private final AiFormatUsageRepository usageRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public int getFormatLimit() {
|
||||
return formatLimit;
|
||||
}
|
||||
@Value("${app.ai.format-limit:3}")
|
||||
private int formatLimit;
|
||||
|
||||
public void setFormatLimit(int formatLimit) {
|
||||
this.formatLimit = formatLimit;
|
||||
}
|
||||
public int getFormatLimit() {
|
||||
return formatLimit;
|
||||
}
|
||||
|
||||
public int incrementAndGetCount(String username) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
LocalDate today = LocalDate.now();
|
||||
AiFormatUsage usage = usageRepository.findByUserAndUseDate(user, today)
|
||||
.orElseGet(() -> {
|
||||
AiFormatUsage u = new AiFormatUsage();
|
||||
u.setUser(user);
|
||||
u.setUseDate(today);
|
||||
u.setCount(0);
|
||||
return u;
|
||||
});
|
||||
usage.setCount(usage.getCount() + 1);
|
||||
usageRepository.save(usage);
|
||||
return usage.getCount();
|
||||
}
|
||||
public void setFormatLimit(int formatLimit) {
|
||||
this.formatLimit = formatLimit;
|
||||
}
|
||||
|
||||
public int getCount(String username) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
return usageRepository.findByUserAndUseDate(user, LocalDate.now())
|
||||
.map(AiFormatUsage::getCount)
|
||||
.orElse(0);
|
||||
}
|
||||
public int incrementAndGetCount(String username) {
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
LocalDate today = LocalDate.now();
|
||||
AiFormatUsage usage = usageRepository
|
||||
.findByUserAndUseDate(user, today)
|
||||
.orElseGet(() -> {
|
||||
AiFormatUsage u = new AiFormatUsage();
|
||||
u.setUser(user);
|
||||
u.setUseDate(today);
|
||||
u.setCount(0);
|
||||
return u;
|
||||
});
|
||||
usage.setCount(usage.getCount() + 1);
|
||||
usageRepository.save(usage);
|
||||
return usage.getCount();
|
||||
}
|
||||
|
||||
public int getCount(String username) {
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
return usageRepository
|
||||
.findByUserAndUseDate(user, LocalDate.now())
|
||||
.map(AiFormatUsage::getCount)
|
||||
.orElse(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import lombok.Value;
|
||||
/** Result for OAuth authentication indicating whether a new user was created. */
|
||||
@Value
|
||||
public class AuthResult {
|
||||
User user;
|
||||
boolean newUser;
|
||||
}
|
||||
|
||||
User user;
|
||||
boolean newUser;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class AvatarGenerator {
|
||||
|
||||
@Value("${app.avatar.base-url}")
|
||||
private String baseUrl;
|
||||
@Value("${app.avatar.base-url}")
|
||||
private String baseUrl;
|
||||
|
||||
@Value("${app.avatar.style}")
|
||||
private String style;
|
||||
@Value("${app.avatar.style}")
|
||||
private String style;
|
||||
|
||||
@Value("${app.avatar.size}")
|
||||
private int size;
|
||||
@Value("${app.avatar.size}")
|
||||
private int size;
|
||||
|
||||
public String generate(String seed) {
|
||||
String encoded = URLEncoder.encode(seed, StandardCharsets.UTF_8);
|
||||
return String.format("%s/%s/png?seed=%s&size=%d", baseUrl, style, encoded, size);
|
||||
}
|
||||
public String generate(String seed) {
|
||||
String encoded = URLEncoder.encode(seed, StandardCharsets.UTF_8);
|
||||
return String.format("%s/%s/png?seed=%s&size=%d", baseUrl, style, encoded, size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@ package com.openisle.service;
|
||||
* Abstract service for verifying CAPTCHA tokens.
|
||||
*/
|
||||
public abstract class CaptchaService {
|
||||
/**
|
||||
* Verify the CAPTCHA token sent from client.
|
||||
*
|
||||
* @param token CAPTCHA token
|
||||
* @return true if token is valid
|
||||
*/
|
||||
public abstract boolean verify(String token);
|
||||
|
||||
/**
|
||||
* Verify the CAPTCHA token sent from client.
|
||||
*
|
||||
* @param token CAPTCHA token
|
||||
* @return true if token is valid
|
||||
*/
|
||||
public abstract boolean verify(String token);
|
||||
}
|
||||
|
||||
@@ -3,77 +3,85 @@ package com.openisle.service;
|
||||
import com.openisle.config.CachingConfig;
|
||||
import com.openisle.model.Category;
|
||||
import com.openisle.repository.CategoryRepository;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CategoryService {
|
||||
private final CategoryRepository categoryRepository;
|
||||
@CacheEvict(value = CachingConfig.CATEGORY_CACHE_NAME, allEntries = true)
|
||||
public Category createCategory(String name, String description, String icon, String smallIcon) {
|
||||
Category category = new Category();
|
||||
category.setName(name);
|
||||
category.setDescription(description);
|
||||
category.setIcon(icon);
|
||||
category.setSmallIcon(smallIcon);
|
||||
return categoryRepository.save(category);
|
||||
}
|
||||
@CacheEvict(value = CachingConfig.CATEGORY_CACHE_NAME, allEntries = true)
|
||||
public Category updateCategory(Long id, String name, String description, String icon, String smallIcon) {
|
||||
Category category = categoryRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Category not found"));
|
||||
if (name != null) {
|
||||
category.setName(name);
|
||||
}
|
||||
if (description != null) {
|
||||
category.setDescription(description);
|
||||
}
|
||||
if (icon != null) {
|
||||
category.setIcon(icon);
|
||||
}
|
||||
if (smallIcon != null) {
|
||||
category.setSmallIcon(smallIcon);
|
||||
}
|
||||
return categoryRepository.save(category);
|
||||
}
|
||||
@CacheEvict(value = CachingConfig.CATEGORY_CACHE_NAME, allEntries = true)
|
||||
public void deleteCategory(Long id) {
|
||||
categoryRepository.deleteById(id);
|
||||
}
|
||||
|
||||
public Category getCategory(Long id) {
|
||||
return categoryRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Category not found"));
|
||||
}
|
||||
private final CategoryRepository categoryRepository;
|
||||
|
||||
/**
|
||||
* 该方法每次首页加载都会访问,加入缓存
|
||||
* @return
|
||||
*/
|
||||
@Cacheable(
|
||||
value = CachingConfig.CATEGORY_CACHE_NAME,
|
||||
key = "'listCategories:'"
|
||||
)
|
||||
public List<Category> listCategories() {
|
||||
return categoryRepository.findAll();
|
||||
}
|
||||
@CacheEvict(value = CachingConfig.CATEGORY_CACHE_NAME, allEntries = true)
|
||||
public Category createCategory(String name, String description, String icon, String smallIcon) {
|
||||
Category category = new Category();
|
||||
category.setName(name);
|
||||
category.setDescription(description);
|
||||
category.setIcon(icon);
|
||||
category.setSmallIcon(smallIcon);
|
||||
return categoryRepository.save(category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检索用的分类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;
|
||||
@CacheEvict(value = CachingConfig.CATEGORY_CACHE_NAME, allEntries = true)
|
||||
public Category updateCategory(
|
||||
Long id,
|
||||
String name,
|
||||
String description,
|
||||
String icon,
|
||||
String smallIcon
|
||||
) {
|
||||
Category category = categoryRepository
|
||||
.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Category not found"));
|
||||
if (name != null) {
|
||||
category.setName(name);
|
||||
}
|
||||
if (description != null) {
|
||||
category.setDescription(description);
|
||||
}
|
||||
if (icon != null) {
|
||||
category.setIcon(icon);
|
||||
}
|
||||
if (smallIcon != null) {
|
||||
category.setSmallIcon(smallIcon);
|
||||
}
|
||||
return categoryRepository.save(category);
|
||||
}
|
||||
|
||||
@CacheEvict(value = CachingConfig.CATEGORY_CACHE_NAME, allEntries = true)
|
||||
public void deleteCategory(Long id) {
|
||||
categoryRepository.deleteById(id);
|
||||
}
|
||||
|
||||
public Category getCategory(Long id) {
|
||||
return categoryRepository
|
||||
.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Category not found"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 该方法每次首页加载都会访问,加入缓存
|
||||
* @return
|
||||
*/
|
||||
@Cacheable(value = CachingConfig.CATEGORY_CACHE_NAME, key = "'listCategories:'")
|
||||
public List<Category> listCategories() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,88 +11,102 @@ import com.openisle.repository.MessageConversationRepository;
|
||||
import com.openisle.repository.MessageParticipantRepository;
|
||||
import com.openisle.repository.MessageRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ChannelService {
|
||||
private final MessageConversationRepository conversationRepository;
|
||||
private final MessageParticipantRepository participantRepository;
|
||||
private final MessageRepository messageRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ChannelDto> listChannels(Long userId) {
|
||||
List<MessageConversation> channels = conversationRepository.findByChannelTrue();
|
||||
return channels.stream().map(c -> toDto(c, userId)).collect(Collectors.toList());
|
||||
private final MessageConversationRepository conversationRepository;
|
||||
private final MessageParticipantRepository participantRepository;
|
||||
private final MessageRepository messageRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ChannelDto> listChannels(Long userId) {
|
||||
List<MessageConversation> channels = conversationRepository.findByChannelTrue();
|
||||
return channels
|
||||
.stream()
|
||||
.map(c -> toDto(c, userId))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ChannelDto joinChannel(Long channelId, Long userId) {
|
||||
MessageConversation channel = conversationRepository
|
||||
.findById(channelId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Channel not found"));
|
||||
User user = userRepository
|
||||
.findById(userId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("User not found"));
|
||||
participantRepository
|
||||
.findByConversationIdAndUserId(channelId, userId)
|
||||
.orElseGet(() -> {
|
||||
MessageParticipant p = new MessageParticipant();
|
||||
p.setConversation(channel);
|
||||
p.setUser(user);
|
||||
MessageParticipant saved = participantRepository.save(p);
|
||||
channel.getParticipants().add(saved);
|
||||
return saved;
|
||||
});
|
||||
return toDto(channel, userId);
|
||||
}
|
||||
|
||||
private ChannelDto toDto(MessageConversation channel, Long userId) {
|
||||
ChannelDto dto = new ChannelDto();
|
||||
dto.setId(channel.getId());
|
||||
dto.setName(channel.getName());
|
||||
dto.setDescription(channel.getDescription());
|
||||
dto.setAvatar(channel.getAvatar());
|
||||
if (channel.getLastMessage() != null) {
|
||||
dto.setLastMessage(toMessageDto(channel.getLastMessage()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ChannelDto joinChannel(Long channelId, Long userId) {
|
||||
MessageConversation channel = conversationRepository.findById(channelId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Channel not found"));
|
||||
User user = userRepository.findById(userId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("User not found"));
|
||||
participantRepository.findByConversationIdAndUserId(channelId, userId)
|
||||
.orElseGet(() -> {
|
||||
MessageParticipant p = new MessageParticipant();
|
||||
p.setConversation(channel);
|
||||
p.setUser(user);
|
||||
MessageParticipant saved = participantRepository.save(p);
|
||||
channel.getParticipants().add(saved);
|
||||
return saved;
|
||||
});
|
||||
return toDto(channel, userId);
|
||||
dto.setMemberCount(channel.getParticipants().size());
|
||||
boolean joined = channel
|
||||
.getParticipants()
|
||||
.stream()
|
||||
.anyMatch(p -> p.getUser().getId().equals(userId));
|
||||
dto.setJoined(joined);
|
||||
if (joined) {
|
||||
MessageParticipant participant = channel
|
||||
.getParticipants()
|
||||
.stream()
|
||||
.filter(p -> p.getUser().getId().equals(userId))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
LocalDateTime lastRead = participant.getLastReadAt() == null
|
||||
? LocalDateTime.of(1970, 1, 1, 0, 0)
|
||||
: participant.getLastReadAt();
|
||||
long unread = messageRepository.countByConversationIdAndCreatedAtAfterAndSenderIdNot(
|
||||
channel.getId(),
|
||||
lastRead,
|
||||
userId
|
||||
);
|
||||
dto.setUnreadCount(unread);
|
||||
} else {
|
||||
dto.setUnreadCount(0);
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
|
||||
private ChannelDto toDto(MessageConversation channel, Long userId) {
|
||||
ChannelDto dto = new ChannelDto();
|
||||
dto.setId(channel.getId());
|
||||
dto.setName(channel.getName());
|
||||
dto.setDescription(channel.getDescription());
|
||||
dto.setAvatar(channel.getAvatar());
|
||||
if (channel.getLastMessage() != null) {
|
||||
dto.setLastMessage(toMessageDto(channel.getLastMessage()));
|
||||
}
|
||||
dto.setMemberCount(channel.getParticipants().size());
|
||||
boolean joined = channel.getParticipants().stream()
|
||||
.anyMatch(p -> p.getUser().getId().equals(userId));
|
||||
dto.setJoined(joined);
|
||||
if (joined) {
|
||||
MessageParticipant participant = channel.getParticipants().stream()
|
||||
.filter(p -> p.getUser().getId().equals(userId))
|
||||
.findFirst().orElse(null);
|
||||
LocalDateTime lastRead = participant.getLastReadAt() == null
|
||||
? LocalDateTime.of(1970, 1, 1, 0, 0)
|
||||
: participant.getLastReadAt();
|
||||
long unread = messageRepository
|
||||
.countByConversationIdAndCreatedAtAfterAndSenderIdNot(channel.getId(), lastRead, userId);
|
||||
dto.setUnreadCount(unread);
|
||||
} else {
|
||||
dto.setUnreadCount(0);
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
private MessageDto toMessageDto(Message message) {
|
||||
MessageDto dto = new MessageDto();
|
||||
dto.setId(message.getId());
|
||||
dto.setContent(message.getContent());
|
||||
dto.setConversationId(message.getConversation().getId());
|
||||
dto.setCreatedAt(message.getCreatedAt());
|
||||
|
||||
private MessageDto toMessageDto(Message message) {
|
||||
MessageDto dto = new MessageDto();
|
||||
dto.setId(message.getId());
|
||||
dto.setContent(message.getContent());
|
||||
dto.setConversationId(message.getConversation().getId());
|
||||
dto.setCreatedAt(message.getCreatedAt());
|
||||
UserSummaryDto userDto = new UserSummaryDto();
|
||||
userDto.setId(message.getSender().getId());
|
||||
userDto.setUsername(message.getSender().getUsername());
|
||||
userDto.setAvatar(message.getSender().getAvatar());
|
||||
dto.setSender(userDto);
|
||||
|
||||
UserSummaryDto userDto = new UserSummaryDto();
|
||||
userDto.setId(message.getSender().getId());
|
||||
userDto.setUsername(message.getSender().getUsername());
|
||||
userDto.setAvatar(message.getSender().getAvatar());
|
||||
dto.setSender(userDto);
|
||||
|
||||
return dto;
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,364 +1,441 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import com.openisle.config.CachingConfig;
|
||||
import com.openisle.exception.RateLimitException;
|
||||
import com.openisle.model.Comment;
|
||||
import com.openisle.model.Post;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.model.CommentSort;
|
||||
import com.openisle.model.NotificationType;
|
||||
import com.openisle.model.PointHistory;
|
||||
import com.openisle.model.CommentSort;
|
||||
import com.openisle.model.Post;
|
||||
import com.openisle.model.Role;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.CommentRepository;
|
||||
import com.openisle.repository.PostRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import com.openisle.repository.ReactionRepository;
|
||||
import com.openisle.repository.CommentSubscriptionRepository;
|
||||
import com.openisle.repository.NotificationRepository;
|
||||
import com.openisle.repository.PointHistoryRepository;
|
||||
import com.openisle.repository.PostRepository;
|
||||
import com.openisle.repository.ReactionRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import com.openisle.service.NotificationService;
|
||||
import com.openisle.service.SubscriptionService;
|
||||
import com.openisle.service.PointService;
|
||||
import com.openisle.model.Role;
|
||||
import com.openisle.exception.RateLimitException;
|
||||
import com.openisle.service.SubscriptionService;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class CommentService {
|
||||
private final CommentRepository commentRepository;
|
||||
private final PostRepository postRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final NotificationService notificationService;
|
||||
private final SubscriptionService subscriptionService;
|
||||
private final ReactionRepository reactionRepository;
|
||||
private final CommentSubscriptionRepository commentSubscriptionRepository;
|
||||
private final NotificationRepository notificationRepository;
|
||||
private final PointHistoryRepository pointHistoryRepository;
|
||||
private final PointService pointService;
|
||||
private final ImageUploader imageUploader;
|
||||
|
||||
@CacheEvict(
|
||||
value = CachingConfig.POST_CACHE_NAME,
|
||||
allEntries = true
|
||||
)
|
||||
@Transactional
|
||||
public Comment addComment(String username, Long postId, String content) {
|
||||
log.debug("addComment called by user {} for post {}", username, postId);
|
||||
long recent = commentRepository.countByAuthorAfter(username,
|
||||
java.time.LocalDateTime.now().minusMinutes(1));
|
||||
if (recent >= 3) {
|
||||
log.debug("Rate limit exceeded for user {}", username);
|
||||
throw new RateLimitException("Too many comments");
|
||||
}
|
||||
User author = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Post post = postRepository.findById(postId)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
if (post.isClosed()) {
|
||||
throw new IllegalStateException("Post closed");
|
||||
}
|
||||
Comment comment = new Comment();
|
||||
comment.setAuthor(author);
|
||||
comment.setPost(post);
|
||||
comment.setContent(content);
|
||||
comment = commentRepository.save(comment);
|
||||
log.debug("Comment {} saved for post {}", comment.getId(), postId);
|
||||
|
||||
// Update post comment statistics
|
||||
updatePostCommentStats(post);
|
||||
|
||||
imageUploader.addReferences(imageUploader.extractUrls(content));
|
||||
if (!author.getId().equals(post.getAuthor().getId())) {
|
||||
notificationService.createNotification(post.getAuthor(), NotificationType.COMMENT_REPLY, post, comment,
|
||||
null, null, null, null);
|
||||
}
|
||||
for (User u : subscriptionService.getPostSubscribers(postId)) {
|
||||
if (!u.getId().equals(author.getId())) {
|
||||
notificationService.createNotification(u, NotificationType.POST_UPDATED, post, comment, null, null,
|
||||
null, null);
|
||||
}
|
||||
}
|
||||
for (User u : subscriptionService.getSubscribers(author.getUsername())) {
|
||||
if (!u.getId().equals(author.getId())) {
|
||||
notificationService.createNotification(u, NotificationType.USER_ACTIVITY, post, comment, null, null,
|
||||
null, null);
|
||||
}
|
||||
}
|
||||
notificationService.notifyMentions(content, author, post, comment);
|
||||
log.debug("addComment finished for comment {}", comment.getId());
|
||||
return comment;
|
||||
private final CommentRepository commentRepository;
|
||||
private final PostRepository postRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final NotificationService notificationService;
|
||||
private final SubscriptionService subscriptionService;
|
||||
private final ReactionRepository reactionRepository;
|
||||
private final CommentSubscriptionRepository commentSubscriptionRepository;
|
||||
private final NotificationRepository notificationRepository;
|
||||
private final PointHistoryRepository pointHistoryRepository;
|
||||
private final PointService pointService;
|
||||
private final ImageUploader imageUploader;
|
||||
|
||||
@CacheEvict(value = CachingConfig.POST_CACHE_NAME, allEntries = true)
|
||||
@Transactional
|
||||
public Comment addComment(String username, Long postId, String content) {
|
||||
log.debug("addComment called by user {} for post {}", username, postId);
|
||||
long recent = commentRepository.countByAuthorAfter(
|
||||
username,
|
||||
java.time.LocalDateTime.now().minusMinutes(1)
|
||||
);
|
||||
if (recent >= 3) {
|
||||
log.debug("Rate limit exceeded for user {}", username);
|
||||
throw new RateLimitException("Too many comments");
|
||||
}
|
||||
User author = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Post post = postRepository
|
||||
.findById(postId)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
if (post.isClosed()) {
|
||||
throw new IllegalStateException("Post closed");
|
||||
}
|
||||
Comment comment = new Comment();
|
||||
comment.setAuthor(author);
|
||||
comment.setPost(post);
|
||||
comment.setContent(content);
|
||||
comment = commentRepository.save(comment);
|
||||
log.debug("Comment {} saved for post {}", comment.getId(), postId);
|
||||
|
||||
// Update post comment statistics
|
||||
updatePostCommentStats(post);
|
||||
|
||||
imageUploader.addReferences(imageUploader.extractUrls(content));
|
||||
if (!author.getId().equals(post.getAuthor().getId())) {
|
||||
notificationService.createNotification(
|
||||
post.getAuthor(),
|
||||
NotificationType.COMMENT_REPLY,
|
||||
post,
|
||||
comment,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
for (User u : subscriptionService.getPostSubscribers(postId)) {
|
||||
if (!u.getId().equals(author.getId())) {
|
||||
notificationService.createNotification(
|
||||
u,
|
||||
NotificationType.POST_UPDATED,
|
||||
post,
|
||||
comment,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
for (User u : subscriptionService.getSubscribers(author.getUsername())) {
|
||||
if (!u.getId().equals(author.getId())) {
|
||||
notificationService.createNotification(
|
||||
u,
|
||||
NotificationType.USER_ACTIVITY,
|
||||
post,
|
||||
comment,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
notificationService.notifyMentions(content, author, post, comment);
|
||||
log.debug("addComment finished for comment {}", comment.getId());
|
||||
return comment;
|
||||
}
|
||||
|
||||
public java.time.LocalDateTime getLastCommentTimeOfUserByUserId(Long userId) {
|
||||
// 根据用户id查询该用户最后回复时间
|
||||
return commentRepository.findLastCommentTimeOfUserByUserId(userId);
|
||||
}
|
||||
|
||||
@CacheEvict(value = CachingConfig.POST_CACHE_NAME, allEntries = true)
|
||||
@Transactional
|
||||
public Comment addReply(String username, Long parentId, String content) {
|
||||
log.debug("addReply called by user {} for parent comment {}", username, parentId);
|
||||
long recent = commentRepository.countByAuthorAfter(
|
||||
username,
|
||||
java.time.LocalDateTime.now().minusMinutes(1)
|
||||
);
|
||||
if (recent >= 3) {
|
||||
log.debug("Rate limit exceeded for user {}", username);
|
||||
throw new RateLimitException("Too many comments");
|
||||
}
|
||||
User author = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Comment parent = commentRepository
|
||||
.findById(parentId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Comment not found"));
|
||||
if (parent.getPost().isClosed()) {
|
||||
throw new IllegalStateException("Post closed");
|
||||
}
|
||||
Comment comment = new Comment();
|
||||
comment.setAuthor(author);
|
||||
comment.setPost(parent.getPost());
|
||||
comment.setParent(parent);
|
||||
comment.setContent(content);
|
||||
comment = commentRepository.save(comment);
|
||||
log.debug("Reply {} saved for parent {}", comment.getId(), parentId);
|
||||
|
||||
// Update post comment statistics
|
||||
updatePostCommentStats(parent.getPost());
|
||||
|
||||
imageUploader.addReferences(imageUploader.extractUrls(content));
|
||||
if (!author.getId().equals(parent.getAuthor().getId())) {
|
||||
notificationService.createNotification(
|
||||
parent.getAuthor(),
|
||||
NotificationType.COMMENT_REPLY,
|
||||
parent.getPost(),
|
||||
comment,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
for (User u : subscriptionService.getCommentSubscribers(parentId)) {
|
||||
if (!u.getId().equals(author.getId())) {
|
||||
notificationService.createNotification(
|
||||
u,
|
||||
NotificationType.COMMENT_REPLY,
|
||||
parent.getPost(),
|
||||
comment,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
for (User u : subscriptionService.getPostSubscribers(parent.getPost().getId())) {
|
||||
if (!u.getId().equals(author.getId())) {
|
||||
notificationService.createNotification(
|
||||
u,
|
||||
NotificationType.POST_UPDATED,
|
||||
parent.getPost(),
|
||||
comment,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
for (User u : subscriptionService.getSubscribers(author.getUsername())) {
|
||||
if (!u.getId().equals(author.getId())) {
|
||||
notificationService.createNotification(
|
||||
u,
|
||||
NotificationType.USER_ACTIVITY,
|
||||
parent.getPost(),
|
||||
comment,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
notificationService.notifyMentions(content, author, parent.getPost(), comment);
|
||||
log.debug("addReply finished for comment {}", comment.getId());
|
||||
return comment;
|
||||
}
|
||||
|
||||
public List<Comment> getCommentsForPost(Long postId, CommentSort sort) {
|
||||
log.debug("getCommentsForPost called for post {} with sort {}", postId, sort);
|
||||
Post post = postRepository
|
||||
.findById(postId)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
List<Comment> list = commentRepository.findByPostAndParentIsNullOrderByCreatedAtAsc(post);
|
||||
java.util.List<Comment> pinned = new java.util.ArrayList<>();
|
||||
java.util.List<Comment> others = new java.util.ArrayList<>();
|
||||
for (Comment c : list) {
|
||||
if (c.getPinnedAt() != null) {
|
||||
pinned.add(c);
|
||||
} else {
|
||||
others.add(c);
|
||||
}
|
||||
}
|
||||
pinned.sort(java.util.Comparator.comparing(Comment::getPinnedAt).reversed());
|
||||
if (sort == CommentSort.NEWEST) {
|
||||
others.sort(java.util.Comparator.comparing(Comment::getCreatedAt).reversed());
|
||||
} else if (sort == CommentSort.MOST_INTERACTIONS) {
|
||||
others.sort((a, b) -> Integer.compare(interactionCount(b), interactionCount(a)));
|
||||
}
|
||||
java.util.List<Comment> result = new java.util.ArrayList<>();
|
||||
result.addAll(pinned);
|
||||
result.addAll(others);
|
||||
log.debug("getCommentsForPost returning {} comments", result.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<Comment> getReplies(Long parentId) {
|
||||
log.debug("getReplies called for parent {}", parentId);
|
||||
Comment parent = commentRepository
|
||||
.findById(parentId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Comment not found"));
|
||||
List<Comment> replies = commentRepository.findByParentOrderByCreatedAtAsc(parent);
|
||||
log.debug("getReplies returning {} replies for parent {}", replies.size(), parentId);
|
||||
return replies;
|
||||
}
|
||||
|
||||
public List<Comment> getRecentCommentsByUser(String username, int limit) {
|
||||
log.debug("getRecentCommentsByUser called for user {} with limit {}", username, limit);
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Pageable pageable = PageRequest.of(0, limit);
|
||||
List<Comment> comments = commentRepository.findByAuthorOrderByCreatedAtDesc(user, pageable);
|
||||
log.debug(
|
||||
"getRecentCommentsByUser returning {} comments for user {}",
|
||||
comments.size(),
|
||||
username
|
||||
);
|
||||
return comments;
|
||||
}
|
||||
|
||||
public java.util.List<User> getParticipants(Long postId, int limit) {
|
||||
log.debug("getParticipants called for post {} with limit {}", postId, limit);
|
||||
Post post = postRepository
|
||||
.findById(postId)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
java.util.LinkedHashSet<User> set = new java.util.LinkedHashSet<>();
|
||||
set.add(post.getAuthor());
|
||||
set.addAll(commentRepository.findDistinctAuthorsByPost(post));
|
||||
java.util.List<User> list = new java.util.ArrayList<>(set);
|
||||
java.util.List<User> result = list.subList(0, Math.min(limit, list.size()));
|
||||
log.debug("getParticipants returning {} users for post {}", result.size(), postId);
|
||||
return result;
|
||||
}
|
||||
|
||||
public java.util.List<Comment> getCommentsByIds(java.util.List<Long> ids) {
|
||||
log.debug("getCommentsByIds called for ids {}", ids);
|
||||
java.util.List<Comment> comments = commentRepository.findAllById(ids);
|
||||
log.debug("getCommentsByIds returning {} comments", comments.size());
|
||||
return comments;
|
||||
}
|
||||
|
||||
public java.time.LocalDateTime getLastCommentTime(Long postId) {
|
||||
log.debug("getLastCommentTime called for post {}", postId);
|
||||
Post post = postRepository
|
||||
.findById(postId)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
java.time.LocalDateTime time = commentRepository.findLastCommentTime(post);
|
||||
log.debug("getLastCommentTime for post {} is {}", postId, time);
|
||||
return time;
|
||||
}
|
||||
|
||||
public long countComments(Long postId) {
|
||||
log.debug("countComments called for post {}", postId);
|
||||
long count = commentRepository.countByPostId(postId);
|
||||
log.debug("countComments for post {} is {}", postId, count);
|
||||
return count;
|
||||
}
|
||||
|
||||
@CacheEvict(value = CachingConfig.POST_CACHE_NAME, allEntries = true)
|
||||
@Transactional
|
||||
public void deleteComment(String username, Long id) {
|
||||
log.debug("deleteComment called by user {} for comment {}", username, id);
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Comment comment = commentRepository
|
||||
.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Comment not found"));
|
||||
if (!user.getId().equals(comment.getAuthor().getId()) && user.getRole() != Role.ADMIN) {
|
||||
log.debug("User {} not authorized to delete comment {}", username, id);
|
||||
throw new IllegalArgumentException("Unauthorized");
|
||||
}
|
||||
deleteCommentCascade(comment);
|
||||
log.debug("deleteComment completed for comment {}", id);
|
||||
}
|
||||
|
||||
@CacheEvict(value = CachingConfig.POST_CACHE_NAME, allEntries = true)
|
||||
@Transactional
|
||||
public void deleteCommentCascade(Comment comment) {
|
||||
log.debug("deleteCommentCascade called for comment {}", comment.getId());
|
||||
List<Comment> replies = commentRepository.findByParentOrderByCreatedAtAsc(comment);
|
||||
for (Comment c : replies) {
|
||||
deleteCommentCascade(c);
|
||||
}
|
||||
|
||||
public java.time.LocalDateTime getLastCommentTimeOfUserByUserId(Long userId) { // 根据用户id查询该用户最后回复时间
|
||||
return commentRepository.findLastCommentTimeOfUserByUserId(userId);
|
||||
// 逻辑删除相关的积分历史记录,并收集受影响的用户
|
||||
List<PointHistory> pointHistories = pointHistoryRepository.findByComment(comment);
|
||||
// 收集需要重新计算积分的用户
|
||||
Set<User> usersToRecalculate = pointHistories
|
||||
.stream()
|
||||
.map(PointHistory::getUser)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// 删除其他相关数据
|
||||
reactionRepository.findByComment(comment).forEach(reactionRepository::delete);
|
||||
commentSubscriptionRepository
|
||||
.findByComment(comment)
|
||||
.forEach(commentSubscriptionRepository::delete);
|
||||
notificationRepository.deleteAll(notificationRepository.findByComment(comment));
|
||||
imageUploader.removeReferences(imageUploader.extractUrls(comment.getContent()));
|
||||
|
||||
// 逻辑删除评论
|
||||
Post post = comment.getPost();
|
||||
commentRepository.delete(comment);
|
||||
// 删除积分历史
|
||||
pointHistoryRepository.deleteAll(pointHistories);
|
||||
|
||||
// Update post comment statistics
|
||||
updatePostCommentStats(post);
|
||||
|
||||
// 重新计算受影响用户的积分
|
||||
if (!usersToRecalculate.isEmpty()) {
|
||||
for (User user : usersToRecalculate) {
|
||||
int newPoints = pointService.recalculateUserPoints(user);
|
||||
user.setPoint(newPoints);
|
||||
log.debug("Recalculated points for user {}: {}", user.getUsername(), newPoints);
|
||||
}
|
||||
userRepository.saveAll(usersToRecalculate);
|
||||
}
|
||||
|
||||
@CacheEvict(
|
||||
value = CachingConfig.POST_CACHE_NAME,
|
||||
allEntries = true
|
||||
)
|
||||
@Transactional
|
||||
public Comment addReply(String username, Long parentId, String content) {
|
||||
log.debug("addReply called by user {} for parent comment {}", username, parentId);
|
||||
long recent = commentRepository.countByAuthorAfter(username,
|
||||
java.time.LocalDateTime.now().minusMinutes(1));
|
||||
if (recent >= 3) {
|
||||
log.debug("Rate limit exceeded for user {}", username);
|
||||
throw new RateLimitException("Too many comments");
|
||||
}
|
||||
User author = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Comment parent = commentRepository.findById(parentId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Comment not found"));
|
||||
if (parent.getPost().isClosed()) {
|
||||
throw new IllegalStateException("Post closed");
|
||||
}
|
||||
Comment comment = new Comment();
|
||||
comment.setAuthor(author);
|
||||
comment.setPost(parent.getPost());
|
||||
comment.setParent(parent);
|
||||
comment.setContent(content);
|
||||
comment = commentRepository.save(comment);
|
||||
log.debug("Reply {} saved for parent {}", comment.getId(), parentId);
|
||||
|
||||
// Update post comment statistics
|
||||
updatePostCommentStats(parent.getPost());
|
||||
|
||||
imageUploader.addReferences(imageUploader.extractUrls(content));
|
||||
if (!author.getId().equals(parent.getAuthor().getId())) {
|
||||
notificationService.createNotification(parent.getAuthor(), NotificationType.COMMENT_REPLY, parent.getPost(),
|
||||
comment, null, null, null, null);
|
||||
}
|
||||
for (User u : subscriptionService.getCommentSubscribers(parentId)) {
|
||||
if (!u.getId().equals(author.getId())) {
|
||||
notificationService.createNotification(u, NotificationType.COMMENT_REPLY, parent.getPost(), comment,
|
||||
null, null, null, null);
|
||||
}
|
||||
}
|
||||
for (User u : subscriptionService.getPostSubscribers(parent.getPost().getId())) {
|
||||
if (!u.getId().equals(author.getId())) {
|
||||
notificationService.createNotification(u, NotificationType.POST_UPDATED, parent.getPost(), comment,
|
||||
null, null, null, null);
|
||||
}
|
||||
}
|
||||
for (User u : subscriptionService.getSubscribers(author.getUsername())) {
|
||||
if (!u.getId().equals(author.getId())) {
|
||||
notificationService.createNotification(u, NotificationType.USER_ACTIVITY, parent.getPost(), comment,
|
||||
null, null, null, null);
|
||||
}
|
||||
}
|
||||
notificationService.notifyMentions(content, author, parent.getPost(), comment);
|
||||
log.debug("addReply finished for comment {}", comment.getId());
|
||||
return comment;
|
||||
log.debug("deleteCommentCascade removed comment {}", comment.getId());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Comment pinComment(String username, Long id) {
|
||||
Comment c = commentRepository
|
||||
.findById(id)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Comment not found"));
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
if (!user.getId().equals(c.getPost().getAuthor().getId()) && user.getRole() != Role.ADMIN) {
|
||||
throw new IllegalArgumentException("Unauthorized");
|
||||
}
|
||||
c.setPinnedAt(LocalDateTime.now());
|
||||
return commentRepository.save(c);
|
||||
}
|
||||
|
||||
public List<Comment> getCommentsForPost(Long postId, CommentSort sort) {
|
||||
log.debug("getCommentsForPost called for post {} with sort {}", postId, sort);
|
||||
Post post = postRepository.findById(postId)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
List<Comment> list = commentRepository.findByPostAndParentIsNullOrderByCreatedAtAsc(post);
|
||||
java.util.List<Comment> pinned = new java.util.ArrayList<>();
|
||||
java.util.List<Comment> others = new java.util.ArrayList<>();
|
||||
for (Comment c : list) {
|
||||
if (c.getPinnedAt() != null) {
|
||||
pinned.add(c);
|
||||
} else {
|
||||
others.add(c);
|
||||
}
|
||||
}
|
||||
pinned.sort(java.util.Comparator.comparing(Comment::getPinnedAt).reversed());
|
||||
if (sort == CommentSort.NEWEST) {
|
||||
others.sort(java.util.Comparator.comparing(Comment::getCreatedAt).reversed());
|
||||
} else if (sort == CommentSort.MOST_INTERACTIONS) {
|
||||
others.sort((a, b) -> Integer.compare(interactionCount(b), interactionCount(a)));
|
||||
}
|
||||
java.util.List<Comment> result = new java.util.ArrayList<>();
|
||||
result.addAll(pinned);
|
||||
result.addAll(others);
|
||||
log.debug("getCommentsForPost returning {} comments", result.size());
|
||||
return result;
|
||||
@Transactional
|
||||
public Comment unpinComment(String username, Long id) {
|
||||
Comment c = commentRepository
|
||||
.findById(id)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Comment not found"));
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
if (!user.getId().equals(c.getPost().getAuthor().getId()) && user.getRole() != Role.ADMIN) {
|
||||
throw new IllegalArgumentException("Unauthorized");
|
||||
}
|
||||
c.setPinnedAt(null);
|
||||
return commentRepository.save(c);
|
||||
}
|
||||
|
||||
public List<Comment> getReplies(Long parentId) {
|
||||
log.debug("getReplies called for parent {}", parentId);
|
||||
Comment parent = commentRepository.findById(parentId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Comment not found"));
|
||||
List<Comment> replies = commentRepository.findByParentOrderByCreatedAtAsc(parent);
|
||||
log.debug("getReplies returning {} replies for parent {}", replies.size(), parentId);
|
||||
return replies;
|
||||
private int interactionCount(Comment comment) {
|
||||
int reactions = reactionRepository.findByComment(comment).size();
|
||||
int replies = commentRepository.findByParentOrderByCreatedAtAsc(comment).size();
|
||||
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);
|
||||
|
||||
public List<Comment> getRecentCommentsByUser(String username, int limit) {
|
||||
log.debug("getRecentCommentsByUser called for user {} with limit {}", username, limit);
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Pageable pageable = PageRequest.of(0, limit);
|
||||
List<Comment> comments = commentRepository.findByAuthorOrderByCreatedAtDesc(user, pageable);
|
||||
log.debug("getRecentCommentsByUser returning {} comments for user {}", comments.size(), username);
|
||||
return comments;
|
||||
}
|
||||
|
||||
public java.util.List<User> getParticipants(Long postId, int limit) {
|
||||
log.debug("getParticipants called for post {} with limit {}", postId, limit);
|
||||
Post post = postRepository.findById(postId)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
java.util.LinkedHashSet<User> set = new java.util.LinkedHashSet<>();
|
||||
set.add(post.getAuthor());
|
||||
set.addAll(commentRepository.findDistinctAuthorsByPost(post));
|
||||
java.util.List<User> list = new java.util.ArrayList<>(set);
|
||||
java.util.List<User> result = list.subList(0, Math.min(limit, list.size()));
|
||||
log.debug("getParticipants returning {} users for post {}", result.size(), postId);
|
||||
return result;
|
||||
}
|
||||
|
||||
public java.util.List<Comment> getCommentsByIds(java.util.List<Long> ids) {
|
||||
log.debug("getCommentsByIds called for ids {}", ids);
|
||||
java.util.List<Comment> comments = commentRepository.findAllById(ids);
|
||||
log.debug("getCommentsByIds returning {} comments", comments.size());
|
||||
return comments;
|
||||
}
|
||||
|
||||
public java.time.LocalDateTime getLastCommentTime(Long postId) {
|
||||
log.debug("getLastCommentTime called for post {}", postId);
|
||||
Post post = postRepository.findById(postId)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
java.time.LocalDateTime time = commentRepository.findLastCommentTime(post);
|
||||
log.debug("getLastCommentTime for post {} is {}", postId, time);
|
||||
return time;
|
||||
}
|
||||
|
||||
public long countComments(Long postId) {
|
||||
log.debug("countComments called for post {}", postId);
|
||||
long count = commentRepository.countByPostId(postId);
|
||||
log.debug("countComments for post {} is {}", postId, count);
|
||||
return count;
|
||||
}
|
||||
|
||||
@CacheEvict(
|
||||
value = CachingConfig.POST_CACHE_NAME,
|
||||
allEntries = true
|
||||
)
|
||||
@Transactional
|
||||
public void deleteComment(String username, Long id) {
|
||||
log.debug("deleteComment called by user {} for comment {}", username, id);
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Comment comment = commentRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Comment not found"));
|
||||
if (!user.getId().equals(comment.getAuthor().getId()) && user.getRole() != Role.ADMIN) {
|
||||
log.debug("User {} not authorized to delete comment {}", username, id);
|
||||
throw new IllegalArgumentException("Unauthorized");
|
||||
}
|
||||
deleteCommentCascade(comment);
|
||||
log.debug("deleteComment completed for comment {}", id);
|
||||
}
|
||||
|
||||
@CacheEvict(
|
||||
value = CachingConfig.POST_CACHE_NAME,
|
||||
allEntries = true
|
||||
)
|
||||
@Transactional
|
||||
public void deleteCommentCascade(Comment comment) {
|
||||
log.debug("deleteCommentCascade called for comment {}", comment.getId());
|
||||
List<Comment> replies = commentRepository.findByParentOrderByCreatedAtAsc(comment);
|
||||
for (Comment c : replies) {
|
||||
deleteCommentCascade(c);
|
||||
}
|
||||
|
||||
// 逻辑删除相关的积分历史记录,并收集受影响的用户
|
||||
List<PointHistory> pointHistories = pointHistoryRepository.findByComment(comment);
|
||||
// 收集需要重新计算积分的用户
|
||||
Set<User> usersToRecalculate = pointHistories.stream().map(PointHistory::getUser).collect(Collectors.toSet());
|
||||
|
||||
// 删除其他相关数据
|
||||
reactionRepository.findByComment(comment).forEach(reactionRepository::delete);
|
||||
commentSubscriptionRepository.findByComment(comment).forEach(commentSubscriptionRepository::delete);
|
||||
notificationRepository.deleteAll(notificationRepository.findByComment(comment));
|
||||
imageUploader.removeReferences(imageUploader.extractUrls(comment.getContent()));
|
||||
|
||||
// 逻辑删除评论
|
||||
Post post = comment.getPost();
|
||||
commentRepository.delete(comment);
|
||||
// 删除积分历史
|
||||
pointHistoryRepository.deleteAll(pointHistories);
|
||||
|
||||
// Update post comment statistics
|
||||
updatePostCommentStats(post);
|
||||
|
||||
// 重新计算受影响用户的积分
|
||||
if (!usersToRecalculate.isEmpty()) {
|
||||
for (User user : usersToRecalculate) {
|
||||
int newPoints = pointService.recalculateUserPoints(user);
|
||||
user.setPoint(newPoints);
|
||||
log.debug("Recalculated points for user {}: {}", user.getUsername(), newPoints);
|
||||
}
|
||||
userRepository.saveAll(usersToRecalculate);
|
||||
}
|
||||
|
||||
log.debug("deleteCommentCascade removed comment {}", comment.getId());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Comment pinComment(String username, Long id) {
|
||||
Comment c = commentRepository.findById(id)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Comment not found"));
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
if (!user.getId().equals(c.getPost().getAuthor().getId()) && user.getRole() != Role.ADMIN) {
|
||||
throw new IllegalArgumentException("Unauthorized");
|
||||
}
|
||||
c.setPinnedAt(LocalDateTime.now());
|
||||
return commentRepository.save(c);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Comment unpinComment(String username, Long id) {
|
||||
Comment c = commentRepository.findById(id)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Comment not found"));
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
if (!user.getId().equals(c.getPost().getAuthor().getId()) && user.getRole() != Role.ADMIN) {
|
||||
throw new IllegalArgumentException("Unauthorized");
|
||||
}
|
||||
c.setPinnedAt(null);
|
||||
return commentRepository.save(c);
|
||||
}
|
||||
|
||||
private int interactionCount(Comment comment) {
|
||||
int reactions = reactionRepository.findByComment(comment).size();
|
||||
int replies = commentRepository.findByParentOrderByCreatedAtAsc(comment).size();
|
||||
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);
|
||||
}
|
||||
log.debug(
|
||||
"Updated post {} stats: commentCount={}, lastReplyAt={}",
|
||||
post.getId(),
|
||||
commentCount,
|
||||
lastReplyAt
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.openisle.service;
|
||||
import com.openisle.model.ContributorConfig;
|
||||
import com.openisle.repository.ContributorConfigRepository;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -10,80 +12,82 @@ import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ContributorService {
|
||||
private static final String OWNER = "nagisa77";
|
||||
private static final String REPO = "OpenIsle";
|
||||
|
||||
private final ContributorConfigRepository repository;
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
private static final String OWNER = "nagisa77";
|
||||
private static final String REPO = "OpenIsle";
|
||||
|
||||
@PostConstruct
|
||||
@Scheduled(cron = "0 0 * * * *")
|
||||
public void updateContributions() {
|
||||
for (ContributorConfig config : repository.findAll()) {
|
||||
long lines = fetchContributionLines(config.getGithubId());
|
||||
if (lines != -1) {
|
||||
config.setContributionLines(lines);
|
||||
repository.save(config);
|
||||
}
|
||||
}
|
||||
private final ContributorConfigRepository repository;
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
@PostConstruct
|
||||
@Scheduled(cron = "0 0 * * * *")
|
||||
public void updateContributions() {
|
||||
for (ContributorConfig config : repository.findAll()) {
|
||||
long lines = fetchContributionLines(config.getGithubId());
|
||||
if (lines != -1) {
|
||||
config.setContributionLines(lines);
|
||||
repository.save(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long fetchContributionLines(String githubId) {
|
||||
try {
|
||||
String url = String.format("https://api.github.com/repos/%s/%s/stats/contributors", OWNER, REPO);
|
||||
ResponseEntity<?> response = restTemplate.getForEntity(url, Object.class);
|
||||
private long fetchContributionLines(String githubId) {
|
||||
try {
|
||||
String url = String.format(
|
||||
"https://api.github.com/repos/%s/%s/stats/contributors",
|
||||
OWNER,
|
||||
REPO
|
||||
);
|
||||
ResponseEntity<?> response = restTemplate.getForEntity(url, Object.class);
|
||||
|
||||
// 检查是否为202,GitHub有时会返回202表示正在生成统计数据
|
||||
if (response.getStatusCodeValue() == 202) {
|
||||
log.warn("GitHub API 返回202,统计数据正在生成中,githubId: {}", githubId);
|
||||
return -1;
|
||||
}
|
||||
// 检查是否为202,GitHub有时会返回202表示正在生成统计数据
|
||||
if (response.getStatusCodeValue() == 202) {
|
||||
log.warn("GitHub API 返回202,统计数据正在生成中,githubId: {}", githubId);
|
||||
return -1;
|
||||
}
|
||||
|
||||
Object body = response.getBody();
|
||||
if (!(body instanceof List)) {
|
||||
// 不是List类型,直接返回0
|
||||
return 0;
|
||||
}
|
||||
List<?> listBody = (List<?>) body;
|
||||
for (Object itemObj : listBody) {
|
||||
if (!(itemObj instanceof Map)) continue;
|
||||
Map<String, Object> item = (Map<String, Object>) itemObj;
|
||||
Map<String, Object> author = (Map<String, Object>) item.get("author");
|
||||
if (author != null && githubId.equals(author.get("login"))) {
|
||||
List<Map<String, Object>> weeks = (List<Map<String, Object>>) item.get("weeks");
|
||||
long total = 0;
|
||||
if (weeks != null) {
|
||||
for (Map<String, Object> week : weeks) {
|
||||
Number a = (Number) week.get("a");
|
||||
Number d = (Number) week.get("d");
|
||||
if (a != null) {
|
||||
total += a.longValue();
|
||||
}
|
||||
if (d != null) {
|
||||
total += d.longValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(e.getMessage());
|
||||
}
|
||||
Object body = response.getBody();
|
||||
if (!(body instanceof List)) {
|
||||
// 不是List类型,直接返回0
|
||||
return 0;
|
||||
}
|
||||
List<?> listBody = (List<?>) body;
|
||||
for (Object itemObj : listBody) {
|
||||
if (!(itemObj instanceof Map)) continue;
|
||||
Map<String, Object> item = (Map<String, Object>) itemObj;
|
||||
Map<String, Object> author = (Map<String, Object>) item.get("author");
|
||||
if (author != null && githubId.equals(author.get("login"))) {
|
||||
List<Map<String, Object>> weeks = (List<Map<String, Object>>) item.get("weeks");
|
||||
long total = 0;
|
||||
if (weeks != null) {
|
||||
for (Map<String, Object> week : weeks) {
|
||||
Number a = (Number) week.get("a");
|
||||
Number d = (Number) week.get("d");
|
||||
if (a != null) {
|
||||
total += a.longValue();
|
||||
}
|
||||
if (d != null) {
|
||||
total += d.longValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(e.getMessage());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public long getContributionLines(String userIname) {
|
||||
return repository.findByUserIname(userIname)
|
||||
.map(ContributorConfig::getContributionLines)
|
||||
.orElse(0L);
|
||||
}
|
||||
public long getContributionLines(String userIname) {
|
||||
return repository
|
||||
.findByUserIname(userIname)
|
||||
.map(ContributorConfig::getContributionLines)
|
||||
.orElse(0L);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,22 +4,21 @@ import com.qcloud.cos.COSClient;
|
||||
import com.qcloud.cos.ClientConfig;
|
||||
import com.qcloud.cos.auth.BasicCOSCredentials;
|
||||
import com.qcloud.cos.auth.COSCredentials;
|
||||
import com.qcloud.cos.model.ObjectMetadata;
|
||||
import com.qcloud.cos.model.PutObjectRequest;
|
||||
import com.qcloud.cos.http.HttpMethodName;
|
||||
import com.qcloud.cos.model.GeneratePresignedUrlRequest;
|
||||
import com.qcloud.cos.model.ObjectMetadata;
|
||||
import com.qcloud.cos.model.PutObjectRequest;
|
||||
import com.qcloud.cos.region.Region;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* ImageUploader implementation using Tencent Cloud COS.
|
||||
@@ -27,98 +26,107 @@ import java.util.concurrent.Executors;
|
||||
@Service
|
||||
public class CosImageUploader extends ImageUploader {
|
||||
|
||||
private final COSClient cosClient;
|
||||
private final String bucketName;
|
||||
private final String baseUrl;
|
||||
private static final String UPLOAD_DIR = "dynamic_assert/";
|
||||
private static final Logger logger = LoggerFactory.getLogger(CosImageUploader.class);
|
||||
private final ExecutorService executor = Executors.newFixedThreadPool(2,
|
||||
new CustomizableThreadFactory("cos-upload-"));
|
||||
private final COSClient cosClient;
|
||||
private final String bucketName;
|
||||
private final String baseUrl;
|
||||
private static final String UPLOAD_DIR = "dynamic_assert/";
|
||||
private static final Logger logger = LoggerFactory.getLogger(CosImageUploader.class);
|
||||
private final ExecutorService executor = Executors.newFixedThreadPool(
|
||||
2,
|
||||
new CustomizableThreadFactory("cos-upload-")
|
||||
);
|
||||
|
||||
@org.springframework.beans.factory.annotation.Autowired
|
||||
public CosImageUploader(
|
||||
com.openisle.repository.ImageRepository imageRepository,
|
||||
@Value("${cos.secret-id:}") String secretId,
|
||||
@Value("${cos.secret-key:}") String secretKey,
|
||||
@Value("${cos.region:ap-guangzhou}") String region,
|
||||
@Value("${cos.bucket-name:}") String bucketName,
|
||||
@Value("${cos.base-url:https://example.com}") String baseUrl) {
|
||||
super(imageRepository, baseUrl);
|
||||
COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
|
||||
ClientConfig config = new ClientConfig(new Region(region));
|
||||
this.cosClient = new COSClient(cred, config);
|
||||
this.bucketName = bucketName;
|
||||
this.baseUrl = baseUrl;
|
||||
logger.debug("COS client initialized for region {} with bucket {}", region, bucketName);
|
||||
}
|
||||
@org.springframework.beans.factory.annotation.Autowired
|
||||
public CosImageUploader(
|
||||
com.openisle.repository.ImageRepository imageRepository,
|
||||
@Value("${cos.secret-id:}") String secretId,
|
||||
@Value("${cos.secret-key:}") String secretKey,
|
||||
@Value("${cos.region:ap-guangzhou}") String region,
|
||||
@Value("${cos.bucket-name:}") String bucketName,
|
||||
@Value("${cos.base-url:https://example.com}") String baseUrl
|
||||
) {
|
||||
super(imageRepository, baseUrl);
|
||||
COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
|
||||
ClientConfig config = new ClientConfig(new Region(region));
|
||||
this.cosClient = new COSClient(cred, config);
|
||||
this.bucketName = bucketName;
|
||||
this.baseUrl = baseUrl;
|
||||
logger.debug("COS client initialized for region {} with bucket {}", region, bucketName);
|
||||
}
|
||||
|
||||
// for tests
|
||||
CosImageUploader(COSClient cosClient,
|
||||
com.openisle.repository.ImageRepository imageRepository,
|
||||
String bucketName,
|
||||
String baseUrl) {
|
||||
super(imageRepository, baseUrl);
|
||||
this.cosClient = cosClient;
|
||||
this.bucketName = bucketName;
|
||||
this.baseUrl = baseUrl;
|
||||
logger.debug("COS client provided directly with bucket {}", bucketName);
|
||||
}
|
||||
// for tests
|
||||
CosImageUploader(
|
||||
COSClient cosClient,
|
||||
com.openisle.repository.ImageRepository imageRepository,
|
||||
String bucketName,
|
||||
String baseUrl
|
||||
) {
|
||||
super(imageRepository, baseUrl);
|
||||
this.cosClient = cosClient;
|
||||
this.bucketName = bucketName;
|
||||
this.baseUrl = baseUrl;
|
||||
logger.debug("COS client provided directly with bucket {}", bucketName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CompletableFuture<String> doUpload(byte[] data, String filename) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
logger.debug("Uploading {} bytes as {}", data.length, filename);
|
||||
String ext = "";
|
||||
int dot = filename.lastIndexOf('.');
|
||||
if (dot != -1) {
|
||||
ext = filename.substring(dot);
|
||||
}
|
||||
String randomName = UUID.randomUUID().toString().replace("-", "") + ext;
|
||||
String objectKey = UPLOAD_DIR + randomName;
|
||||
logger.debug("Generated object key {}", objectKey);
|
||||
|
||||
ObjectMetadata meta = new ObjectMetadata();
|
||||
meta.setContentLength(data.length);
|
||||
PutObjectRequest req = new PutObjectRequest(
|
||||
bucketName,
|
||||
objectKey,
|
||||
new ByteArrayInputStream(data),
|
||||
meta);
|
||||
logger.debug("Sending PutObject request to bucket {}", bucketName);
|
||||
cosClient.putObject(req);
|
||||
String url = baseUrl + "/" + objectKey;
|
||||
logger.debug("Upload successful, accessible at {}", url);
|
||||
return url;
|
||||
}, executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteFromStore(String key) {
|
||||
try {
|
||||
cosClient.deleteObject(bucketName, key);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to delete image {} from COS", key, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.Map<String, String> presignUpload(String filename) {
|
||||
@Override
|
||||
protected CompletableFuture<String> doUpload(byte[] data, String filename) {
|
||||
return CompletableFuture.supplyAsync(
|
||||
() -> {
|
||||
logger.debug("Uploading {} bytes as {}", data.length, filename);
|
||||
String ext = "";
|
||||
int dot = filename.lastIndexOf('.');
|
||||
if (dot != -1) {
|
||||
ext = filename.substring(dot);
|
||||
ext = filename.substring(dot);
|
||||
}
|
||||
String randomName = java.util.UUID.randomUUID().toString().replace("-", "") + ext;
|
||||
String randomName = UUID.randomUUID().toString().replace("-", "") + ext;
|
||||
String objectKey = UPLOAD_DIR + randomName;
|
||||
java.util.Date expiration = new java.util.Date(System.currentTimeMillis() + 15 * 60 * 1000L);
|
||||
GeneratePresignedUrlRequest req = new GeneratePresignedUrlRequest(bucketName, objectKey, HttpMethodName.PUT);
|
||||
req.setExpiration(expiration);
|
||||
java.net.URL url = cosClient.generatePresignedUrl(req);
|
||||
String fileUrl = baseUrl + "/" + objectKey;
|
||||
return java.util.Map.of(
|
||||
"uploadUrl", url.toString(),
|
||||
"fileUrl", fileUrl,
|
||||
"key", objectKey
|
||||
logger.debug("Generated object key {}", objectKey);
|
||||
|
||||
ObjectMetadata meta = new ObjectMetadata();
|
||||
meta.setContentLength(data.length);
|
||||
PutObjectRequest req = new PutObjectRequest(
|
||||
bucketName,
|
||||
objectKey,
|
||||
new ByteArrayInputStream(data),
|
||||
meta
|
||||
);
|
||||
logger.debug("Sending PutObject request to bucket {}", bucketName);
|
||||
cosClient.putObject(req);
|
||||
String url = baseUrl + "/" + objectKey;
|
||||
logger.debug("Upload successful, accessible at {}", url);
|
||||
return url;
|
||||
},
|
||||
executor
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteFromStore(String key) {
|
||||
try {
|
||||
cosClient.deleteObject(bucketName, key);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to delete image {} from COS", key, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.Map<String, String> presignUpload(String filename) {
|
||||
String ext = "";
|
||||
int dot = filename.lastIndexOf('.');
|
||||
if (dot != -1) {
|
||||
ext = filename.substring(dot);
|
||||
}
|
||||
String randomName = java.util.UUID.randomUUID().toString().replace("-", "") + ext;
|
||||
String objectKey = UPLOAD_DIR + randomName;
|
||||
java.util.Date expiration = new java.util.Date(System.currentTimeMillis() + 15 * 60 * 1000L);
|
||||
GeneratePresignedUrlRequest req = new GeneratePresignedUrlRequest(
|
||||
bucketName,
|
||||
objectKey,
|
||||
HttpMethodName.PUT
|
||||
);
|
||||
req.setExpiration(expiration);
|
||||
java.net.URL url = cosClient.generatePresignedUrl(req);
|
||||
String fileUrl = baseUrl + "/" + objectKey;
|
||||
return java.util.Map.of("uploadUrl", url.toString(), "fileUrl", fileUrl, "key", objectKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.openisle.model.Role;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.*;
|
||||
@@ -12,96 +13,123 @@ import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DiscordAuthService {
|
||||
private final UserRepository userRepository;
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
@Value("${discord.client-id:}")
|
||||
private String clientId;
|
||||
private final UserRepository userRepository;
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
@Value("${discord.client-secret:}")
|
||||
private String clientSecret;
|
||||
@Value("${discord.client-id:}")
|
||||
private String clientId;
|
||||
|
||||
public Optional<AuthResult> authenticate(String code, com.openisle.model.RegisterMode mode, String redirectUri, boolean viaInvite) {
|
||||
try {
|
||||
String tokenUrl = "https://discord.com/api/oauth2/token";
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
@Value("${discord.client-secret:}")
|
||||
private String clientSecret;
|
||||
|
||||
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
|
||||
body.add("client_id", clientId);
|
||||
body.add("client_secret", clientSecret);
|
||||
body.add("grant_type", "authorization_code");
|
||||
body.add("code", code);
|
||||
if (redirectUri != null) {
|
||||
body.add("redirect_uri", redirectUri);
|
||||
}
|
||||
body.add("scope", "identify email");
|
||||
public Optional<AuthResult> authenticate(
|
||||
String code,
|
||||
com.openisle.model.RegisterMode mode,
|
||||
String redirectUri,
|
||||
boolean viaInvite
|
||||
) {
|
||||
try {
|
||||
String tokenUrl = "https://discord.com/api/oauth2/token";
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
|
||||
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
|
||||
ResponseEntity<JsonNode> tokenRes = restTemplate.postForEntity(tokenUrl, request, JsonNode.class);
|
||||
if (!tokenRes.getStatusCode().is2xxSuccessful() || tokenRes.getBody() == null || !tokenRes.getBody().has("access_token")) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String accessToken = tokenRes.getBody().get("access_token").asText();
|
||||
HttpHeaders authHeaders = new HttpHeaders();
|
||||
authHeaders.setBearerAuth(accessToken);
|
||||
HttpEntity<Void> entity = new HttpEntity<>(authHeaders);
|
||||
ResponseEntity<JsonNode> userRes = restTemplate.exchange(
|
||||
"https://discord.com/api/users/@me", HttpMethod.GET, entity, JsonNode.class);
|
||||
if (!userRes.getStatusCode().is2xxSuccessful() || userRes.getBody() == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
JsonNode userNode = userRes.getBody();
|
||||
String email = userNode.hasNonNull("email") ? userNode.get("email").asText() : null;
|
||||
String username = userNode.hasNonNull("username") ? userNode.get("username").asText() : null;
|
||||
String id = userNode.hasNonNull("id") ? userNode.get("id").asText() : null;
|
||||
String avatar = null;
|
||||
if (userNode.hasNonNull("avatar") && id != null) {
|
||||
avatar = "https://cdn.discordapp.com/avatars/" + id + "/" + userNode.get("avatar").asText() + ".png";
|
||||
}
|
||||
if (email == null) {
|
||||
email = (username != null ? username : id) + "@users.noreply.discord.com";
|
||||
}
|
||||
return Optional.of(processUser(email, username, avatar, mode, viaInvite));
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
|
||||
body.add("client_id", clientId);
|
||||
body.add("client_secret", clientSecret);
|
||||
body.add("grant_type", "authorization_code");
|
||||
body.add("code", code);
|
||||
if (redirectUri != null) {
|
||||
body.add("redirect_uri", redirectUri);
|
||||
}
|
||||
body.add("scope", "identify email");
|
||||
|
||||
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
|
||||
ResponseEntity<JsonNode> tokenRes = restTemplate.postForEntity(
|
||||
tokenUrl,
|
||||
request,
|
||||
JsonNode.class
|
||||
);
|
||||
if (
|
||||
!tokenRes.getStatusCode().is2xxSuccessful() ||
|
||||
tokenRes.getBody() == null ||
|
||||
!tokenRes.getBody().has("access_token")
|
||||
) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String accessToken = tokenRes.getBody().get("access_token").asText();
|
||||
HttpHeaders authHeaders = new HttpHeaders();
|
||||
authHeaders.setBearerAuth(accessToken);
|
||||
HttpEntity<Void> entity = new HttpEntity<>(authHeaders);
|
||||
ResponseEntity<JsonNode> userRes = restTemplate.exchange(
|
||||
"https://discord.com/api/users/@me",
|
||||
HttpMethod.GET,
|
||||
entity,
|
||||
JsonNode.class
|
||||
);
|
||||
if (!userRes.getStatusCode().is2xxSuccessful() || userRes.getBody() == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
JsonNode userNode = userRes.getBody();
|
||||
String email = userNode.hasNonNull("email") ? userNode.get("email").asText() : null;
|
||||
String username = userNode.hasNonNull("username") ? userNode.get("username").asText() : null;
|
||||
String id = userNode.hasNonNull("id") ? userNode.get("id").asText() : null;
|
||||
String avatar = null;
|
||||
if (userNode.hasNonNull("avatar") && id != null) {
|
||||
avatar =
|
||||
"https://cdn.discordapp.com/avatars/" +
|
||||
id +
|
||||
"/" +
|
||||
userNode.get("avatar").asText() +
|
||||
".png";
|
||||
}
|
||||
if (email == null) {
|
||||
email = (username != null ? username : id) + "@users.noreply.discord.com";
|
||||
}
|
||||
return Optional.of(processUser(email, username, avatar, mode, viaInvite));
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private AuthResult processUser(String email, String username, String avatar, com.openisle.model.RegisterMode mode, boolean viaInvite) {
|
||||
Optional<User> existing = userRepository.findByEmail(email);
|
||||
if (existing.isPresent()) {
|
||||
User user = existing.get();
|
||||
if (!user.isVerified()) {
|
||||
user.setVerified(true);
|
||||
user.setVerificationCode(null);
|
||||
userRepository.save(user);
|
||||
}
|
||||
return new AuthResult(user, false);
|
||||
}
|
||||
String baseUsername = username != null ? username : email.split("@")[0];
|
||||
String finalUsername = baseUsername;
|
||||
int suffix = 1;
|
||||
while (userRepository.findByUsername(finalUsername).isPresent()) {
|
||||
finalUsername = baseUsername + suffix++;
|
||||
}
|
||||
User user = new User();
|
||||
user.setUsername(finalUsername);
|
||||
user.setEmail(email);
|
||||
user.setPassword("");
|
||||
user.setRole(Role.USER);
|
||||
private AuthResult processUser(
|
||||
String email,
|
||||
String username,
|
||||
String avatar,
|
||||
com.openisle.model.RegisterMode mode,
|
||||
boolean viaInvite
|
||||
) {
|
||||
Optional<User> existing = userRepository.findByEmail(email);
|
||||
if (existing.isPresent()) {
|
||||
User user = existing.get();
|
||||
if (!user.isVerified()) {
|
||||
user.setVerified(true);
|
||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT || viaInvite);
|
||||
if (avatar != null) {
|
||||
user.setAvatar(avatar);
|
||||
} else {
|
||||
user.setAvatar("https://cdn.discordapp.com/embed/avatars/0.png");
|
||||
}
|
||||
return new AuthResult(userRepository.save(user), true);
|
||||
user.setVerificationCode(null);
|
||||
userRepository.save(user);
|
||||
}
|
||||
return new AuthResult(user, false);
|
||||
}
|
||||
String baseUsername = username != null ? username : email.split("@")[0];
|
||||
String finalUsername = baseUsername;
|
||||
int suffix = 1;
|
||||
while (userRepository.findByUsername(finalUsername).isPresent()) {
|
||||
finalUsername = baseUsername + suffix++;
|
||||
}
|
||||
User user = new User();
|
||||
user.setUsername(finalUsername);
|
||||
user.setEmail(email);
|
||||
user.setPassword("");
|
||||
user.setRole(Role.USER);
|
||||
user.setVerified(true);
|
||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT || viaInvite);
|
||||
if (avatar != null) {
|
||||
user.setAvatar(avatar);
|
||||
} else {
|
||||
user.setAvatar("https://cdn.discordapp.com/embed/avatars/0.png");
|
||||
}
|
||||
return new AuthResult(userRepository.save(user), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,68 +9,79 @@ import com.openisle.repository.DraftRepository;
|
||||
import com.openisle.repository.TagRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import com.openisle.service.ImageUploader;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DraftService {
|
||||
private final DraftRepository draftRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final CategoryRepository categoryRepository;
|
||||
private final TagRepository tagRepository;
|
||||
private final ImageUploader imageUploader;
|
||||
|
||||
@Transactional
|
||||
public Draft saveDraft(String username, Long categoryId, String title, String content, List<Long> tagIds) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Draft draft = draftRepository.findByAuthor(user).orElse(new Draft());
|
||||
String oldContent = draft.getContent();
|
||||
boolean existing = draft.getId() != null;
|
||||
draft.setAuthor(user);
|
||||
draft.setTitle(title);
|
||||
draft.setContent(content);
|
||||
if (categoryId != null) {
|
||||
Category category = categoryRepository.findById(categoryId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Category not found"));
|
||||
draft.setCategory(category);
|
||||
} else {
|
||||
draft.setCategory(null);
|
||||
}
|
||||
Set<Tag> tags = new HashSet<>();
|
||||
if (tagIds != null && !tagIds.isEmpty()) {
|
||||
tags.addAll(tagRepository.findAllById(tagIds));
|
||||
}
|
||||
draft.setTags(tags);
|
||||
Draft saved = draftRepository.save(draft);
|
||||
if (existing) {
|
||||
imageUploader.adjustReferences(oldContent == null ? "" : oldContent, content);
|
||||
} else {
|
||||
imageUploader.addReferences(imageUploader.extractUrls(content));
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
private final DraftRepository draftRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final CategoryRepository categoryRepository;
|
||||
private final TagRepository tagRepository;
|
||||
private final ImageUploader imageUploader;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Draft> getDraft(String username) {
|
||||
return userRepository.findByUsername(username)
|
||||
.flatMap(draftRepository::findByAuthor);
|
||||
@Transactional
|
||||
public Draft saveDraft(
|
||||
String username,
|
||||
Long categoryId,
|
||||
String title,
|
||||
String content,
|
||||
List<Long> tagIds
|
||||
) {
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Draft draft = draftRepository.findByAuthor(user).orElse(new Draft());
|
||||
String oldContent = draft.getContent();
|
||||
boolean existing = draft.getId() != null;
|
||||
draft.setAuthor(user);
|
||||
draft.setTitle(title);
|
||||
draft.setContent(content);
|
||||
if (categoryId != null) {
|
||||
Category category = categoryRepository
|
||||
.findById(categoryId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Category not found"));
|
||||
draft.setCategory(category);
|
||||
} else {
|
||||
draft.setCategory(null);
|
||||
}
|
||||
Set<Tag> tags = new HashSet<>();
|
||||
if (tagIds != null && !tagIds.isEmpty()) {
|
||||
tags.addAll(tagRepository.findAllById(tagIds));
|
||||
}
|
||||
draft.setTags(tags);
|
||||
Draft saved = draftRepository.save(draft);
|
||||
if (existing) {
|
||||
imageUploader.adjustReferences(oldContent == null ? "" : oldContent, content);
|
||||
} else {
|
||||
imageUploader.addReferences(imageUploader.extractUrls(content));
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteDraft(String username) {
|
||||
userRepository.findByUsername(username).ifPresent(user ->
|
||||
draftRepository.findByAuthor(user).ifPresent(draft -> {
|
||||
imageUploader.removeReferences(imageUploader.extractUrls(draft.getContent()));
|
||||
draftRepository.delete(draft);
|
||||
})
|
||||
);
|
||||
}
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Draft> getDraft(String username) {
|
||||
return userRepository.findByUsername(username).flatMap(draftRepository::findByAuthor);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteDraft(String username) {
|
||||
userRepository
|
||||
.findByUsername(username)
|
||||
.ifPresent(user ->
|
||||
draftRepository
|
||||
.findByAuthor(user)
|
||||
.ifPresent(draft -> {
|
||||
imageUploader.removeReferences(imageUploader.extractUrls(draft.getContent()));
|
||||
draftRepository.delete(draft);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@ package com.openisle.service;
|
||||
* Abstract email sender used to deliver emails.
|
||||
*/
|
||||
public abstract class EmailSender {
|
||||
/**
|
||||
* Send an email to a recipient.
|
||||
* @param to recipient email address
|
||||
* @param subject email subject
|
||||
* @param text email body
|
||||
*/
|
||||
public abstract void sendEmail(String to, String subject, String text);
|
||||
|
||||
/**
|
||||
* Send an email to a recipient.
|
||||
* @param to recipient email address
|
||||
* @param subject email subject
|
||||
* @param text email body
|
||||
*/
|
||||
public abstract void sendEmail(String to, String subject, String text);
|
||||
}
|
||||
|
||||
@@ -4,123 +4,156 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.openisle.model.Role;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import com.openisle.service.AvatarGenerator;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import com.openisle.service.AvatarGenerator;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.Optional;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GithubAuthService {
|
||||
private final UserRepository userRepository;
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
private final AvatarGenerator avatarGenerator;
|
||||
|
||||
@Value("${github.client-id:}")
|
||||
private String clientId;
|
||||
private final UserRepository userRepository;
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
private final AvatarGenerator avatarGenerator;
|
||||
|
||||
@Value("${github.client-secret:}")
|
||||
private String clientSecret;
|
||||
@Value("${github.client-id:}")
|
||||
private String clientId;
|
||||
|
||||
public Optional<AuthResult> authenticate(String code, com.openisle.model.RegisterMode mode, String redirectUri, boolean viaInvite) {
|
||||
@Value("${github.client-secret:}")
|
||||
private String clientSecret;
|
||||
|
||||
public Optional<AuthResult> authenticate(
|
||||
String code,
|
||||
com.openisle.model.RegisterMode mode,
|
||||
String redirectUri,
|
||||
boolean viaInvite
|
||||
) {
|
||||
try {
|
||||
String tokenUrl = "https://github.com/login/oauth/access_token";
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
Map<String, String> body = new HashMap<>();
|
||||
body.put("client_id", clientId);
|
||||
body.put("client_secret", clientSecret);
|
||||
body.put("code", code);
|
||||
if (redirectUri != null) {
|
||||
body.put("redirect_uri", redirectUri);
|
||||
}
|
||||
|
||||
HttpEntity<Map<String, String>> request = new HttpEntity<>(body, headers);
|
||||
ResponseEntity<JsonNode> tokenRes = restTemplate.postForEntity(
|
||||
tokenUrl,
|
||||
request,
|
||||
JsonNode.class
|
||||
);
|
||||
if (
|
||||
!tokenRes.getStatusCode().is2xxSuccessful() ||
|
||||
tokenRes.getBody() == null ||
|
||||
!tokenRes.getBody().has("access_token")
|
||||
) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String accessToken = tokenRes.getBody().get("access_token").asText();
|
||||
HttpHeaders authHeaders = new HttpHeaders();
|
||||
authHeaders.setBearerAuth(accessToken);
|
||||
authHeaders.set(HttpHeaders.USER_AGENT, "OpenIsle");
|
||||
HttpEntity<Void> entity = new HttpEntity<>(authHeaders);
|
||||
ResponseEntity<JsonNode> userRes = restTemplate.exchange(
|
||||
"https://api.github.com/user",
|
||||
HttpMethod.GET,
|
||||
entity,
|
||||
JsonNode.class
|
||||
);
|
||||
if (!userRes.getStatusCode().is2xxSuccessful() || userRes.getBody() == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
JsonNode userNode = userRes.getBody();
|
||||
String username = userNode.hasNonNull("login") ? userNode.get("login").asText() : null;
|
||||
String avatarUrl = userNode.hasNonNull("avatar_url")
|
||||
? userNode.get("avatar_url").asText()
|
||||
: null;
|
||||
String email = null;
|
||||
if (userNode.hasNonNull("email")) {
|
||||
email = userNode.get("email").asText();
|
||||
}
|
||||
if (email == null || email.isEmpty()) {
|
||||
try {
|
||||
String tokenUrl = "https://github.com/login/oauth/access_token";
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
Map<String, String> body = new HashMap<>();
|
||||
body.put("client_id", clientId);
|
||||
body.put("client_secret", clientSecret);
|
||||
body.put("code", code);
|
||||
if (redirectUri != null) {
|
||||
body.put("redirect_uri", redirectUri);
|
||||
ResponseEntity<JsonNode> emailsRes = restTemplate.exchange(
|
||||
"https://api.github.com/user/emails",
|
||||
HttpMethod.GET,
|
||||
entity,
|
||||
JsonNode.class
|
||||
);
|
||||
if (
|
||||
emailsRes.getStatusCode().is2xxSuccessful() &&
|
||||
emailsRes.getBody() != null &&
|
||||
emailsRes.getBody().isArray()
|
||||
) {
|
||||
for (JsonNode n : emailsRes.getBody()) {
|
||||
if (n.has("primary") && n.get("primary").asBoolean()) {
|
||||
email = n.get("email").asText();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
HttpEntity<Map<String, String>> request = new HttpEntity<>(body, headers);
|
||||
ResponseEntity<JsonNode> tokenRes = restTemplate.postForEntity(tokenUrl, request, JsonNode.class);
|
||||
if (!tokenRes.getStatusCode().is2xxSuccessful() || tokenRes.getBody() == null || !tokenRes.getBody().has("access_token")) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String accessToken = tokenRes.getBody().get("access_token").asText();
|
||||
HttpHeaders authHeaders = new HttpHeaders();
|
||||
authHeaders.setBearerAuth(accessToken);
|
||||
authHeaders.set(HttpHeaders.USER_AGENT, "OpenIsle");
|
||||
HttpEntity<Void> entity = new HttpEntity<>(authHeaders);
|
||||
ResponseEntity<JsonNode> userRes = restTemplate.exchange(
|
||||
"https://api.github.com/user", HttpMethod.GET, entity, JsonNode.class);
|
||||
if (!userRes.getStatusCode().is2xxSuccessful() || userRes.getBody() == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
JsonNode userNode = userRes.getBody();
|
||||
String username = userNode.hasNonNull("login") ? userNode.get("login").asText() : null;
|
||||
String avatarUrl = userNode.hasNonNull("avatar_url") ? userNode.get("avatar_url").asText() : null;
|
||||
String email = null;
|
||||
if (userNode.hasNonNull("email")) {
|
||||
email = userNode.get("email").asText();
|
||||
}
|
||||
if (email == null || email.isEmpty()) {
|
||||
try {
|
||||
ResponseEntity<JsonNode> emailsRes = restTemplate.exchange(
|
||||
"https://api.github.com/user/emails", HttpMethod.GET, entity, JsonNode.class);
|
||||
if (emailsRes.getStatusCode().is2xxSuccessful() && emailsRes.getBody() != null && emailsRes.getBody().isArray()) {
|
||||
for (JsonNode n : emailsRes.getBody()) {
|
||||
if (n.has("primary") && n.get("primary").asBoolean()) {
|
||||
email = n.get("email").asText();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (HttpClientErrorException ignored) {
|
||||
// ignore when the email API is not accessible
|
||||
}
|
||||
}
|
||||
if (email == null) {
|
||||
email = username + "@users.noreply.github.com";
|
||||
}
|
||||
return Optional.of(processUser(email, username, avatarUrl, mode, viaInvite));
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
} catch (HttpClientErrorException ignored) {
|
||||
// ignore when the email API is not accessible
|
||||
}
|
||||
}
|
||||
if (email == null) {
|
||||
email = username + "@users.noreply.github.com";
|
||||
}
|
||||
return Optional.of(processUser(email, username, avatarUrl, mode, viaInvite));
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private AuthResult processUser(String email, String username, String avatar, com.openisle.model.RegisterMode mode, boolean viaInvite) {
|
||||
Optional<User> existing = userRepository.findByEmail(email);
|
||||
if (existing.isPresent()) {
|
||||
User user = existing.get();
|
||||
if (!user.isVerified()) {
|
||||
user.setVerified(true);
|
||||
user.setVerificationCode(null);
|
||||
userRepository.save(user);
|
||||
}
|
||||
return new AuthResult(user, false);
|
||||
}
|
||||
String baseUsername = username != null ? username : email.split("@")[0];
|
||||
String finalUsername = baseUsername;
|
||||
int suffix = 1;
|
||||
while (userRepository.findByUsername(finalUsername).isPresent()) {
|
||||
finalUsername = baseUsername + suffix++;
|
||||
}
|
||||
User user = new User();
|
||||
user.setUsername(finalUsername);
|
||||
user.setEmail(email);
|
||||
user.setPassword("");
|
||||
user.setRole(Role.USER);
|
||||
private AuthResult processUser(
|
||||
String email,
|
||||
String username,
|
||||
String avatar,
|
||||
com.openisle.model.RegisterMode mode,
|
||||
boolean viaInvite
|
||||
) {
|
||||
Optional<User> existing = userRepository.findByEmail(email);
|
||||
if (existing.isPresent()) {
|
||||
User user = existing.get();
|
||||
if (!user.isVerified()) {
|
||||
user.setVerified(true);
|
||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT || viaInvite);
|
||||
if (avatar != null) {
|
||||
user.setAvatar(avatar);
|
||||
} else {
|
||||
user.setAvatar(avatarGenerator.generate(finalUsername));
|
||||
}
|
||||
return new AuthResult(userRepository.save(user), true);
|
||||
user.setVerificationCode(null);
|
||||
userRepository.save(user);
|
||||
}
|
||||
return new AuthResult(user, false);
|
||||
}
|
||||
String baseUsername = username != null ? username : email.split("@")[0];
|
||||
String finalUsername = baseUsername;
|
||||
int suffix = 1;
|
||||
while (userRepository.findByUsername(finalUsername).isPresent()) {
|
||||
finalUsername = baseUsername + suffix++;
|
||||
}
|
||||
User user = new User();
|
||||
user.setUsername(finalUsername);
|
||||
user.setEmail(email);
|
||||
user.setPassword("");
|
||||
user.setRole(Role.USER);
|
||||
user.setVerified(true);
|
||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT || viaInvite);
|
||||
if (avatar != null) {
|
||||
user.setAvatar(avatar);
|
||||
} else {
|
||||
user.setAvatar(avatarGenerator.generate(finalUsername));
|
||||
}
|
||||
return new AuthResult(userRepository.save(user), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,72 +7,84 @@ import com.google.api.client.json.jackson2.JacksonFactory;
|
||||
import com.openisle.model.Role;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import com.openisle.service.AvatarGenerator;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.openisle.service.AvatarGenerator;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GoogleAuthService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final AvatarGenerator avatarGenerator;
|
||||
private final UserRepository userRepository;
|
||||
private final AvatarGenerator avatarGenerator;
|
||||
|
||||
@Value("${google.client-id:}")
|
||||
private String clientId;
|
||||
@Value("${google.client-id:}")
|
||||
private String clientId;
|
||||
|
||||
public Optional<AuthResult> authenticate(String idTokenString, com.openisle.model.RegisterMode mode, boolean viaInvite) {
|
||||
GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new JacksonFactory())
|
||||
.setAudience(Collections.singletonList(clientId))
|
||||
.build();
|
||||
try {
|
||||
GoogleIdToken idToken = verifier.verify(idTokenString);
|
||||
if (idToken == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
GoogleIdToken.Payload payload = idToken.getPayload();
|
||||
String email = payload.getEmail();
|
||||
String name = (String) payload.get("name");
|
||||
String picture = (String) payload.get("picture");
|
||||
return Optional.of(processUser(email, name, picture, mode, viaInvite));
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
public Optional<AuthResult> authenticate(
|
||||
String idTokenString,
|
||||
com.openisle.model.RegisterMode mode,
|
||||
boolean viaInvite
|
||||
) {
|
||||
GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(
|
||||
new NetHttpTransport(),
|
||||
new JacksonFactory()
|
||||
)
|
||||
.setAudience(Collections.singletonList(clientId))
|
||||
.build();
|
||||
try {
|
||||
GoogleIdToken idToken = verifier.verify(idTokenString);
|
||||
if (idToken == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
GoogleIdToken.Payload payload = idToken.getPayload();
|
||||
String email = payload.getEmail();
|
||||
String name = (String) payload.get("name");
|
||||
String picture = (String) payload.get("picture");
|
||||
return Optional.of(processUser(email, name, picture, mode, viaInvite));
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private AuthResult processUser(String email, String name, String avatar, com.openisle.model.RegisterMode mode, boolean viaInvite) {
|
||||
Optional<User> existing = userRepository.findByEmail(email);
|
||||
if (existing.isPresent()) {
|
||||
User user = existing.get();
|
||||
if (!user.isVerified()) {
|
||||
user.setVerified(true);
|
||||
user.setVerificationCode(null);
|
||||
userRepository.save(user);
|
||||
}
|
||||
return new AuthResult(user, false);
|
||||
}
|
||||
User user = new User();
|
||||
String baseUsername = email.split("@")[0];
|
||||
String username = baseUsername;
|
||||
int suffix = 1;
|
||||
while (userRepository.findByUsername(username).isPresent()) {
|
||||
username = baseUsername + suffix++;
|
||||
}
|
||||
user.setUsername(username);
|
||||
user.setEmail(email);
|
||||
user.setPassword("");
|
||||
user.setRole(Role.USER);
|
||||
private AuthResult processUser(
|
||||
String email,
|
||||
String name,
|
||||
String avatar,
|
||||
com.openisle.model.RegisterMode mode,
|
||||
boolean viaInvite
|
||||
) {
|
||||
Optional<User> existing = userRepository.findByEmail(email);
|
||||
if (existing.isPresent()) {
|
||||
User user = existing.get();
|
||||
if (!user.isVerified()) {
|
||||
user.setVerified(true);
|
||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT || viaInvite);
|
||||
if (avatar != null) {
|
||||
user.setAvatar(avatar);
|
||||
} else {
|
||||
user.setAvatar(avatarGenerator.generate(username));
|
||||
}
|
||||
return new AuthResult(userRepository.save(user), true);
|
||||
user.setVerificationCode(null);
|
||||
userRepository.save(user);
|
||||
}
|
||||
return new AuthResult(user, false);
|
||||
}
|
||||
User user = new User();
|
||||
String baseUsername = email.split("@")[0];
|
||||
String username = baseUsername;
|
||||
int suffix = 1;
|
||||
while (userRepository.findByUsername(username).isPresent()) {
|
||||
username = baseUsername + suffix++;
|
||||
}
|
||||
user.setUsername(username);
|
||||
user.setEmail(email);
|
||||
user.setPassword("");
|
||||
user.setRole(Role.USER);
|
||||
user.setVerified(true);
|
||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT || viaInvite);
|
||||
if (avatar != null) {
|
||||
user.setAvatar(avatar);
|
||||
} else {
|
||||
user.setAvatar(avatarGenerator.generate(username));
|
||||
}
|
||||
return new AuthResult(userRepository.save(user), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.openisle.service;
|
||||
|
||||
import com.openisle.model.Image;
|
||||
import com.openisle.repository.ImageRepository;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -13,94 +12,102 @@ import java.util.regex.Pattern;
|
||||
* Abstract service for uploading images and tracking their references.
|
||||
*/
|
||||
public abstract class ImageUploader {
|
||||
private final ImageRepository imageRepository;
|
||||
private final String baseUrl;
|
||||
private final Pattern urlPattern;
|
||||
|
||||
protected ImageUploader(ImageRepository imageRepository, String baseUrl) {
|
||||
this.imageRepository = imageRepository;
|
||||
if (baseUrl.endsWith("/")) {
|
||||
this.baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
|
||||
private final ImageRepository imageRepository;
|
||||
private final String baseUrl;
|
||||
private final Pattern urlPattern;
|
||||
|
||||
protected ImageUploader(ImageRepository imageRepository, String baseUrl) {
|
||||
this.imageRepository = imageRepository;
|
||||
if (baseUrl.endsWith("/")) {
|
||||
this.baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
|
||||
} else {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
this.urlPattern = Pattern.compile(Pattern.quote(this.baseUrl) + "/[^\\s)]+");
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload an image asynchronously and return a future of its accessible URL.
|
||||
*/
|
||||
public CompletableFuture<String> upload(byte[] data, String filename) {
|
||||
return doUpload(data, filename).thenApply(url -> url);
|
||||
}
|
||||
|
||||
protected abstract CompletableFuture<String> doUpload(byte[] data, String filename);
|
||||
|
||||
protected abstract void deleteFromStore(String key);
|
||||
|
||||
/**
|
||||
* Generate a presigned PUT URL for direct browser upload.
|
||||
* Default implementation is unsupported.
|
||||
*/
|
||||
public java.util.Map<String, String> presignUpload(String filename) {
|
||||
throw new UnsupportedOperationException("presignUpload not supported");
|
||||
}
|
||||
|
||||
/** Extract COS URLs from text. */
|
||||
public Set<String> extractUrls(String text) {
|
||||
Set<String> set = new HashSet<>();
|
||||
if (text == null) return set;
|
||||
Matcher m = urlPattern.matcher(text);
|
||||
while (m.find()) {
|
||||
set.add(m.group());
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
public void addReferences(Set<String> urls) {
|
||||
for (String u : urls) addReference(u);
|
||||
}
|
||||
|
||||
public void removeReferences(Set<String> urls) {
|
||||
for (String u : urls) removeReference(u);
|
||||
}
|
||||
|
||||
public void adjustReferences(String oldText, String newText) {
|
||||
Set<String> oldUrls = extractUrls(oldText);
|
||||
Set<String> newUrls = extractUrls(newText);
|
||||
for (String u : newUrls) {
|
||||
if (!oldUrls.contains(u)) addReference(u);
|
||||
}
|
||||
for (String u : oldUrls) {
|
||||
if (!newUrls.contains(u)) removeReference(u);
|
||||
}
|
||||
}
|
||||
|
||||
private void addReference(String url) {
|
||||
if (!url.startsWith(baseUrl)) return;
|
||||
imageRepository
|
||||
.findByUrl(url)
|
||||
.ifPresentOrElse(
|
||||
img -> {
|
||||
img.setRefCount(img.getRefCount() + 1);
|
||||
imageRepository.save(img);
|
||||
},
|
||||
() -> {
|
||||
Image img = new Image();
|
||||
img.setUrl(url);
|
||||
img.setRefCount(1);
|
||||
imageRepository.save(img);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void removeReference(String url) {
|
||||
if (!url.startsWith(baseUrl)) return;
|
||||
imageRepository
|
||||
.findByUrl(url)
|
||||
.ifPresent(img -> {
|
||||
long count = img.getRefCount() - 1;
|
||||
if (count <= 0) {
|
||||
imageRepository.delete(img);
|
||||
String key = url.substring(baseUrl.length() + 1);
|
||||
deleteFromStore(key);
|
||||
} else {
|
||||
this.baseUrl = baseUrl;
|
||||
img.setRefCount(count);
|
||||
imageRepository.save(img);
|
||||
}
|
||||
this.urlPattern = Pattern.compile(Pattern.quote(this.baseUrl) + "/[^\\s)]+");
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload an image asynchronously and return a future of its accessible URL.
|
||||
*/
|
||||
public CompletableFuture<String> upload(byte[] data, String filename) {
|
||||
return doUpload(data, filename).thenApply(url -> url);
|
||||
}
|
||||
|
||||
protected abstract CompletableFuture<String> doUpload(byte[] data, String filename);
|
||||
|
||||
protected abstract void deleteFromStore(String key);
|
||||
|
||||
/**
|
||||
* Generate a presigned PUT URL for direct browser upload.
|
||||
* Default implementation is unsupported.
|
||||
*/
|
||||
public java.util.Map<String, String> presignUpload(String filename) {
|
||||
throw new UnsupportedOperationException("presignUpload not supported");
|
||||
}
|
||||
|
||||
/** Extract COS URLs from text. */
|
||||
public Set<String> extractUrls(String text) {
|
||||
Set<String> set = new HashSet<>();
|
||||
if (text == null) return set;
|
||||
Matcher m = urlPattern.matcher(text);
|
||||
while (m.find()) {
|
||||
set.add(m.group());
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
public void addReferences(Set<String> urls) {
|
||||
for (String u : urls) addReference(u);
|
||||
}
|
||||
|
||||
public void removeReferences(Set<String> urls) {
|
||||
for (String u : urls) removeReference(u);
|
||||
}
|
||||
|
||||
public void adjustReferences(String oldText, String newText) {
|
||||
Set<String> oldUrls = extractUrls(oldText);
|
||||
Set<String> newUrls = extractUrls(newText);
|
||||
for (String u : newUrls) {
|
||||
if (!oldUrls.contains(u)) addReference(u);
|
||||
}
|
||||
for (String u : oldUrls) {
|
||||
if (!newUrls.contains(u)) removeReference(u);
|
||||
}
|
||||
}
|
||||
|
||||
private void addReference(String url) {
|
||||
if (!url.startsWith(baseUrl)) return;
|
||||
imageRepository.findByUrl(url).ifPresentOrElse(img -> {
|
||||
img.setRefCount(img.getRefCount() + 1);
|
||||
imageRepository.save(img);
|
||||
}, () -> {
|
||||
Image img = new Image();
|
||||
img.setUrl(url);
|
||||
img.setRefCount(1);
|
||||
imageRepository.save(img);
|
||||
});
|
||||
}
|
||||
|
||||
private void removeReference(String url) {
|
||||
if (!url.startsWith(baseUrl)) return;
|
||||
imageRepository.findByUrl(url).ifPresent(img -> {
|
||||
long count = img.getRefCount() - 1;
|
||||
if (count <= 0) {
|
||||
imageRepository.delete(img);
|
||||
String key = url.substring(baseUrl.length() + 1);
|
||||
deleteFromStore(key);
|
||||
} else {
|
||||
img.setRefCount(count);
|
||||
imageRepository.save(img);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,81 +4,88 @@ import com.openisle.model.InviteToken;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.InviteTokenRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InviteService {
|
||||
private final InviteTokenRepository inviteTokenRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final JwtService jwtService;
|
||||
private final PointService pointService;
|
||||
|
||||
@Value
|
||||
public class InviteValidateResult {
|
||||
InviteToken inviteToken;
|
||||
boolean validate;
|
||||
private final InviteTokenRepository inviteTokenRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final JwtService jwtService;
|
||||
private final PointService pointService;
|
||||
|
||||
@Value
|
||||
public class InviteValidateResult {
|
||||
|
||||
InviteToken inviteToken;
|
||||
boolean validate;
|
||||
}
|
||||
|
||||
public String generate(String username) {
|
||||
User inviter = userRepository.findByUsername(username).orElseThrow();
|
||||
LocalDate today = LocalDate.now();
|
||||
Optional<InviteToken> existing = inviteTokenRepository.findByInviterAndCreatedDate(
|
||||
inviter,
|
||||
today
|
||||
);
|
||||
if (existing.isPresent()) {
|
||||
InviteToken inviteToken = existing.get();
|
||||
return inviteToken.getShortToken() != null
|
||||
? inviteToken.getShortToken()
|
||||
: inviteToken.getToken();
|
||||
}
|
||||
|
||||
public String generate(String username) {
|
||||
User inviter = userRepository.findByUsername(username).orElseThrow();
|
||||
LocalDate today = LocalDate.now();
|
||||
Optional<InviteToken> existing = inviteTokenRepository.findByInviterAndCreatedDate(inviter, today);
|
||||
if (existing.isPresent()) {
|
||||
InviteToken inviteToken = existing.get();
|
||||
return inviteToken.getShortToken() != null ? inviteToken.getShortToken() : inviteToken.getToken();
|
||||
}
|
||||
String token = jwtService.generateInviteToken(username);
|
||||
String shortToken;
|
||||
do {
|
||||
shortToken = java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 8);
|
||||
} while (inviteTokenRepository.existsByShortToken(shortToken));
|
||||
|
||||
String token = jwtService.generateInviteToken(username);
|
||||
String shortToken;
|
||||
do {
|
||||
shortToken = java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 8);
|
||||
} while (inviteTokenRepository.existsByShortToken(shortToken));
|
||||
InviteToken inviteToken = new InviteToken();
|
||||
inviteToken.setToken(token);
|
||||
inviteToken.setShortToken(shortToken);
|
||||
inviteToken.setInviter(inviter);
|
||||
inviteToken.setCreatedDate(today);
|
||||
inviteToken.setUsageCount(0);
|
||||
inviteTokenRepository.save(inviteToken);
|
||||
return shortToken;
|
||||
}
|
||||
|
||||
InviteToken inviteToken = new InviteToken();
|
||||
inviteToken.setToken(token);
|
||||
inviteToken.setShortToken(shortToken);
|
||||
inviteToken.setInviter(inviter);
|
||||
inviteToken.setCreatedDate(today);
|
||||
inviteToken.setUsageCount(0);
|
||||
inviteTokenRepository.save(inviteToken);
|
||||
return shortToken;
|
||||
public InviteValidateResult validate(String token) {
|
||||
if (token == null || token.isEmpty()) {
|
||||
return new InviteValidateResult(null, false);
|
||||
}
|
||||
|
||||
public InviteValidateResult validate(String token) {
|
||||
if (token == null || token.isEmpty()) {
|
||||
return new InviteValidateResult(null, false);
|
||||
}
|
||||
|
||||
InviteToken invite = inviteTokenRepository.findById(token).orElse(null);
|
||||
String realToken = token;
|
||||
if (invite == null) {
|
||||
invite = inviteTokenRepository.findByShortToken(token).orElse(null);
|
||||
if (invite == null) {
|
||||
return new InviteValidateResult(null, false);
|
||||
}
|
||||
realToken = invite.getToken();
|
||||
}
|
||||
|
||||
try {
|
||||
jwtService.validateAndGetSubjectForInvite(realToken);
|
||||
} catch (Exception e) {
|
||||
return new InviteValidateResult(null, false);
|
||||
}
|
||||
|
||||
return new InviteValidateResult(invite, invite.getUsageCount() < 3);
|
||||
InviteToken invite = inviteTokenRepository.findById(token).orElse(null);
|
||||
String realToken = token;
|
||||
if (invite == null) {
|
||||
invite = inviteTokenRepository.findByShortToken(token).orElse(null);
|
||||
if (invite == null) {
|
||||
return new InviteValidateResult(null, false);
|
||||
}
|
||||
realToken = invite.getToken();
|
||||
}
|
||||
|
||||
public void consume(String token, String newUserName) {
|
||||
InviteToken invite = inviteTokenRepository.findById(token)
|
||||
.orElseGet(() -> inviteTokenRepository.findByShortToken(token).orElseThrow());
|
||||
invite.setUsageCount(invite.getUsageCount() + 1);
|
||||
inviteTokenRepository.save(invite);
|
||||
pointService.awardForInvite(invite.getInviter().getUsername(), newUserName);
|
||||
try {
|
||||
jwtService.validateAndGetSubjectForInvite(realToken);
|
||||
} catch (Exception e) {
|
||||
return new InviteValidateResult(null, false);
|
||||
}
|
||||
|
||||
return new InviteValidateResult(invite, invite.getUsageCount() < 3);
|
||||
}
|
||||
|
||||
public void consume(String token, String newUserName) {
|
||||
InviteToken invite = inviteTokenRepository
|
||||
.findById(token)
|
||||
.orElseGet(() -> inviteTokenRepository.findByShortToken(token).orElseThrow());
|
||||
invite.setUsageCount(invite.getUsageCount() + 1);
|
||||
inviteTokenRepository.save(invite);
|
||||
pointService.awardForInvite(invite.getInviter().getUsername(), newUserName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,120 +3,119 @@ package com.openisle.service;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Key;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Date;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import java.security.Key;
|
||||
import java.util.Date;
|
||||
|
||||
@Service
|
||||
public class JwtService {
|
||||
@Value("${app.jwt.secret}")
|
||||
private String secret;
|
||||
|
||||
@Value("${app.jwt.reason-secret}")
|
||||
private String reasonSecret;
|
||||
@Value("${app.jwt.secret}")
|
||||
private String secret;
|
||||
|
||||
@Value("${app.jwt.reset-secret}")
|
||||
private String resetSecret;
|
||||
@Value("${app.jwt.reason-secret}")
|
||||
private String reasonSecret;
|
||||
|
||||
@Value("${app.jwt.invite-secret}")
|
||||
private String inviteSecret;
|
||||
@Value("${app.jwt.reset-secret}")
|
||||
private String resetSecret;
|
||||
|
||||
@Value("${app.jwt.expiration}")
|
||||
private long expiration;
|
||||
@Value("${app.jwt.invite-secret}")
|
||||
private String inviteSecret;
|
||||
|
||||
private Key getSigningKeyForSecret(String signSecret) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] keyBytes = digest.digest(signSecret.getBytes(StandardCharsets.UTF_8));
|
||||
return Keys.hmacShaKeyFor(keyBytes);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("SHA-256 not available", e);
|
||||
}
|
||||
@Value("${app.jwt.expiration}")
|
||||
private long expiration;
|
||||
|
||||
private Key getSigningKeyForSecret(String signSecret) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] keyBytes = digest.digest(signSecret.getBytes(StandardCharsets.UTF_8));
|
||||
return Keys.hmacShaKeyFor(keyBytes);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("SHA-256 not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String generateToken(String subject) {
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + expiration);
|
||||
return Jwts.builder()
|
||||
.setSubject(subject)
|
||||
.setIssuedAt(now)
|
||||
.setExpiration(expiryDate)
|
||||
.signWith(getSigningKeyForSecret(secret))
|
||||
.compact();
|
||||
}
|
||||
public String generateToken(String subject) {
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + expiration);
|
||||
return Jwts.builder()
|
||||
.setSubject(subject)
|
||||
.setIssuedAt(now)
|
||||
.setExpiration(expiryDate)
|
||||
.signWith(getSigningKeyForSecret(secret))
|
||||
.compact();
|
||||
}
|
||||
|
||||
public String generateReasonToken(String subject) {
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + expiration);
|
||||
return Jwts.builder()
|
||||
.setSubject(subject)
|
||||
.setIssuedAt(now)
|
||||
.setExpiration(expiryDate)
|
||||
.signWith(getSigningKeyForSecret(reasonSecret))
|
||||
.compact();
|
||||
}
|
||||
public String generateReasonToken(String subject) {
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + expiration);
|
||||
return Jwts.builder()
|
||||
.setSubject(subject)
|
||||
.setIssuedAt(now)
|
||||
.setExpiration(expiryDate)
|
||||
.signWith(getSigningKeyForSecret(reasonSecret))
|
||||
.compact();
|
||||
}
|
||||
|
||||
public String generateResetToken(String subject) {
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + expiration);
|
||||
return Jwts.builder()
|
||||
.setSubject(subject)
|
||||
.setIssuedAt(now)
|
||||
.setExpiration(expiryDate)
|
||||
.signWith(getSigningKeyForSecret(resetSecret))
|
||||
.compact();
|
||||
}
|
||||
public String generateResetToken(String subject) {
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + expiration);
|
||||
return Jwts.builder()
|
||||
.setSubject(subject)
|
||||
.setIssuedAt(now)
|
||||
.setExpiration(expiryDate)
|
||||
.signWith(getSigningKeyForSecret(resetSecret))
|
||||
.compact();
|
||||
}
|
||||
|
||||
public String generateInviteToken(String subject) {
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + expiration);
|
||||
return Jwts.builder()
|
||||
.setSubject(subject)
|
||||
.setIssuedAt(now)
|
||||
.setExpiration(expiryDate)
|
||||
.signWith(getSigningKeyForSecret(inviteSecret))
|
||||
.compact();
|
||||
}
|
||||
public String generateInviteToken(String subject) {
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + expiration);
|
||||
return Jwts.builder()
|
||||
.setSubject(subject)
|
||||
.setIssuedAt(now)
|
||||
.setExpiration(expiryDate)
|
||||
.signWith(getSigningKeyForSecret(inviteSecret))
|
||||
.compact();
|
||||
}
|
||||
|
||||
public String validateAndGetSubject(String token) {
|
||||
Claims claims = Jwts.parserBuilder()
|
||||
.setSigningKey(getSigningKeyForSecret(secret))
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
return claims.getSubject();
|
||||
}
|
||||
public String validateAndGetSubject(String token) {
|
||||
Claims claims = Jwts.parserBuilder()
|
||||
.setSigningKey(getSigningKeyForSecret(secret))
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
return claims.getSubject();
|
||||
}
|
||||
|
||||
public String validateAndGetSubjectForReason(String token) {
|
||||
Claims claims = Jwts.parserBuilder()
|
||||
.setSigningKey(getSigningKeyForSecret(reasonSecret))
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
return claims.getSubject();
|
||||
}
|
||||
public String validateAndGetSubjectForReason(String token) {
|
||||
Claims claims = Jwts.parserBuilder()
|
||||
.setSigningKey(getSigningKeyForSecret(reasonSecret))
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
return claims.getSubject();
|
||||
}
|
||||
|
||||
public String validateAndGetSubjectForReset(String token) {
|
||||
Claims claims = Jwts.parserBuilder()
|
||||
.setSigningKey(getSigningKeyForSecret(resetSecret))
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
return claims.getSubject();
|
||||
}
|
||||
public String validateAndGetSubjectForReset(String token) {
|
||||
Claims claims = Jwts.parserBuilder()
|
||||
.setSigningKey(getSigningKeyForSecret(resetSecret))
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
return claims.getSubject();
|
||||
}
|
||||
|
||||
public String validateAndGetSubjectForInvite(String token) {
|
||||
Claims claims = Jwts.parserBuilder()
|
||||
.setSigningKey(getSigningKeyForSecret(inviteSecret))
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
return claims.getSubject();
|
||||
}
|
||||
public String validateAndGetSubjectForInvite(String token) {
|
||||
Claims claims = Jwts.parserBuilder()
|
||||
.setSigningKey(getSigningKeyForSecret(inviteSecret))
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
return claims.getSubject();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,90 +1,92 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import com.openisle.repository.ExperienceLogRepository;
|
||||
import com.openisle.model.ExperienceLog;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.ExperienceLogRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import java.time.LocalDate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LevelService {
|
||||
private final UserRepository userRepository;
|
||||
// repositories for experience-related entities
|
||||
private final ExperienceLogRepository experienceLogRepository;
|
||||
private final UserVisitService userVisitService;
|
||||
|
||||
private static final int[] LEVEL_EXP = {100,200,300,600,1200,10000};
|
||||
private final UserRepository userRepository;
|
||||
// repositories for experience-related entities
|
||||
private final ExperienceLogRepository experienceLogRepository;
|
||||
private final UserVisitService userVisitService;
|
||||
|
||||
private ExperienceLog getTodayLog(User user) {
|
||||
LocalDate today = LocalDate.now();
|
||||
return experienceLogRepository.findByUserAndLogDate(user, today)
|
||||
.orElseGet(() -> {
|
||||
ExperienceLog log = new ExperienceLog();
|
||||
log.setUser(user);
|
||||
log.setLogDate(today);
|
||||
log.setPostCount(0);
|
||||
log.setCommentCount(0);
|
||||
log.setReactionCount(0);
|
||||
return experienceLogRepository.save(log);
|
||||
});
|
||||
private static final int[] LEVEL_EXP = { 100, 200, 300, 600, 1200, 10000 };
|
||||
|
||||
private ExperienceLog getTodayLog(User user) {
|
||||
LocalDate today = LocalDate.now();
|
||||
return experienceLogRepository
|
||||
.findByUserAndLogDate(user, today)
|
||||
.orElseGet(() -> {
|
||||
ExperienceLog log = new ExperienceLog();
|
||||
log.setUser(user);
|
||||
log.setLogDate(today);
|
||||
log.setPostCount(0);
|
||||
log.setCommentCount(0);
|
||||
log.setReactionCount(0);
|
||||
return experienceLogRepository.save(log);
|
||||
});
|
||||
}
|
||||
|
||||
private int addExperience(User user, int amount) {
|
||||
user.setExperience(user.getExperience() + amount);
|
||||
userRepository.save(user);
|
||||
return amount;
|
||||
}
|
||||
|
||||
public int awardForPost(String username) {
|
||||
User user = userRepository.findByUsername(username).orElseThrow();
|
||||
ExperienceLog log = getTodayLog(user);
|
||||
if (log.getPostCount() > 1) return 0;
|
||||
log.setPostCount(log.getPostCount() + 1);
|
||||
experienceLogRepository.save(log);
|
||||
return addExperience(user, 30);
|
||||
}
|
||||
|
||||
public int awardForComment(String username) {
|
||||
User user = userRepository.findByUsername(username).orElseThrow();
|
||||
ExperienceLog log = getTodayLog(user);
|
||||
if (log.getCommentCount() > 3) return 0;
|
||||
log.setCommentCount(log.getCommentCount() + 1);
|
||||
experienceLogRepository.save(log);
|
||||
return addExperience(user, 10);
|
||||
}
|
||||
|
||||
public int awardForReaction(String username) {
|
||||
User user = userRepository.findByUsername(username).orElseThrow();
|
||||
ExperienceLog log = getTodayLog(user);
|
||||
if (log.getReactionCount() > 3) return 0;
|
||||
log.setReactionCount(log.getReactionCount() + 1);
|
||||
experienceLogRepository.save(log);
|
||||
return addExperience(user, 5);
|
||||
}
|
||||
|
||||
public int awardForSignin(String username) {
|
||||
boolean first = userVisitService.recordVisit(username);
|
||||
if (!first) return 0;
|
||||
User user = userRepository.findByUsername(username).orElseThrow();
|
||||
return addExperience(user, 5);
|
||||
}
|
||||
|
||||
public int getLevel(int exp) {
|
||||
int level = 0;
|
||||
for (int t : LEVEL_EXP) {
|
||||
if (exp >= t) level++;
|
||||
else break;
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
private int addExperience(User user, int amount) {
|
||||
user.setExperience(user.getExperience() + amount);
|
||||
userRepository.save(user);
|
||||
return amount;
|
||||
}
|
||||
|
||||
public int awardForPost(String username) {
|
||||
User user = userRepository.findByUsername(username).orElseThrow();
|
||||
ExperienceLog log = getTodayLog(user);
|
||||
if (log.getPostCount() > 1) return 0;
|
||||
log.setPostCount(log.getPostCount() + 1);
|
||||
experienceLogRepository.save(log);
|
||||
return addExperience(user,30);
|
||||
}
|
||||
|
||||
public int awardForComment(String username) {
|
||||
User user = userRepository.findByUsername(username).orElseThrow();
|
||||
ExperienceLog log = getTodayLog(user);
|
||||
if (log.getCommentCount() > 3) return 0;
|
||||
log.setCommentCount(log.getCommentCount() + 1);
|
||||
experienceLogRepository.save(log);
|
||||
return addExperience(user,10);
|
||||
}
|
||||
|
||||
public int awardForReaction(String username) {
|
||||
User user = userRepository.findByUsername(username).orElseThrow();
|
||||
ExperienceLog log = getTodayLog(user);
|
||||
if (log.getReactionCount() > 3) return 0;
|
||||
log.setReactionCount(log.getReactionCount() + 1);
|
||||
experienceLogRepository.save(log);
|
||||
return addExperience(user,5);
|
||||
}
|
||||
|
||||
public int awardForSignin(String username) {
|
||||
boolean first = userVisitService.recordVisit(username);
|
||||
if (!first) return 0;
|
||||
User user = userRepository.findByUsername(username).orElseThrow();
|
||||
return addExperience(user,5);
|
||||
}
|
||||
|
||||
public int getLevel(int exp) {
|
||||
int level = 0;
|
||||
for (int t : LEVEL_EXP) {
|
||||
if (exp >= t) level++; else break;
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
public int nextLevelExp(int exp) {
|
||||
for (int t : LEVEL_EXP) {
|
||||
if (exp < t) return t;
|
||||
}
|
||||
return LEVEL_EXP[LEVEL_EXP.length-1];
|
||||
public int nextLevelExp(int exp) {
|
||||
for (int t : LEVEL_EXP) {
|
||||
if (exp < t) return t;
|
||||
}
|
||||
return LEVEL_EXP[LEVEL_EXP.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,188 +2,201 @@ package com.openisle.service;
|
||||
|
||||
import com.openisle.dto.CommentMedalDto;
|
||||
import com.openisle.dto.ContributorMedalDto;
|
||||
import com.openisle.dto.FeaturedMedalDto;
|
||||
import com.openisle.dto.MedalDto;
|
||||
import com.openisle.dto.PioneerMedalDto;
|
||||
import com.openisle.dto.PostMedalDto;
|
||||
import com.openisle.dto.SeedUserMedalDto;
|
||||
import com.openisle.dto.PioneerMedalDto;
|
||||
import com.openisle.dto.FeaturedMedalDto;
|
||||
import com.openisle.model.MedalType;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.CommentRepository;
|
||||
import com.openisle.repository.PostRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MedalService {
|
||||
private static final long COMMENT_TARGET = 100;
|
||||
private static final long POST_TARGET = 100;
|
||||
private static final LocalDateTime SEED_USER_DEADLINE = LocalDateTime.of(2025, 9, 16, 0, 0);
|
||||
private static final long CONTRIBUTION_TARGET = 1;
|
||||
private static final long PIONEER_LIMIT = 1000;
|
||||
|
||||
private final CommentRepository commentRepository;
|
||||
private final PostRepository postRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final ContributorService contributorService;
|
||||
private static final long COMMENT_TARGET = 100;
|
||||
private static final long POST_TARGET = 100;
|
||||
private static final LocalDateTime SEED_USER_DEADLINE = LocalDateTime.of(2025, 9, 16, 0, 0);
|
||||
private static final long CONTRIBUTION_TARGET = 1;
|
||||
private static final long PIONEER_LIMIT = 1000;
|
||||
|
||||
public List<MedalDto> getMedals(Long userId) {
|
||||
List<MedalDto> medals = new ArrayList<>();
|
||||
User user = null;
|
||||
if (userId != null) {
|
||||
user = userRepository.findById(userId).orElse(null);
|
||||
private final CommentRepository commentRepository;
|
||||
private final PostRepository postRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final ContributorService contributorService;
|
||||
|
||||
public List<MedalDto> getMedals(Long userId) {
|
||||
List<MedalDto> medals = new ArrayList<>();
|
||||
User user = null;
|
||||
if (userId != null) {
|
||||
user = userRepository.findById(userId).orElse(null);
|
||||
}
|
||||
MedalType selected = user != null ? user.getDisplayMedal() : null;
|
||||
|
||||
CommentMedalDto commentMedal = new CommentMedalDto();
|
||||
commentMedal.setIcon(
|
||||
"https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/achi_comment.png"
|
||||
);
|
||||
commentMedal.setTitle("评论达人");
|
||||
commentMedal.setDescription("评论超过100条");
|
||||
commentMedal.setType(MedalType.COMMENT);
|
||||
commentMedal.setTargetCommentCount(COMMENT_TARGET);
|
||||
if (user != null) {
|
||||
long count = commentRepository.countByAuthor_Id(userId);
|
||||
commentMedal.setCurrentCommentCount(count);
|
||||
commentMedal.setCompleted(count >= COMMENT_TARGET);
|
||||
} else {
|
||||
commentMedal.setCurrentCommentCount(0);
|
||||
commentMedal.setCompleted(false);
|
||||
}
|
||||
commentMedal.setSelected(selected == MedalType.COMMENT);
|
||||
medals.add(commentMedal);
|
||||
|
||||
PostMedalDto postMedal = new PostMedalDto();
|
||||
postMedal.setIcon(
|
||||
"https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/achi_post.png"
|
||||
);
|
||||
postMedal.setTitle("发帖达人");
|
||||
postMedal.setDescription("发帖超过100条");
|
||||
postMedal.setType(MedalType.POST);
|
||||
postMedal.setTargetPostCount(POST_TARGET);
|
||||
if (user != null) {
|
||||
long count = postRepository.countByAuthor_Id(userId);
|
||||
postMedal.setCurrentPostCount(count);
|
||||
postMedal.setCompleted(count >= POST_TARGET);
|
||||
} else {
|
||||
postMedal.setCurrentPostCount(0);
|
||||
postMedal.setCompleted(false);
|
||||
}
|
||||
postMedal.setSelected(selected == MedalType.POST);
|
||||
medals.add(postMedal);
|
||||
|
||||
FeaturedMedalDto featuredMedal = new FeaturedMedalDto();
|
||||
featuredMedal.setIcon(
|
||||
"https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/achi_rss.png"
|
||||
);
|
||||
featuredMedal.setTitle("精选作者");
|
||||
featuredMedal.setDescription("至少有1篇文章被收录为精选");
|
||||
featuredMedal.setType(MedalType.FEATURED);
|
||||
featuredMedal.setTargetFeaturedCount(1);
|
||||
if (user != null) {
|
||||
long count = postRepository.countByAuthor_IdAndRssExcludedFalse(user.getId());
|
||||
featuredMedal.setCurrentFeaturedCount(count);
|
||||
featuredMedal.setCompleted(count >= 1);
|
||||
} else {
|
||||
featuredMedal.setCurrentFeaturedCount(0);
|
||||
featuredMedal.setCompleted(false);
|
||||
}
|
||||
featuredMedal.setSelected(selected == MedalType.FEATURED);
|
||||
medals.add(featuredMedal);
|
||||
|
||||
ContributorMedalDto contributorMedal = new ContributorMedalDto();
|
||||
contributorMedal.setIcon(
|
||||
"https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/achi_coder.png"
|
||||
);
|
||||
contributorMedal.setTitle("贡献者");
|
||||
contributorMedal.setDescription("对仓库贡献超过1行代码");
|
||||
contributorMedal.setType(MedalType.CONTRIBUTOR);
|
||||
contributorMedal.setTargetContributionLines(CONTRIBUTION_TARGET);
|
||||
if (user != null) {
|
||||
long lines = contributorService.getContributionLines(user.getUsername());
|
||||
contributorMedal.setCurrentContributionLines(lines);
|
||||
contributorMedal.setCompleted(lines >= CONTRIBUTION_TARGET);
|
||||
} else {
|
||||
contributorMedal.setCurrentContributionLines(0);
|
||||
contributorMedal.setCompleted(false);
|
||||
}
|
||||
contributorMedal.setSelected(selected == MedalType.CONTRIBUTOR);
|
||||
medals.add(contributorMedal);
|
||||
|
||||
SeedUserMedalDto seedUserMedal = new SeedUserMedalDto();
|
||||
seedUserMedal.setIcon(
|
||||
"https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/achi_seed.png"
|
||||
);
|
||||
seedUserMedal.setTitle("种子用户");
|
||||
seedUserMedal.setDescription("2025.9.16前注册的用户");
|
||||
seedUserMedal.setType(MedalType.SEED);
|
||||
if (user != null) {
|
||||
seedUserMedal.setRegisterDate(user.getCreatedAt());
|
||||
seedUserMedal.setCompleted(user.getCreatedAt().isBefore(SEED_USER_DEADLINE));
|
||||
} else {
|
||||
seedUserMedal.setCompleted(false);
|
||||
}
|
||||
seedUserMedal.setSelected(selected == MedalType.SEED);
|
||||
medals.add(seedUserMedal);
|
||||
|
||||
PioneerMedalDto pioneerMedal = new PioneerMedalDto();
|
||||
pioneerMedal.setIcon(
|
||||
"https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/achi_pioneer.png"
|
||||
);
|
||||
pioneerMedal.setTitle("开山鼻祖");
|
||||
pioneerMedal.setDescription("前1000位加入的用户");
|
||||
pioneerMedal.setType(MedalType.PIONEER);
|
||||
if (user != null) {
|
||||
long rank = userRepository.countByCreatedAtBefore(user.getCreatedAt()) + 1;
|
||||
pioneerMedal.setRank(rank);
|
||||
pioneerMedal.setCompleted(rank <= PIONEER_LIMIT);
|
||||
} else {
|
||||
pioneerMedal.setCompleted(false);
|
||||
}
|
||||
pioneerMedal.setSelected(selected == MedalType.PIONEER);
|
||||
medals.add(pioneerMedal);
|
||||
if (user != null && selected == null) {
|
||||
for (MedalDto medal : medals) {
|
||||
if (medal.isCompleted()) {
|
||||
medal.setSelected(true);
|
||||
user.setDisplayMedal(medal.getType());
|
||||
userRepository.save(user);
|
||||
break;
|
||||
}
|
||||
MedalType selected = user != null ? user.getDisplayMedal() : null;
|
||||
|
||||
CommentMedalDto commentMedal = new CommentMedalDto();
|
||||
commentMedal.setIcon("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/achi_comment.png");
|
||||
commentMedal.setTitle("评论达人");
|
||||
commentMedal.setDescription("评论超过100条");
|
||||
commentMedal.setType(MedalType.COMMENT);
|
||||
commentMedal.setTargetCommentCount(COMMENT_TARGET);
|
||||
if (user != null) {
|
||||
long count = commentRepository.countByAuthor_Id(userId);
|
||||
commentMedal.setCurrentCommentCount(count);
|
||||
commentMedal.setCompleted(count >= COMMENT_TARGET);
|
||||
} else {
|
||||
commentMedal.setCurrentCommentCount(0);
|
||||
commentMedal.setCompleted(false);
|
||||
}
|
||||
commentMedal.setSelected(selected == MedalType.COMMENT);
|
||||
medals.add(commentMedal);
|
||||
|
||||
PostMedalDto postMedal = new PostMedalDto();
|
||||
postMedal.setIcon("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/achi_post.png");
|
||||
postMedal.setTitle("发帖达人");
|
||||
postMedal.setDescription("发帖超过100条");
|
||||
postMedal.setType(MedalType.POST);
|
||||
postMedal.setTargetPostCount(POST_TARGET);
|
||||
if (user != null) {
|
||||
long count = postRepository.countByAuthor_Id(userId);
|
||||
postMedal.setCurrentPostCount(count);
|
||||
postMedal.setCompleted(count >= POST_TARGET);
|
||||
} else {
|
||||
postMedal.setCurrentPostCount(0);
|
||||
postMedal.setCompleted(false);
|
||||
}
|
||||
postMedal.setSelected(selected == MedalType.POST);
|
||||
medals.add(postMedal);
|
||||
|
||||
FeaturedMedalDto featuredMedal = new FeaturedMedalDto();
|
||||
featuredMedal.setIcon("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/achi_rss.png");
|
||||
featuredMedal.setTitle("精选作者");
|
||||
featuredMedal.setDescription("至少有1篇文章被收录为精选");
|
||||
featuredMedal.setType(MedalType.FEATURED);
|
||||
featuredMedal.setTargetFeaturedCount(1);
|
||||
if (user != null) {
|
||||
long count = postRepository.countByAuthor_IdAndRssExcludedFalse(user.getId());
|
||||
featuredMedal.setCurrentFeaturedCount(count);
|
||||
featuredMedal.setCompleted(count >= 1);
|
||||
} else {
|
||||
featuredMedal.setCurrentFeaturedCount(0);
|
||||
featuredMedal.setCompleted(false);
|
||||
}
|
||||
featuredMedal.setSelected(selected == MedalType.FEATURED);
|
||||
medals.add(featuredMedal);
|
||||
|
||||
ContributorMedalDto contributorMedal = new ContributorMedalDto();
|
||||
contributorMedal.setIcon("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/achi_coder.png");
|
||||
contributorMedal.setTitle("贡献者");
|
||||
contributorMedal.setDescription("对仓库贡献超过1行代码");
|
||||
contributorMedal.setType(MedalType.CONTRIBUTOR);
|
||||
contributorMedal.setTargetContributionLines(CONTRIBUTION_TARGET);
|
||||
if (user != null) {
|
||||
long lines = contributorService.getContributionLines(user.getUsername());
|
||||
contributorMedal.setCurrentContributionLines(lines);
|
||||
contributorMedal.setCompleted(lines >= CONTRIBUTION_TARGET);
|
||||
} else {
|
||||
contributorMedal.setCurrentContributionLines(0);
|
||||
contributorMedal.setCompleted(false);
|
||||
}
|
||||
contributorMedal.setSelected(selected == MedalType.CONTRIBUTOR);
|
||||
medals.add(contributorMedal);
|
||||
|
||||
SeedUserMedalDto seedUserMedal = new SeedUserMedalDto();
|
||||
seedUserMedal.setIcon("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/achi_seed.png");
|
||||
seedUserMedal.setTitle("种子用户");
|
||||
seedUserMedal.setDescription("2025.9.16前注册的用户");
|
||||
seedUserMedal.setType(MedalType.SEED);
|
||||
if (user != null) {
|
||||
seedUserMedal.setRegisterDate(user.getCreatedAt());
|
||||
seedUserMedal.setCompleted(user.getCreatedAt().isBefore(SEED_USER_DEADLINE));
|
||||
} else {
|
||||
seedUserMedal.setCompleted(false);
|
||||
}
|
||||
seedUserMedal.setSelected(selected == MedalType.SEED);
|
||||
medals.add(seedUserMedal);
|
||||
|
||||
PioneerMedalDto pioneerMedal = new PioneerMedalDto();
|
||||
pioneerMedal.setIcon("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/achi_pioneer.png");
|
||||
pioneerMedal.setTitle("开山鼻祖");
|
||||
pioneerMedal.setDescription("前1000位加入的用户");
|
||||
pioneerMedal.setType(MedalType.PIONEER);
|
||||
if (user != null) {
|
||||
long rank = userRepository.countByCreatedAtBefore(user.getCreatedAt()) + 1;
|
||||
pioneerMedal.setRank(rank);
|
||||
pioneerMedal.setCompleted(rank <= PIONEER_LIMIT);
|
||||
} else {
|
||||
pioneerMedal.setCompleted(false);
|
||||
}
|
||||
pioneerMedal.setSelected(selected == MedalType.PIONEER);
|
||||
medals.add(pioneerMedal);
|
||||
if (user != null && selected == null) {
|
||||
for (MedalDto medal : medals) {
|
||||
if (medal.isCompleted()) {
|
||||
medal.setSelected(true);
|
||||
user.setDisplayMedal(medal.getType());
|
||||
userRepository.save(user);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return medals;
|
||||
}
|
||||
}
|
||||
|
||||
public void ensureDisplayMedal(User user) {
|
||||
if (user == null || user.getDisplayMedal() != null) {
|
||||
return;
|
||||
}
|
||||
if (commentRepository.countByAuthor_Id(user.getId()) >= COMMENT_TARGET) {
|
||||
user.setDisplayMedal(MedalType.COMMENT);
|
||||
} else if (postRepository.countByAuthor_Id(user.getId()) >= POST_TARGET) {
|
||||
user.setDisplayMedal(MedalType.POST);
|
||||
} else if (postRepository.countByAuthor_IdAndRssExcludedFalse(user.getId()) >= 1) {
|
||||
user.setDisplayMedal(MedalType.FEATURED);
|
||||
} else if (contributorService.getContributionLines(user.getUsername()) >= CONTRIBUTION_TARGET) {
|
||||
user.setDisplayMedal(MedalType.CONTRIBUTOR);
|
||||
} else if (userRepository.countByCreatedAtBefore(user.getCreatedAt()) < PIONEER_LIMIT) {
|
||||
user.setDisplayMedal(MedalType.PIONEER);
|
||||
} else if (user.getCreatedAt().isBefore(SEED_USER_DEADLINE)) {
|
||||
user.setDisplayMedal(MedalType.SEED);
|
||||
}
|
||||
if (user.getDisplayMedal() != null) {
|
||||
userRepository.save(user);
|
||||
}
|
||||
}
|
||||
return medals;
|
||||
}
|
||||
|
||||
public void selectMedal(String username, MedalType type) {
|
||||
User user = userRepository.findByUsername(username).orElseThrow();
|
||||
boolean completed = getMedals(user.getId()).stream()
|
||||
.filter(m -> m.getType() == type)
|
||||
.findFirst()
|
||||
.map(MedalDto::isCompleted)
|
||||
.orElse(false);
|
||||
if (!completed) {
|
||||
throw new IllegalArgumentException("Medal not completed");
|
||||
}
|
||||
user.setDisplayMedal(type);
|
||||
userRepository.save(user);
|
||||
public void ensureDisplayMedal(User user) {
|
||||
if (user == null || user.getDisplayMedal() != null) {
|
||||
return;
|
||||
}
|
||||
if (commentRepository.countByAuthor_Id(user.getId()) >= COMMENT_TARGET) {
|
||||
user.setDisplayMedal(MedalType.COMMENT);
|
||||
} else if (postRepository.countByAuthor_Id(user.getId()) >= POST_TARGET) {
|
||||
user.setDisplayMedal(MedalType.POST);
|
||||
} else if (postRepository.countByAuthor_IdAndRssExcludedFalse(user.getId()) >= 1) {
|
||||
user.setDisplayMedal(MedalType.FEATURED);
|
||||
} else if (contributorService.getContributionLines(user.getUsername()) >= CONTRIBUTION_TARGET) {
|
||||
user.setDisplayMedal(MedalType.CONTRIBUTOR);
|
||||
} else if (userRepository.countByCreatedAtBefore(user.getCreatedAt()) < PIONEER_LIMIT) {
|
||||
user.setDisplayMedal(MedalType.PIONEER);
|
||||
} else if (user.getCreatedAt().isBefore(SEED_USER_DEADLINE)) {
|
||||
user.setDisplayMedal(MedalType.SEED);
|
||||
}
|
||||
if (user.getDisplayMedal() != null) {
|
||||
userRepository.save(user);
|
||||
}
|
||||
}
|
||||
|
||||
public void selectMedal(String username, MedalType type) {
|
||||
User user = userRepository.findByUsername(username).orElseThrow();
|
||||
boolean completed = getMedals(user.getId())
|
||||
.stream()
|
||||
.filter(m -> m.getType() == type)
|
||||
.findFirst()
|
||||
.map(MedalDto::isCompleted)
|
||||
.orElse(false);
|
||||
if (!completed) {
|
||||
throw new IllegalArgumentException("Medal not completed");
|
||||
}
|
||||
user.setDisplayMedal(type);
|
||||
userRepository.save(user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import com.openisle.model.Message;
|
||||
import com.openisle.model.MessageConversation;
|
||||
import com.openisle.model.MessageParticipant;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.model.Reaction;
|
||||
import com.openisle.repository.MessageConversationRepository;
|
||||
import com.openisle.repository.MessageParticipantRepository;
|
||||
import com.openisle.repository.MessageRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import com.openisle.repository.ReactionRepository;
|
||||
import com.openisle.dto.ConversationDetailDto;
|
||||
import com.openisle.dto.ConversationDto;
|
||||
import com.openisle.dto.MessageDto;
|
||||
import com.openisle.dto.MessageNotificationPayload;
|
||||
import com.openisle.dto.ReactionDto;
|
||||
import com.openisle.dto.UserSummaryDto;
|
||||
import com.openisle.mapper.ReactionMapper;
|
||||
import com.openisle.dto.MessageNotificationPayload;
|
||||
import com.openisle.model.Message;
|
||||
import com.openisle.model.MessageConversation;
|
||||
import com.openisle.model.MessageParticipant;
|
||||
import com.openisle.model.Reaction;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.MessageConversationRepository;
|
||||
import com.openisle.repository.MessageParticipantRepository;
|
||||
import com.openisle.repository.MessageRepository;
|
||||
import com.openisle.repository.ReactionRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Page;
|
||||
@@ -24,326 +29,403 @@ import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class MessageService {
|
||||
|
||||
private final MessageRepository messageRepository;
|
||||
private final MessageConversationRepository conversationRepository;
|
||||
private final MessageParticipantRepository participantRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final NotificationProducer notificationProducer;
|
||||
private final ReactionRepository reactionRepository;
|
||||
private final ReactionMapper reactionMapper;
|
||||
private final MessageRepository messageRepository;
|
||||
private final MessageConversationRepository conversationRepository;
|
||||
private final MessageParticipantRepository participantRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final NotificationProducer notificationProducer;
|
||||
private final ReactionRepository reactionRepository;
|
||||
private final ReactionMapper reactionMapper;
|
||||
|
||||
@Transactional
|
||||
public Message sendMessage(Long senderId, Long recipientId, String content, Long replyToId) {
|
||||
log.info("Attempting to send message from user {} to user {}", senderId, recipientId);
|
||||
User sender = userRepository.findById(senderId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Sender not found"));
|
||||
User recipient = userRepository.findById(recipientId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Recipient not found"));
|
||||
@Transactional
|
||||
public Message sendMessage(Long senderId, Long recipientId, String content, Long replyToId) {
|
||||
log.info("Attempting to send message from user {} to user {}", senderId, recipientId);
|
||||
User sender = userRepository
|
||||
.findById(senderId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Sender not found"));
|
||||
User recipient = userRepository
|
||||
.findById(recipientId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Recipient not found"));
|
||||
|
||||
log.info("Finding or creating conversation for users {} and {}", sender.getUsername(), recipient.getUsername());
|
||||
MessageConversation conversation = findOrCreateConversation(sender, recipient);
|
||||
log.info("Conversation found or created with ID: {}", conversation.getId());
|
||||
log.info(
|
||||
"Finding or creating conversation for users {} and {}",
|
||||
sender.getUsername(),
|
||||
recipient.getUsername()
|
||||
);
|
||||
MessageConversation conversation = findOrCreateConversation(sender, recipient);
|
||||
log.info("Conversation found or created with ID: {}", conversation.getId());
|
||||
|
||||
Message message = new Message();
|
||||
message.setConversation(conversation);
|
||||
message.setSender(sender);
|
||||
message.setContent(content);
|
||||
if (replyToId != null) {
|
||||
Message replyTo = messageRepository.findById(replyToId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Message not found"));
|
||||
message.setReplyTo(replyTo);
|
||||
}
|
||||
message = messageRepository.save(message);
|
||||
log.info("Message saved with ID: {}", message.getId());
|
||||
Message message = new Message();
|
||||
message.setConversation(conversation);
|
||||
message.setSender(sender);
|
||||
message.setContent(content);
|
||||
if (replyToId != null) {
|
||||
Message replyTo = messageRepository
|
||||
.findById(replyToId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Message not found"));
|
||||
message.setReplyTo(replyTo);
|
||||
}
|
||||
message = messageRepository.save(message);
|
||||
log.info("Message saved with ID: {}", message.getId());
|
||||
|
||||
conversation.setLastMessage(message);
|
||||
conversationRepository.save(conversation);
|
||||
log.info("Conversation {} updated with last message ID {}", conversation.getId(), message.getId());
|
||||
conversation.setLastMessage(message);
|
||||
conversationRepository.save(conversation);
|
||||
log.info(
|
||||
"Conversation {} updated with last message ID {}",
|
||||
conversation.getId(),
|
||||
message.getId()
|
||||
);
|
||||
|
||||
|
||||
try {
|
||||
MessageDto messageDto = toDto(message);
|
||||
|
||||
long unreadCount = getUnreadMessageCount(recipientId);
|
||||
try {
|
||||
MessageDto messageDto = toDto(message);
|
||||
|
||||
// 创建包含对话和参与者信息的完整payload
|
||||
Map<String, Object> conversationInfo = new HashMap<>();
|
||||
conversationInfo.put("id", conversation.getId());
|
||||
conversationInfo.put("participants", conversation.getParticipants().stream()
|
||||
.map(p -> {
|
||||
Map<String, Object> participantInfo = new HashMap<>();
|
||||
participantInfo.put("userId", p.getUser().getId());
|
||||
participantInfo.put("username", p.getUser().getUsername());
|
||||
return participantInfo;
|
||||
}).collect(Collectors.toList()));
|
||||
|
||||
Map<String, Object> combinedPayload = new HashMap<>();
|
||||
combinedPayload.put("message", messageDto);
|
||||
combinedPayload.put("unreadCount", unreadCount);
|
||||
combinedPayload.put("conversation", conversationInfo);
|
||||
combinedPayload.put("senderId", senderId);
|
||||
if (notificationProducer != null) {
|
||||
log.info("NotificationProducer is available");
|
||||
} else {
|
||||
log.info("ERROR: NotificationProducer is NULL!");
|
||||
return message;
|
||||
}
|
||||
log.info("Recipient username: {}", recipient.getUsername());
|
||||
|
||||
notificationProducer.sendNotification(new MessageNotificationPayload(recipient.getUsername(), combinedPayload));
|
||||
log.info("=== Notification call completed ===");
|
||||
} catch (Exception e) {
|
||||
log.error("=== Error in notification process ===", e);
|
||||
}
|
||||
long unreadCount = getUnreadMessageCount(recipientId);
|
||||
|
||||
// 创建包含对话和参与者信息的完整payload
|
||||
Map<String, Object> conversationInfo = new HashMap<>();
|
||||
conversationInfo.put("id", conversation.getId());
|
||||
conversationInfo.put(
|
||||
"participants",
|
||||
conversation
|
||||
.getParticipants()
|
||||
.stream()
|
||||
.map(p -> {
|
||||
Map<String, Object> participantInfo = new HashMap<>();
|
||||
participantInfo.put("userId", p.getUser().getId());
|
||||
participantInfo.put("username", p.getUser().getUsername());
|
||||
return participantInfo;
|
||||
})
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
|
||||
Map<String, Object> combinedPayload = new HashMap<>();
|
||||
combinedPayload.put("message", messageDto);
|
||||
combinedPayload.put("unreadCount", unreadCount);
|
||||
combinedPayload.put("conversation", conversationInfo);
|
||||
combinedPayload.put("senderId", senderId);
|
||||
if (notificationProducer != null) {
|
||||
log.info("NotificationProducer is available");
|
||||
} else {
|
||||
log.info("ERROR: NotificationProducer is NULL!");
|
||||
return message;
|
||||
}
|
||||
log.info("Recipient username: {}", recipient.getUsername());
|
||||
|
||||
notificationProducer.sendNotification(
|
||||
new MessageNotificationPayload(recipient.getUsername(), combinedPayload)
|
||||
);
|
||||
log.info("=== Notification call completed ===");
|
||||
} catch (Exception e) {
|
||||
log.error("=== Error in notification process ===", e);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Message sendMessageToConversation(Long senderId, Long conversationId, String content, Long replyToId) {
|
||||
User sender = userRepository.findById(senderId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Sender not found"));
|
||||
MessageConversation conversation = conversationRepository.findByIdWithParticipantsAndUsers(conversationId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Conversation not found"));
|
||||
return message;
|
||||
}
|
||||
|
||||
// Join the conversation if not already a participant (useful for channels)
|
||||
participantRepository.findByConversationIdAndUserId(conversationId, senderId)
|
||||
.orElseGet(() -> {
|
||||
MessageParticipant p = new MessageParticipant();
|
||||
p.setConversation(conversation);
|
||||
p.setUser(sender);
|
||||
return participantRepository.save(p);
|
||||
});
|
||||
@Transactional
|
||||
public Message sendMessageToConversation(
|
||||
Long senderId,
|
||||
Long conversationId,
|
||||
String content,
|
||||
Long replyToId
|
||||
) {
|
||||
User sender = userRepository
|
||||
.findById(senderId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Sender not found"));
|
||||
MessageConversation conversation = conversationRepository
|
||||
.findByIdWithParticipantsAndUsers(conversationId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Conversation not found"));
|
||||
|
||||
Message message = new Message();
|
||||
message.setConversation(conversation);
|
||||
message.setSender(sender);
|
||||
message.setContent(content);
|
||||
if (replyToId != null) {
|
||||
Message replyTo = messageRepository.findById(replyToId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Message not found"));
|
||||
message.setReplyTo(replyTo);
|
||||
}
|
||||
message = messageRepository.save(message);
|
||||
// Join the conversation if not already a participant (useful for channels)
|
||||
participantRepository
|
||||
.findByConversationIdAndUserId(conversationId, senderId)
|
||||
.orElseGet(() -> {
|
||||
MessageParticipant p = new MessageParticipant();
|
||||
p.setConversation(conversation);
|
||||
p.setUser(sender);
|
||||
return participantRepository.save(p);
|
||||
});
|
||||
|
||||
conversation.setLastMessage(message);
|
||||
conversationRepository.save(conversation);
|
||||
Message message = new Message();
|
||||
message.setConversation(conversation);
|
||||
message.setSender(sender);
|
||||
message.setContent(content);
|
||||
if (replyToId != null) {
|
||||
Message replyTo = messageRepository
|
||||
.findById(replyToId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Message not found"));
|
||||
message.setReplyTo(replyTo);
|
||||
}
|
||||
message = messageRepository.save(message);
|
||||
|
||||
MessageDto messageDto = toDto(message);
|
||||
conversation.setLastMessage(message);
|
||||
conversationRepository.save(conversation);
|
||||
|
||||
// Build participant payloads once to avoid duplicate broadcasts
|
||||
java.util.List<Map<String, Object>> participantInfos = conversation.getParticipants().stream()
|
||||
.filter(p -> !p.getUser().getId().equals(senderId))
|
||||
.map(p -> {
|
||||
Map<String, Object> info = new HashMap<>();
|
||||
info.put("userId", p.getUser().getId());
|
||||
info.put("username", p.getUser().getUsername());
|
||||
info.put("unreadCount", getUnreadMessageCount(p.getUser().getId()));
|
||||
info.put("channelUnread", getUnreadChannelCount(p.getUser().getId()));
|
||||
return info;
|
||||
}).collect(Collectors.toList());
|
||||
MessageDto messageDto = toDto(message);
|
||||
|
||||
Map<String, Object> conversationInfo = new HashMap<>();
|
||||
conversationInfo.put("id", conversation.getId());
|
||||
conversationInfo.put("participants", participantInfos);
|
||||
// Build participant payloads once to avoid duplicate broadcasts
|
||||
java.util.List<Map<String, Object>> participantInfos = conversation
|
||||
.getParticipants()
|
||||
.stream()
|
||||
.filter(p -> !p.getUser().getId().equals(senderId))
|
||||
.map(p -> {
|
||||
Map<String, Object> info = new HashMap<>();
|
||||
info.put("userId", p.getUser().getId());
|
||||
info.put("username", p.getUser().getUsername());
|
||||
info.put("unreadCount", getUnreadMessageCount(p.getUser().getId()));
|
||||
info.put("channelUnread", getUnreadChannelCount(p.getUser().getId()));
|
||||
return info;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> combinedPayload = new HashMap<>();
|
||||
combinedPayload.put("message", messageDto);
|
||||
combinedPayload.put("conversation", conversationInfo);
|
||||
combinedPayload.put("senderId", senderId);
|
||||
Map<String, Object> conversationInfo = new HashMap<>();
|
||||
conversationInfo.put("id", conversation.getId());
|
||||
conversationInfo.put("participants", participantInfos);
|
||||
|
||||
// Use sender's username for sharding; only one notification is needed
|
||||
notificationProducer.sendNotification(new MessageNotificationPayload(sender.getUsername(), combinedPayload));
|
||||
Map<String, Object> combinedPayload = new HashMap<>();
|
||||
combinedPayload.put("message", messageDto);
|
||||
combinedPayload.put("conversation", conversationInfo);
|
||||
combinedPayload.put("senderId", senderId);
|
||||
|
||||
return message;
|
||||
// Use sender's username for sharding; only one notification is needed
|
||||
notificationProducer.sendNotification(
|
||||
new MessageNotificationPayload(sender.getUsername(), combinedPayload)
|
||||
);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public MessageDto toDto(Message message) {
|
||||
MessageDto dto = new MessageDto();
|
||||
dto.setId(message.getId());
|
||||
dto.setContent(message.getContent());
|
||||
dto.setConversationId(message.getConversation().getId());
|
||||
dto.setCreatedAt(message.getCreatedAt());
|
||||
|
||||
UserSummaryDto userSummaryDto = new UserSummaryDto();
|
||||
userSummaryDto.setId(message.getSender().getId());
|
||||
userSummaryDto.setUsername(message.getSender().getUsername());
|
||||
userSummaryDto.setAvatar(message.getSender().getAvatar());
|
||||
dto.setSender(userSummaryDto);
|
||||
|
||||
if (message.getReplyTo() != null) {
|
||||
Message reply = message.getReplyTo();
|
||||
MessageDto replyDto = new MessageDto();
|
||||
replyDto.setId(reply.getId());
|
||||
replyDto.setContent(reply.getContent());
|
||||
UserSummaryDto replySender = new UserSummaryDto();
|
||||
replySender.setId(reply.getSender().getId());
|
||||
replySender.setUsername(reply.getSender().getUsername());
|
||||
replySender.setAvatar(reply.getSender().getAvatar());
|
||||
replyDto.setSender(replySender);
|
||||
dto.setReplyTo(replyDto);
|
||||
}
|
||||
|
||||
public MessageDto toDto(Message message) {
|
||||
MessageDto dto = new MessageDto();
|
||||
dto.setId(message.getId());
|
||||
dto.setContent(message.getContent());
|
||||
dto.setConversationId(message.getConversation().getId());
|
||||
dto.setCreatedAt(message.getCreatedAt());
|
||||
java.util.List<Reaction> reactions = reactionRepository.findByMessage(message);
|
||||
java.util.List<ReactionDto> reactionDtos = reactions
|
||||
.stream()
|
||||
.map(reactionMapper::toDto)
|
||||
.collect(Collectors.toList());
|
||||
dto.setReactions(reactionDtos);
|
||||
|
||||
UserSummaryDto userSummaryDto = new UserSummaryDto();
|
||||
userSummaryDto.setId(message.getSender().getId());
|
||||
userSummaryDto.setUsername(message.getSender().getUsername());
|
||||
userSummaryDto.setAvatar(message.getSender().getAvatar());
|
||||
dto.setSender(userSummaryDto);
|
||||
return dto;
|
||||
}
|
||||
|
||||
if (message.getReplyTo() != null) {
|
||||
Message reply = message.getReplyTo();
|
||||
MessageDto replyDto = new MessageDto();
|
||||
replyDto.setId(reply.getId());
|
||||
replyDto.setContent(reply.getContent());
|
||||
UserSummaryDto replySender = new UserSummaryDto();
|
||||
replySender.setId(reply.getSender().getId());
|
||||
replySender.setUsername(reply.getSender().getUsername());
|
||||
replySender.setAvatar(reply.getSender().getAvatar());
|
||||
replyDto.setSender(replySender);
|
||||
dto.setReplyTo(replyDto);
|
||||
}
|
||||
public MessageConversation findOrCreateConversation(Long user1Id, Long user2Id) {
|
||||
User user1 = userRepository
|
||||
.findById(user1Id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("User1 not found"));
|
||||
User user2 = userRepository
|
||||
.findById(user2Id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("User2 not found"));
|
||||
return findOrCreateConversation(user1, user2);
|
||||
}
|
||||
|
||||
java.util.List<Reaction> reactions = reactionRepository.findByMessage(message);
|
||||
java.util.List<ReactionDto> reactionDtos = reactions.stream()
|
||||
.map(reactionMapper::toDto)
|
||||
.collect(Collectors.toList());
|
||||
dto.setReactions(reactionDtos);
|
||||
private MessageConversation findOrCreateConversation(User user1, User user2) {
|
||||
log.info(
|
||||
"Searching for existing conversation between {} and {}",
|
||||
user1.getUsername(),
|
||||
user2.getUsername()
|
||||
);
|
||||
return conversationRepository
|
||||
.findConversationsByUsers(user1, user2)
|
||||
.stream()
|
||||
.findFirst()
|
||||
.orElseGet(() -> {
|
||||
log.info("No existing conversation found. Creating a new one.");
|
||||
MessageConversation conversation = new MessageConversation();
|
||||
conversation = conversationRepository.save(conversation);
|
||||
log.info("New conversation created with ID: {}", conversation.getId());
|
||||
|
||||
return dto;
|
||||
MessageParticipant participant1 = new MessageParticipant();
|
||||
participant1.setConversation(conversation);
|
||||
participant1.setUser(user1);
|
||||
participantRepository.save(participant1);
|
||||
log.info(
|
||||
"Participant {} added to conversation {}",
|
||||
user1.getUsername(),
|
||||
conversation.getId()
|
||||
);
|
||||
|
||||
MessageParticipant participant2 = new MessageParticipant();
|
||||
participant2.setConversation(conversation);
|
||||
participant2.setUser(user2);
|
||||
participantRepository.save(participant2);
|
||||
log.info(
|
||||
"Participant {} added to conversation {}",
|
||||
user2.getUsername(),
|
||||
conversation.getId()
|
||||
);
|
||||
|
||||
return conversation;
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ConversationDto> getConversations(Long userId) {
|
||||
List<MessageConversation> conversations =
|
||||
conversationRepository.findConversationsByUserIdOrderByLastMessageDesc(userId);
|
||||
return conversations
|
||||
.stream()
|
||||
.filter(c -> !c.isChannel())
|
||||
.map(c -> toDto(c, userId))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private ConversationDto toDto(MessageConversation conversation, Long userId) {
|
||||
ConversationDto dto = new ConversationDto();
|
||||
dto.setId(conversation.getId());
|
||||
dto.setChannel(conversation.isChannel());
|
||||
dto.setName(conversation.getName());
|
||||
dto.setAvatar(conversation.getAvatar());
|
||||
dto.setCreatedAt(conversation.getCreatedAt());
|
||||
if (conversation.getLastMessage() != null) {
|
||||
dto.setLastMessage(toDto(conversation.getLastMessage()));
|
||||
}
|
||||
dto.setParticipants(
|
||||
conversation
|
||||
.getParticipants()
|
||||
.stream()
|
||||
.map(p -> {
|
||||
UserSummaryDto userDto = new UserSummaryDto();
|
||||
userDto.setId(p.getUser().getId());
|
||||
userDto.setUsername(p.getUser().getUsername());
|
||||
userDto.setAvatar(p.getUser().getAvatar());
|
||||
return userDto;
|
||||
})
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
|
||||
public MessageConversation findOrCreateConversation(Long user1Id, Long user2Id) {
|
||||
User user1 = userRepository.findById(user1Id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("User1 not found"));
|
||||
User user2 = userRepository.findById(user2Id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("User2 not found"));
|
||||
return findOrCreateConversation(user1, user2);
|
||||
MessageParticipant self = conversation
|
||||
.getParticipants()
|
||||
.stream()
|
||||
.filter(p -> p.getUser().getId().equals(userId))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("Participant not found in conversation"));
|
||||
|
||||
LocalDateTime lastRead = self.getLastReadAt() == null
|
||||
? LocalDateTime.of(1970, 1, 1, 0, 0)
|
||||
: self.getLastReadAt();
|
||||
// 只计算别人发送给当前用户的未读消息
|
||||
long unreadCount = messageRepository.countByConversationIdAndCreatedAtAfterAndSenderIdNot(
|
||||
conversation.getId(),
|
||||
lastRead,
|
||||
userId
|
||||
);
|
||||
dto.setUnreadCount(unreadCount);
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ConversationDetailDto getConversationDetails(
|
||||
Long conversationId,
|
||||
Long userId,
|
||||
Pageable pageable
|
||||
) {
|
||||
markConversationAsRead(conversationId, userId);
|
||||
|
||||
MessageConversation conversation = conversationRepository
|
||||
.findById(conversationId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Conversation not found"));
|
||||
|
||||
Page<Message> messagesPage = messageRepository.findByConversationId(conversationId, pageable);
|
||||
Page<MessageDto> messageDtoPage = messagesPage.map(this::toDto);
|
||||
|
||||
List<UserSummaryDto> participants = conversation
|
||||
.getParticipants()
|
||||
.stream()
|
||||
.map(p -> {
|
||||
UserSummaryDto userDto = new UserSummaryDto();
|
||||
userDto.setId(p.getUser().getId());
|
||||
userDto.setUsername(p.getUser().getUsername());
|
||||
userDto.setAvatar(p.getUser().getAvatar());
|
||||
return userDto;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
ConversationDetailDto detailDto = new ConversationDetailDto();
|
||||
detailDto.setId(conversation.getId());
|
||||
detailDto.setName(conversation.getName());
|
||||
detailDto.setChannel(conversation.isChannel());
|
||||
detailDto.setAvatar(conversation.getAvatar());
|
||||
detailDto.setParticipants(participants);
|
||||
detailDto.setMessages(messageDtoPage);
|
||||
|
||||
return detailDto;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void markConversationAsRead(Long conversationId, Long userId) {
|
||||
MessageParticipant participant = participantRepository
|
||||
.findByConversationIdAndUserId(conversationId, userId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Participant not found"));
|
||||
participant.setLastReadAt(LocalDateTime.now());
|
||||
participantRepository.save(participant);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public long getUnreadMessageCount(Long userId) {
|
||||
List<MessageParticipant> participations = participantRepository.findByUserId(userId);
|
||||
long totalUnreadCount = 0;
|
||||
for (MessageParticipant p : participations) {
|
||||
if (p.getConversation().isChannel()) continue;
|
||||
LocalDateTime lastRead = p.getLastReadAt() == null
|
||||
? LocalDateTime.of(1970, 1, 1, 0, 0)
|
||||
: p.getLastReadAt();
|
||||
// 只计算别人发送给当前用户的未读消息
|
||||
totalUnreadCount += messageRepository.countByConversationIdAndCreatedAtAfterAndSenderIdNot(
|
||||
p.getConversation().getId(),
|
||||
lastRead,
|
||||
userId
|
||||
);
|
||||
}
|
||||
return totalUnreadCount;
|
||||
}
|
||||
|
||||
private MessageConversation findOrCreateConversation(User user1, User user2) {
|
||||
log.info("Searching for existing conversation between {} and {}", user1.getUsername(), user2.getUsername());
|
||||
return conversationRepository.findConversationsByUsers(user1, user2).stream()
|
||||
.findFirst()
|
||||
.orElseGet(() -> {
|
||||
log.info("No existing conversation found. Creating a new one.");
|
||||
MessageConversation conversation = new MessageConversation();
|
||||
conversation = conversationRepository.save(conversation);
|
||||
log.info("New conversation created with ID: {}", conversation.getId());
|
||||
|
||||
MessageParticipant participant1 = new MessageParticipant();
|
||||
participant1.setConversation(conversation);
|
||||
participant1.setUser(user1);
|
||||
participantRepository.save(participant1);
|
||||
log.info("Participant {} added to conversation {}", user1.getUsername(), conversation.getId());
|
||||
|
||||
MessageParticipant participant2 = new MessageParticipant();
|
||||
participant2.setConversation(conversation);
|
||||
participant2.setUser(user2);
|
||||
participantRepository.save(participant2);
|
||||
log.info("Participant {} added to conversation {}", user2.getUsername(), conversation.getId());
|
||||
|
||||
return conversation;
|
||||
});
|
||||
@Transactional(readOnly = true)
|
||||
public long getUnreadChannelCount(Long userId) {
|
||||
List<MessageParticipant> participations = participantRepository.findByUserId(userId);
|
||||
long unreadChannelCount = 0;
|
||||
for (MessageParticipant p : participations) {
|
||||
if (!p.getConversation().isChannel()) continue;
|
||||
LocalDateTime lastRead = p.getLastReadAt() == null
|
||||
? LocalDateTime.of(1970, 1, 1, 0, 0)
|
||||
: p.getLastReadAt();
|
||||
long unread = messageRepository.countByConversationIdAndCreatedAtAfterAndSenderIdNot(
|
||||
p.getConversation().getId(),
|
||||
lastRead,
|
||||
userId
|
||||
);
|
||||
if (unread > 0) {
|
||||
unreadChannelCount++;
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ConversationDto> getConversations(Long userId) {
|
||||
List<MessageConversation> conversations = conversationRepository.findConversationsByUserIdOrderByLastMessageDesc(userId);
|
||||
return conversations.stream()
|
||||
.filter(c -> !c.isChannel())
|
||||
.map(c -> toDto(c, userId))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private ConversationDto toDto(MessageConversation conversation, Long userId) {
|
||||
ConversationDto dto = new ConversationDto();
|
||||
dto.setId(conversation.getId());
|
||||
dto.setChannel(conversation.isChannel());
|
||||
dto.setName(conversation.getName());
|
||||
dto.setAvatar(conversation.getAvatar());
|
||||
dto.setCreatedAt(conversation.getCreatedAt());
|
||||
if (conversation.getLastMessage() != null) {
|
||||
dto.setLastMessage(toDto(conversation.getLastMessage()));
|
||||
}
|
||||
dto.setParticipants(conversation.getParticipants().stream()
|
||||
.map(p -> {
|
||||
UserSummaryDto userDto = new UserSummaryDto();
|
||||
userDto.setId(p.getUser().getId());
|
||||
userDto.setUsername(p.getUser().getUsername());
|
||||
userDto.setAvatar(p.getUser().getAvatar());
|
||||
return userDto;
|
||||
})
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
MessageParticipant self = conversation.getParticipants().stream()
|
||||
.filter(p -> p.getUser().getId().equals(userId))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("Participant not found in conversation"));
|
||||
|
||||
LocalDateTime lastRead = self.getLastReadAt() == null ? LocalDateTime.of(1970, 1, 1, 0, 0) : self.getLastReadAt();
|
||||
// 只计算别人发送给当前用户的未读消息
|
||||
long unreadCount = messageRepository.countByConversationIdAndCreatedAtAfterAndSenderIdNot(conversation.getId(), lastRead, userId);
|
||||
dto.setUnreadCount(unreadCount);
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ConversationDetailDto getConversationDetails(Long conversationId, Long userId, Pageable pageable) {
|
||||
markConversationAsRead(conversationId, userId);
|
||||
|
||||
MessageConversation conversation = conversationRepository.findById(conversationId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Conversation not found"));
|
||||
|
||||
Page<Message> messagesPage = messageRepository.findByConversationId(conversationId, pageable);
|
||||
Page<MessageDto> messageDtoPage = messagesPage.map(this::toDto);
|
||||
|
||||
List<UserSummaryDto> participants = conversation.getParticipants().stream()
|
||||
.map(p -> {
|
||||
UserSummaryDto userDto = new UserSummaryDto();
|
||||
userDto.setId(p.getUser().getId());
|
||||
userDto.setUsername(p.getUser().getUsername());
|
||||
userDto.setAvatar(p.getUser().getAvatar());
|
||||
return userDto;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
ConversationDetailDto detailDto = new ConversationDetailDto();
|
||||
detailDto.setId(conversation.getId());
|
||||
detailDto.setName(conversation.getName());
|
||||
detailDto.setChannel(conversation.isChannel());
|
||||
detailDto.setAvatar(conversation.getAvatar());
|
||||
detailDto.setParticipants(participants);
|
||||
detailDto.setMessages(messageDtoPage);
|
||||
|
||||
return detailDto;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void markConversationAsRead(Long conversationId, Long userId) {
|
||||
MessageParticipant participant = participantRepository.findByConversationIdAndUserId(conversationId, userId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Participant not found"));
|
||||
participant.setLastReadAt(LocalDateTime.now());
|
||||
participantRepository.save(participant);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public long getUnreadMessageCount(Long userId) {
|
||||
List<MessageParticipant> participations = participantRepository.findByUserId(userId);
|
||||
long totalUnreadCount = 0;
|
||||
for (MessageParticipant p : participations) {
|
||||
if (p.getConversation().isChannel()) continue;
|
||||
LocalDateTime lastRead = p.getLastReadAt() == null ? LocalDateTime.of(1970, 1, 1, 0, 0) : p.getLastReadAt();
|
||||
// 只计算别人发送给当前用户的未读消息
|
||||
totalUnreadCount += messageRepository.countByConversationIdAndCreatedAtAfterAndSenderIdNot(p.getConversation().getId(), lastRead, userId);
|
||||
}
|
||||
return totalUnreadCount;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public long getUnreadChannelCount(Long userId) {
|
||||
List<MessageParticipant> participations = participantRepository.findByUserId(userId);
|
||||
long unreadChannelCount = 0;
|
||||
for (MessageParticipant p : participations) {
|
||||
if (!p.getConversation().isChannel()) continue;
|
||||
LocalDateTime lastRead = p.getLastReadAt() == null ? LocalDateTime.of(1970, 1, 1, 0, 0) : p.getLastReadAt();
|
||||
long unread = messageRepository.countByConversationIdAndCreatedAtAfterAndSenderIdNot(p.getConversation().getId(), lastRead, userId);
|
||||
if (unread > 0) {
|
||||
unreadChannelCount++;
|
||||
}
|
||||
}
|
||||
return unreadChannelCount;
|
||||
}
|
||||
}
|
||||
return unreadChannelCount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,49 +15,45 @@ import org.springframework.stereotype.Service;
|
||||
@Slf4j
|
||||
public class NotificationProducer {
|
||||
|
||||
private final RabbitTemplate rabbitTemplate;
|
||||
private final ShardingStrategy shardingStrategy;
|
||||
|
||||
@Value("${rabbitmq.sharding.enabled}")
|
||||
private boolean shardingEnabled;
|
||||
private final RabbitTemplate rabbitTemplate;
|
||||
private final ShardingStrategy shardingStrategy;
|
||||
|
||||
public void sendNotification(MessageNotificationPayload payload) {
|
||||
String targetUsername = payload.getTargetUsername();
|
||||
|
||||
try {
|
||||
if (shardingEnabled) {
|
||||
// 使用分片策略发送消息
|
||||
sendShardedNotification(payload, targetUsername);
|
||||
} else {
|
||||
// 使用原始单队列方式发送(向后兼容)
|
||||
sendLegacyNotification(payload);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send message to RabbitMQ for user: {}", targetUsername, e);
|
||||
throw e;
|
||||
}
|
||||
@Value("${rabbitmq.sharding.enabled}")
|
||||
private boolean shardingEnabled;
|
||||
|
||||
public void sendNotification(MessageNotificationPayload payload) {
|
||||
String targetUsername = payload.getTargetUsername();
|
||||
|
||||
try {
|
||||
if (shardingEnabled) {
|
||||
// 使用分片策略发送消息
|
||||
sendShardedNotification(payload, targetUsername);
|
||||
} else {
|
||||
// 使用原始单队列方式发送(向后兼容)
|
||||
sendLegacyNotification(payload);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send message to RabbitMQ for user: {}", targetUsername, e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用分片策略发送消息
|
||||
*/
|
||||
private void sendShardedNotification(MessageNotificationPayload payload, String targetUsername) {
|
||||
ShardInfo shardInfo = shardingStrategy.getShardInfo(targetUsername);
|
||||
rabbitTemplate.convertAndSend(
|
||||
RabbitMQConfig.EXCHANGE_NAME,
|
||||
shardInfo.getRoutingKey(),
|
||||
payload
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用原始单队列方式发送消息(向后兼容)
|
||||
*/
|
||||
private void sendLegacyNotification(MessageNotificationPayload payload) {
|
||||
rabbitTemplate.convertAndSend(
|
||||
RabbitMQConfig.EXCHANGE_NAME,
|
||||
RabbitMQConfig.ROUTING_KEY,
|
||||
payload
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用分片策略发送消息
|
||||
*/
|
||||
private void sendShardedNotification(MessageNotificationPayload payload, String targetUsername) {
|
||||
ShardInfo shardInfo = shardingStrategy.getShardInfo(targetUsername);
|
||||
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, shardInfo.getRoutingKey(), payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用原始单队列方式发送消息(向后兼容)
|
||||
*/
|
||||
private void sendLegacyNotification(MessageNotificationPayload payload) {
|
||||
rabbitTemplate.convertAndSend(
|
||||
RabbitMQConfig.EXCHANGE_NAME,
|
||||
RabbitMQConfig.ROUTING_KEY,
|
||||
payload
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,288 +1,378 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import com.openisle.model.*;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.openisle.dto.NotificationPreferenceDto;
|
||||
import com.openisle.model.*;
|
||||
import com.openisle.repository.NotificationRepository;
|
||||
import com.openisle.repository.ReactionRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import com.openisle.service.EmailSender;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import java.util.Map;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
import java.util.EnumSet;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/** Service for creating and retrieving notifications. */
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class NotificationService {
|
||||
private final NotificationRepository notificationRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final EmailSender emailSender;
|
||||
private final PushNotificationService pushNotificationService;
|
||||
private final ReactionRepository reactionRepository;
|
||||
private final Executor notificationExecutor;
|
||||
|
||||
@Value("${app.website-url}")
|
||||
private String websiteUrl;
|
||||
private final NotificationRepository notificationRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final EmailSender emailSender;
|
||||
private final PushNotificationService pushNotificationService;
|
||||
private final ReactionRepository reactionRepository;
|
||||
private final Executor notificationExecutor;
|
||||
|
||||
private static final Pattern MENTION_PATTERN = Pattern.compile("@\\[([^\\]]+)\\]");
|
||||
@Value("${app.website-url}")
|
||||
private String websiteUrl;
|
||||
|
||||
private static final Set<NotificationType> EMAIL_TYPES = EnumSet.of(
|
||||
NotificationType.COMMENT_REPLY,
|
||||
NotificationType.LOTTERY_WIN,
|
||||
NotificationType.LOTTERY_DRAW
|
||||
);
|
||||
private static final Pattern MENTION_PATTERN = Pattern.compile("@\\[([^\\]]+)\\]");
|
||||
|
||||
private String buildPayload(String body, String url) {
|
||||
// Ensure push notifications contain a link to the related resource so
|
||||
// that verifications can assert its presence and users can navigate
|
||||
// directly from the notification.
|
||||
if (url == null || url.isBlank()) {
|
||||
return body;
|
||||
}
|
||||
return body + ", 点击以查看: " + url;
|
||||
private static final Set<NotificationType> EMAIL_TYPES = EnumSet.of(
|
||||
NotificationType.COMMENT_REPLY,
|
||||
NotificationType.LOTTERY_WIN,
|
||||
NotificationType.LOTTERY_DRAW
|
||||
);
|
||||
|
||||
private String buildPayload(String body, String url) {
|
||||
// Ensure push notifications contain a link to the related resource so
|
||||
// that verifications can assert its presence and users can navigate
|
||||
// directly from the notification.
|
||||
if (url == null || url.isBlank()) {
|
||||
return body;
|
||||
}
|
||||
return body + ", 点击以查看: " + url;
|
||||
}
|
||||
|
||||
public void sendCustomPush(User user, String body, String url) {
|
||||
pushNotificationService.sendNotification(user, buildPayload(body, url));
|
||||
public void sendCustomPush(User user, String body, String url) {
|
||||
pushNotificationService.sendNotification(user, buildPayload(body, url));
|
||||
}
|
||||
|
||||
public Notification createNotification(
|
||||
User user,
|
||||
NotificationType type,
|
||||
Post post,
|
||||
Comment comment,
|
||||
Boolean approved
|
||||
) {
|
||||
return createNotification(user, type, post, comment, approved, null, null, null);
|
||||
}
|
||||
|
||||
public Notification createNotification(
|
||||
User user,
|
||||
NotificationType type,
|
||||
Post post,
|
||||
Comment comment,
|
||||
Boolean approved,
|
||||
User fromUser,
|
||||
ReactionType reactionType,
|
||||
String content
|
||||
) {
|
||||
Notification n = new Notification();
|
||||
n.setUser(user);
|
||||
n.setType(type);
|
||||
n.setPost(post);
|
||||
n.setComment(comment);
|
||||
n.setApproved(approved);
|
||||
n.setFromUser(fromUser);
|
||||
n.setReactionType(reactionType);
|
||||
n.setContent(content);
|
||||
if (type == NotificationType.POST_VIEWED && fromUser != null && post != null) {
|
||||
notificationRepository.deleteByTypeAndFromUserAndPost(type, fromUser, post);
|
||||
}
|
||||
n = notificationRepository.save(n);
|
||||
|
||||
public Notification createNotification(User user, NotificationType type, Post post, Comment comment, Boolean approved) {
|
||||
return createNotification(user, type, post, comment, approved, null, null, null);
|
||||
// Runnable asyncTask = () -> {
|
||||
if (
|
||||
type == NotificationType.COMMENT_REPLY &&
|
||||
user.getEmail() != null &&
|
||||
post != null &&
|
||||
comment != null &&
|
||||
!user.getDisabledEmailNotificationTypes().contains(NotificationType.COMMENT_REPLY)
|
||||
) {
|
||||
String url = String.format(
|
||||
"%s/posts/%d#comment-%d",
|
||||
websiteUrl,
|
||||
post.getId(),
|
||||
comment.getId()
|
||||
);
|
||||
emailSender.sendEmail(user.getEmail(), "有人回复了你", url);
|
||||
sendCustomPush(user, "有人回复了你", url);
|
||||
} else if (type == NotificationType.REACTION && comment != null) {
|
||||
// long count = reactionRepository.countReceived(comment.getAuthor().getUsername());
|
||||
// if (count % 5 == 0) {
|
||||
// String url = websiteUrl + "/messages";
|
||||
// sendCustomPush(comment.getAuthor(), "你有新的互动", url);
|
||||
// if (comment.getAuthor().getEmail() != null) {
|
||||
// emailSender.sendEmail(comment.getAuthor().getEmail(), "你有新的互动", "你有新的互动, 点击以查看: " + url);
|
||||
// }
|
||||
// }
|
||||
} else if (type == NotificationType.REACTION && post != null) {
|
||||
// long count = reactionRepository.countReceived(post.getAuthor().getUsername());
|
||||
// if (count % 5 == 0) {
|
||||
// String url = websiteUrl + "/messages";
|
||||
// sendCustomPush(post.getAuthor(), "你有新的互动", url);
|
||||
// if (post.getAuthor().getEmail() != null) {
|
||||
// emailSender.sendEmail(post.getAuthor().getEmail(), "你有新的互动", "你有新的互动, 点击以查看: " + url);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
// };
|
||||
|
||||
public Notification createNotification(User user, NotificationType type, Post post, Comment comment, Boolean approved,
|
||||
User fromUser, ReactionType reactionType, String content) {
|
||||
Notification n = new Notification();
|
||||
n.setUser(user);
|
||||
n.setType(type);
|
||||
n.setPost(post);
|
||||
n.setComment(comment);
|
||||
n.setApproved(approved);
|
||||
n.setFromUser(fromUser);
|
||||
n.setReactionType(reactionType);
|
||||
n.setContent(content);
|
||||
if (type == NotificationType.POST_VIEWED && fromUser != null && post != null) {
|
||||
notificationRepository.deleteByTypeAndFromUserAndPost(type, fromUser, post);
|
||||
}
|
||||
n = notificationRepository.save(n);
|
||||
// if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
// TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
// @Override
|
||||
// public void afterCommit() {
|
||||
// notificationExecutor.execute(asyncTask);
|
||||
// }
|
||||
// });
|
||||
// } else {
|
||||
// notificationExecutor.execute(asyncTask);
|
||||
// }
|
||||
|
||||
// Runnable asyncTask = () -> {
|
||||
if (type == NotificationType.COMMENT_REPLY && user.getEmail() != null && post != null && comment != null
|
||||
&& !user.getDisabledEmailNotificationTypes().contains(NotificationType.COMMENT_REPLY)) {
|
||||
String url = String.format("%s/posts/%d#comment-%d", websiteUrl, post.getId(), comment.getId());
|
||||
emailSender.sendEmail(user.getEmail(), "有人回复了你", url);
|
||||
sendCustomPush(user, "有人回复了你", url);
|
||||
} else if (type == NotificationType.REACTION && comment != null) {
|
||||
// long count = reactionRepository.countReceived(comment.getAuthor().getUsername());
|
||||
// if (count % 5 == 0) {
|
||||
// String url = websiteUrl + "/messages";
|
||||
// sendCustomPush(comment.getAuthor(), "你有新的互动", url);
|
||||
// if (comment.getAuthor().getEmail() != null) {
|
||||
// emailSender.sendEmail(comment.getAuthor().getEmail(), "你有新的互动", "你有新的互动, 点击以查看: " + url);
|
||||
// }
|
||||
// }
|
||||
} else if (type == NotificationType.REACTION && post != null) {
|
||||
// long count = reactionRepository.countReceived(post.getAuthor().getUsername());
|
||||
// if (count % 5 == 0) {
|
||||
// String url = websiteUrl + "/messages";
|
||||
// sendCustomPush(post.getAuthor(), "你有新的互动", url);
|
||||
// if (post.getAuthor().getEmail() != null) {
|
||||
// emailSender.sendEmail(post.getAuthor().getEmail(), "你有新的互动", "你有新的互动, 点击以查看: " + url);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
// };
|
||||
return n;
|
||||
}
|
||||
|
||||
// if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
// TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
// @Override
|
||||
// public void afterCommit() {
|
||||
// notificationExecutor.execute(asyncTask);
|
||||
// }
|
||||
// });
|
||||
// } else {
|
||||
// notificationExecutor.execute(asyncTask);
|
||||
// }
|
||||
|
||||
return n;
|
||||
public void deleteReactionNotification(
|
||||
User fromUser,
|
||||
Post post,
|
||||
Comment comment,
|
||||
ReactionType reactionType
|
||||
) {
|
||||
if (post != null) {
|
||||
notificationRepository.deleteByTypeAndFromUserAndPostAndReactionType(
|
||||
NotificationType.REACTION,
|
||||
fromUser,
|
||||
post,
|
||||
reactionType
|
||||
);
|
||||
} else if (comment != null) {
|
||||
notificationRepository.deleteByTypeAndFromUserAndCommentAndReactionType(
|
||||
NotificationType.REACTION,
|
||||
fromUser,
|
||||
comment,
|
||||
reactionType
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteReactionNotification(User fromUser, Post post, Comment comment, ReactionType reactionType) {
|
||||
if (post != null) {
|
||||
notificationRepository.deleteByTypeAndFromUserAndPostAndReactionType(NotificationType.REACTION, fromUser, post, reactionType);
|
||||
} else if (comment != null) {
|
||||
notificationRepository.deleteByTypeAndFromUserAndCommentAndReactionType(NotificationType.REACTION, fromUser, comment, reactionType);
|
||||
}
|
||||
/**
|
||||
* Create notifications for all admins when a user submits a register request.
|
||||
* Old register request notifications from the same applicant are removed first.
|
||||
*/
|
||||
@org.springframework.transaction.annotation.Transactional
|
||||
public void createRegisterRequestNotifications(User applicant, String reason) {
|
||||
notificationRepository.deleteByTypeAndFromUser(NotificationType.REGISTER_REQUEST, applicant);
|
||||
for (User admin : userRepository.findByRole(Role.ADMIN)) {
|
||||
createNotification(
|
||||
admin,
|
||||
NotificationType.REGISTER_REQUEST,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
applicant,
|
||||
null,
|
||||
reason
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create notifications for all admins when a user submits a register request.
|
||||
* Old register request notifications from the same applicant are removed first.
|
||||
*/
|
||||
@org.springframework.transaction.annotation.Transactional
|
||||
public void createRegisterRequestNotifications(User applicant, String reason) {
|
||||
notificationRepository.deleteByTypeAndFromUser(NotificationType.REGISTER_REQUEST, applicant);
|
||||
for (User admin : userRepository.findByRole(Role.ADMIN)) {
|
||||
createNotification(admin, NotificationType.REGISTER_REQUEST, null, null,
|
||||
null, applicant, null, reason);
|
||||
}
|
||||
/**
|
||||
* Create notifications for all admins when a user redeems an activity.
|
||||
* Old redeem notifications from the same user are removed first.
|
||||
*/
|
||||
@org.springframework.transaction.annotation.Transactional
|
||||
public void createActivityRedeemNotifications(User user, String content) {
|
||||
notificationRepository.deleteByTypeAndFromUser(NotificationType.ACTIVITY_REDEEM, user);
|
||||
for (User admin : userRepository.findByRole(Role.ADMIN)) {
|
||||
createNotification(
|
||||
admin,
|
||||
NotificationType.ACTIVITY_REDEEM,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
user,
|
||||
null,
|
||||
content
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create notifications for all admins when a user redeems an activity.
|
||||
* Old redeem notifications from the same user are removed first.
|
||||
*/
|
||||
@org.springframework.transaction.annotation.Transactional
|
||||
public void createActivityRedeemNotifications(User user, String content) {
|
||||
notificationRepository.deleteByTypeAndFromUser(NotificationType.ACTIVITY_REDEEM, user);
|
||||
for (User admin : userRepository.findByRole(Role.ADMIN)) {
|
||||
createNotification(admin, NotificationType.ACTIVITY_REDEEM, null, null,
|
||||
null, user, null, content);
|
||||
}
|
||||
/**
|
||||
* Create notifications for all admins when a user redeems a point good.
|
||||
* Old redeem notifications from the same user are removed first.
|
||||
*/
|
||||
@org.springframework.transaction.annotation.Transactional
|
||||
public void createPointRedeemNotifications(User user, String content) {
|
||||
// notificationRepository.deleteByTypeAndFromUser(NotificationType.POINT_REDEEM, user);
|
||||
for (User admin : userRepository.findByRole(Role.ADMIN)) {
|
||||
createNotification(
|
||||
admin,
|
||||
NotificationType.POINT_REDEEM,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
user,
|
||||
null,
|
||||
content
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create notifications for all admins when a user redeems a point good.
|
||||
* Old redeem notifications from the same user are removed first.
|
||||
*/
|
||||
@org.springframework.transaction.annotation.Transactional
|
||||
public void createPointRedeemNotifications(User user, String content) {
|
||||
// notificationRepository.deleteByTypeAndFromUser(NotificationType.POINT_REDEEM, user);
|
||||
for (User admin : userRepository.findByRole(Role.ADMIN)) {
|
||||
createNotification(admin, NotificationType.POINT_REDEEM, null, null,
|
||||
null, user, null, content);
|
||||
}
|
||||
public List<NotificationPreferenceDto> listPreferences(String username) {
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Set<NotificationType> disabled = user.getDisabledNotificationTypes();
|
||||
List<NotificationPreferenceDto> prefs = new ArrayList<>();
|
||||
for (NotificationType nt : NotificationType.values()) {
|
||||
NotificationPreferenceDto dto = new NotificationPreferenceDto();
|
||||
dto.setType(nt);
|
||||
dto.setEnabled(!disabled.contains(nt));
|
||||
prefs.add(dto);
|
||||
}
|
||||
return prefs;
|
||||
}
|
||||
|
||||
public List<NotificationPreferenceDto> listPreferences(String username) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Set<NotificationType> disabled = user.getDisabledNotificationTypes();
|
||||
List<NotificationPreferenceDto> prefs = new ArrayList<>();
|
||||
for (NotificationType nt : NotificationType.values()) {
|
||||
NotificationPreferenceDto dto = new NotificationPreferenceDto();
|
||||
dto.setType(nt);
|
||||
dto.setEnabled(!disabled.contains(nt));
|
||||
prefs.add(dto);
|
||||
}
|
||||
return prefs;
|
||||
public void updatePreference(String username, NotificationType type, boolean enabled) {
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Set<NotificationType> disabled = user.getDisabledNotificationTypes();
|
||||
if (enabled) {
|
||||
disabled.remove(type);
|
||||
} else {
|
||||
disabled.add(type);
|
||||
}
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
public void updatePreference(String username, NotificationType type, boolean enabled) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Set<NotificationType> disabled = user.getDisabledNotificationTypes();
|
||||
if (enabled) {
|
||||
disabled.remove(type);
|
||||
} else {
|
||||
disabled.add(type);
|
||||
}
|
||||
userRepository.save(user);
|
||||
public List<NotificationPreferenceDto> listEmailPreferences(String username) {
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Set<NotificationType> disabled = user.getDisabledEmailNotificationTypes();
|
||||
List<NotificationPreferenceDto> prefs = new ArrayList<>();
|
||||
for (NotificationType nt : EMAIL_TYPES) {
|
||||
NotificationPreferenceDto dto = new NotificationPreferenceDto();
|
||||
dto.setType(nt);
|
||||
dto.setEnabled(!disabled.contains(nt));
|
||||
prefs.add(dto);
|
||||
}
|
||||
return prefs;
|
||||
}
|
||||
|
||||
public List<NotificationPreferenceDto> listEmailPreferences(String username) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Set<NotificationType> disabled = user.getDisabledEmailNotificationTypes();
|
||||
List<NotificationPreferenceDto> prefs = new ArrayList<>();
|
||||
for (NotificationType nt : EMAIL_TYPES) {
|
||||
NotificationPreferenceDto dto = new NotificationPreferenceDto();
|
||||
dto.setType(nt);
|
||||
dto.setEnabled(!disabled.contains(nt));
|
||||
prefs.add(dto);
|
||||
}
|
||||
return prefs;
|
||||
public void updateEmailPreference(String username, NotificationType type, boolean enabled) {
|
||||
if (!EMAIL_TYPES.contains(type)) {
|
||||
return;
|
||||
}
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Set<NotificationType> disabled = user.getDisabledEmailNotificationTypes();
|
||||
if (enabled) {
|
||||
disabled.remove(type);
|
||||
} else {
|
||||
disabled.add(type);
|
||||
}
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
public void updateEmailPreference(String username, NotificationType type, boolean enabled) {
|
||||
if (!EMAIL_TYPES.contains(type)) {
|
||||
return;
|
||||
}
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Set<NotificationType> disabled = user.getDisabledEmailNotificationTypes();
|
||||
if (enabled) {
|
||||
disabled.remove(type);
|
||||
} else {
|
||||
disabled.add(type);
|
||||
}
|
||||
userRepository.save(user);
|
||||
public List<Notification> listNotifications(String username, Boolean read, int page, int size) {
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Set<NotificationType> disabled = user.getDisabledNotificationTypes();
|
||||
org.springframework.data.domain.Pageable pageable =
|
||||
org.springframework.data.domain.PageRequest.of(page, size);
|
||||
org.springframework.data.domain.Page<Notification> result;
|
||||
if (read == null) {
|
||||
if (disabled.isEmpty()) {
|
||||
result = notificationRepository.findByUserOrderByCreatedAtDesc(user, pageable);
|
||||
} else {
|
||||
result = notificationRepository.findByUserAndTypeNotInOrderByCreatedAtDesc(
|
||||
user,
|
||||
disabled,
|
||||
pageable
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (disabled.isEmpty()) {
|
||||
result = notificationRepository.findByUserAndReadOrderByCreatedAtDesc(user, read, pageable);
|
||||
} else {
|
||||
result = notificationRepository.findByUserAndReadAndTypeNotInOrderByCreatedAtDesc(
|
||||
user,
|
||||
read,
|
||||
disabled,
|
||||
pageable
|
||||
);
|
||||
}
|
||||
}
|
||||
return result.getContent();
|
||||
}
|
||||
|
||||
public List<Notification> listNotifications(String username, Boolean read, int page, int size) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Set<NotificationType> disabled = user.getDisabledNotificationTypes();
|
||||
org.springframework.data.domain.Pageable pageable = org.springframework.data.domain.PageRequest.of(page, size);
|
||||
org.springframework.data.domain.Page<Notification> result;
|
||||
if (read == null) {
|
||||
if (disabled.isEmpty()) {
|
||||
result = notificationRepository.findByUserOrderByCreatedAtDesc(user, pageable);
|
||||
} else {
|
||||
result = notificationRepository.findByUserAndTypeNotInOrderByCreatedAtDesc(user, disabled, pageable);
|
||||
}
|
||||
} else {
|
||||
if (disabled.isEmpty()) {
|
||||
result = notificationRepository.findByUserAndReadOrderByCreatedAtDesc(user, read, pageable);
|
||||
} else {
|
||||
result = notificationRepository.findByUserAndReadAndTypeNotInOrderByCreatedAtDesc(user, read, disabled, pageable);
|
||||
}
|
||||
}
|
||||
return result.getContent();
|
||||
public void markRead(String username, List<Long> ids) {
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
List<Notification> notifs = notificationRepository.findAllById(ids);
|
||||
for (Notification n : notifs) {
|
||||
if (n.getUser().getId().equals(user.getId())) {
|
||||
n.setRead(true);
|
||||
}
|
||||
}
|
||||
notificationRepository.saveAll(notifs);
|
||||
}
|
||||
|
||||
public void markRead(String username, List<Long> ids) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
List<Notification> notifs = notificationRepository.findAllById(ids);
|
||||
for (Notification n : notifs) {
|
||||
if (n.getUser().getId().equals(user.getId())) {
|
||||
n.setRead(true);
|
||||
}
|
||||
}
|
||||
notificationRepository.saveAll(notifs);
|
||||
public long countUnread(String username) {
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Set<NotificationType> disabled = user.getDisabledNotificationTypes();
|
||||
if (disabled.isEmpty()) {
|
||||
return notificationRepository.countByUserAndRead(user, false);
|
||||
}
|
||||
return notificationRepository.countByUserAndReadAndTypeNotIn(user, false, disabled);
|
||||
}
|
||||
|
||||
public long countUnread(String username) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Set<NotificationType> disabled = user.getDisabledNotificationTypes();
|
||||
if (disabled.isEmpty()) {
|
||||
return notificationRepository.countByUserAndRead(user, false);
|
||||
}
|
||||
return notificationRepository.countByUserAndReadAndTypeNotIn(user, false, disabled);
|
||||
public void notifyMentions(String content, User fromUser, Post post, Comment comment) {
|
||||
if (content == null || fromUser == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
public void notifyMentions(String content, User fromUser, Post post, Comment comment) {
|
||||
if (content == null || fromUser == null) {
|
||||
return;
|
||||
}
|
||||
Matcher matcher = MENTION_PATTERN.matcher(content);
|
||||
Set<String> names = new HashSet<>();
|
||||
while (matcher.find()) {
|
||||
names.add(matcher.group(1));
|
||||
}
|
||||
for (String name : names) {
|
||||
userRepository.findByUsername(name).ifPresent(target -> {
|
||||
if (!target.getId().equals(fromUser.getId())) {
|
||||
createNotification(target, NotificationType.MENTION, post, comment, null, fromUser, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
Matcher matcher = MENTION_PATTERN.matcher(content);
|
||||
Set<String> names = new HashSet<>();
|
||||
while (matcher.find()) {
|
||||
names.add(matcher.group(1));
|
||||
}
|
||||
for (String name : names) {
|
||||
userRepository
|
||||
.findByUsername(name)
|
||||
.ifPresent(target -> {
|
||||
if (!target.getId().equals(fromUser.getId())) {
|
||||
createNotification(
|
||||
target,
|
||||
NotificationType.MENTION,
|
||||
post,
|
||||
comment,
|
||||
null,
|
||||
fromUser,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import java.util.*;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -9,57 +10,56 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
public class OpenAiService {
|
||||
|
||||
@Value("${openai.api-key:}")
|
||||
private String apiKey;
|
||||
@Value("${openai.api-key:}")
|
||||
private String apiKey;
|
||||
|
||||
@Value("${openai.model:gpt-4o}")
|
||||
private String model;
|
||||
@Value("${openai.model:gpt-4o}")
|
||||
private String model;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
public Optional<String> formatMarkdown(String text) {
|
||||
if (apiKey == null || apiKey.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String url = "https://api.openai.com/v1/chat/completions";
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("model", model);
|
||||
List<Map<String, String>> messages = new ArrayList<>();
|
||||
messages.add(Map.of("role", "system", "content", "请优化以下 Markdown 文本的格式,不改变其内容。"));
|
||||
messages.add(Map.of("role", "user", "content", text));
|
||||
body.put("messages", messages);
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
|
||||
try {
|
||||
ResponseEntity<Map> resp = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class);
|
||||
Map respBody = resp.getBody();
|
||||
if (respBody != null) {
|
||||
Object choicesObj = respBody.get("choices");
|
||||
if (choicesObj instanceof List choices && !choices.isEmpty()) {
|
||||
Object first = choices.get(0);
|
||||
if (first instanceof Map firstMap) {
|
||||
Object messageObj = firstMap.get("message");
|
||||
if (messageObj instanceof Map message) {
|
||||
Object content = message.get("content");
|
||||
if (content instanceof String str) {
|
||||
return Optional.of(str.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return Optional.empty();
|
||||
public Optional<String> formatMarkdown(String text) {
|
||||
if (apiKey == null || apiKey.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String url = "https://api.openai.com/v1/chat/completions";
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("model", model);
|
||||
List<Map<String, String>> messages = new ArrayList<>();
|
||||
messages.add(
|
||||
Map.of("role", "system", "content", "请优化以下 Markdown 文本的格式,不改变其内容。")
|
||||
);
|
||||
messages.add(Map.of("role", "user", "content", text));
|
||||
body.put("messages", messages);
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
|
||||
try {
|
||||
ResponseEntity<Map> resp = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class);
|
||||
Map respBody = resp.getBody();
|
||||
if (respBody != null) {
|
||||
Object choicesObj = respBody.get("choices");
|
||||
if (choicesObj instanceof List choices && !choices.isEmpty()) {
|
||||
Object first = choices.get(0);
|
||||
if (first instanceof Map firstMap) {
|
||||
Object messageObj = firstMap.get("message");
|
||||
if (messageObj instanceof Map message) {
|
||||
Object content = message.get("content");
|
||||
if (content instanceof String str) {
|
||||
return Optional.of(str.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +1,74 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import com.openisle.model.PasswordStrength;
|
||||
import com.openisle.exception.FieldException;
|
||||
import com.openisle.model.PasswordStrength;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class PasswordValidator {
|
||||
private PasswordStrength strength;
|
||||
|
||||
public PasswordValidator(@Value("${app.password.strength:LOW}") PasswordStrength strength) {
|
||||
this.strength = strength;
|
||||
}
|
||||
private PasswordStrength strength;
|
||||
|
||||
public PasswordStrength getStrength() {
|
||||
return strength;
|
||||
}
|
||||
public PasswordValidator(@Value("${app.password.strength:LOW}") PasswordStrength strength) {
|
||||
this.strength = strength;
|
||||
}
|
||||
|
||||
public void setStrength(PasswordStrength strength) {
|
||||
this.strength = strength;
|
||||
}
|
||||
public PasswordStrength getStrength() {
|
||||
return strength;
|
||||
}
|
||||
|
||||
public void validate(String password) {
|
||||
if (password == null || password.isEmpty()) {
|
||||
throw new FieldException("password", "Password cannot be empty");
|
||||
}
|
||||
switch (strength) {
|
||||
case MEDIUM:
|
||||
checkMedium(password);
|
||||
break;
|
||||
case HIGH:
|
||||
checkHigh(password);
|
||||
break;
|
||||
default:
|
||||
checkLow(password);
|
||||
break;
|
||||
}
|
||||
}
|
||||
public void setStrength(PasswordStrength strength) {
|
||||
this.strength = strength;
|
||||
}
|
||||
|
||||
private void checkLow(String password) {
|
||||
if (password.length() < 6) {
|
||||
throw new FieldException("password", "Password must be at least 6 characters long");
|
||||
}
|
||||
public void validate(String password) {
|
||||
if (password == null || password.isEmpty()) {
|
||||
throw new FieldException("password", "Password cannot be empty");
|
||||
}
|
||||
switch (strength) {
|
||||
case MEDIUM:
|
||||
checkMedium(password);
|
||||
break;
|
||||
case HIGH:
|
||||
checkHigh(password);
|
||||
break;
|
||||
default:
|
||||
checkLow(password);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void checkMedium(String password) {
|
||||
if (password.length() < 8) {
|
||||
throw new FieldException("password", "Password must be at least 8 characters long");
|
||||
}
|
||||
if (!password.matches(".*[A-Za-z].*") || !password.matches(".*\\d.*")) {
|
||||
throw new FieldException("password", "Password must contain letters and numbers");
|
||||
}
|
||||
private void checkLow(String password) {
|
||||
if (password.length() < 6) {
|
||||
throw new FieldException("password", "Password must be at least 6 characters long");
|
||||
}
|
||||
}
|
||||
|
||||
private void checkHigh(String password) {
|
||||
if (password.length() < 12) {
|
||||
throw new FieldException("password", "Password must be at least 12 characters long");
|
||||
}
|
||||
if (!password.matches(".*[A-Z].*")) {
|
||||
throw new FieldException("password", "Password must contain uppercase letters");
|
||||
}
|
||||
if (!password.matches(".*[a-z].*")) {
|
||||
throw new FieldException("password", "Password must contain lowercase letters");
|
||||
}
|
||||
if (!password.matches(".*\\d.*")) {
|
||||
throw new FieldException("password", "Password must contain numbers");
|
||||
}
|
||||
if (!password.matches(".*[^A-Za-z0-9].*")) {
|
||||
throw new FieldException("password", "Password must contain special characters");
|
||||
}
|
||||
private void checkMedium(String password) {
|
||||
if (password.length() < 8) {
|
||||
throw new FieldException("password", "Password must be at least 8 characters long");
|
||||
}
|
||||
if (!password.matches(".*[A-Za-z].*") || !password.matches(".*\\d.*")) {
|
||||
throw new FieldException("password", "Password must contain letters and numbers");
|
||||
}
|
||||
}
|
||||
|
||||
private void checkHigh(String password) {
|
||||
if (password.length() < 12) {
|
||||
throw new FieldException("password", "Password must be at least 12 characters long");
|
||||
}
|
||||
if (!password.matches(".*[A-Z].*")) {
|
||||
throw new FieldException("password", "Password must contain uppercase letters");
|
||||
}
|
||||
if (!password.matches(".*[a-z].*")) {
|
||||
throw new FieldException("password", "Password must contain lowercase letters");
|
||||
}
|
||||
if (!password.matches(".*\\d.*")) {
|
||||
throw new FieldException("password", "Password must contain numbers");
|
||||
}
|
||||
if (!password.matches(".*[^A-Za-z0-9].*")) {
|
||||
throw new FieldException("password", "Password must contain special characters");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,40 +9,41 @@ import com.openisle.model.User;
|
||||
import com.openisle.repository.PointGoodRepository;
|
||||
import com.openisle.repository.PointHistoryRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Service for point mall operations. */
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PointMallService {
|
||||
private final PointGoodRepository pointGoodRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final NotificationService notificationService;
|
||||
private final PointHistoryRepository pointHistoryRepository;
|
||||
|
||||
public List<PointGood> listGoods() {
|
||||
return pointGoodRepository.findAll();
|
||||
}
|
||||
private final PointGoodRepository pointGoodRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final NotificationService notificationService;
|
||||
private final PointHistoryRepository pointHistoryRepository;
|
||||
|
||||
public int redeem(User user, Long goodId, String contact) {
|
||||
PointGood good = pointGoodRepository.findById(goodId)
|
||||
.orElseThrow(() -> new NotFoundException("Good not found"));
|
||||
if (user.getPoint() < good.getCost()) {
|
||||
throw new FieldException("point", "Insufficient points");
|
||||
}
|
||||
user.setPoint(user.getPoint() - good.getCost());
|
||||
userRepository.save(user);
|
||||
notificationService.createPointRedeemNotifications(user, good.getName() + ": " + contact);
|
||||
PointHistory history = new PointHistory();
|
||||
history.setUser(user);
|
||||
history.setType(PointHistoryType.REDEEM);
|
||||
history.setAmount(-good.getCost());
|
||||
history.setBalance(user.getPoint());
|
||||
history.setCreatedAt(java.time.LocalDateTime.now());
|
||||
pointHistoryRepository.save(history);
|
||||
return user.getPoint();
|
||||
public List<PointGood> listGoods() {
|
||||
return pointGoodRepository.findAll();
|
||||
}
|
||||
|
||||
public int redeem(User user, Long goodId, String contact) {
|
||||
PointGood good = pointGoodRepository
|
||||
.findById(goodId)
|
||||
.orElseThrow(() -> new NotFoundException("Good not found"));
|
||||
if (user.getPoint() < good.getCost()) {
|
||||
throw new FieldException("point", "Insufficient points");
|
||||
}
|
||||
user.setPoint(user.getPoint() - good.getCost());
|
||||
userRepository.save(user);
|
||||
notificationService.createPointRedeemNotifications(user, good.getName() + ": " + contact);
|
||||
PointHistory history = new PointHistory();
|
||||
history.setUser(user);
|
||||
history.setType(PointHistoryType.REDEEM);
|
||||
history.setAmount(-good.getCost());
|
||||
history.setBalance(user.getPoint());
|
||||
history.setCreatedAt(java.time.LocalDateTime.now());
|
||||
pointHistoryRepository.save(history);
|
||||
return user.getPoint();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,253 +1,275 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import com.openisle.exception.FieldException;
|
||||
import com.openisle.model.*;
|
||||
import com.openisle.repository.*;
|
||||
import com.openisle.exception.FieldException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PointService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final PointLogRepository pointLogRepository;
|
||||
private final PostRepository postRepository;
|
||||
private final CommentRepository commentRepository;
|
||||
private final PointHistoryRepository pointHistoryRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final PointLogRepository pointLogRepository;
|
||||
private final PostRepository postRepository;
|
||||
private final CommentRepository commentRepository;
|
||||
private final PointHistoryRepository pointHistoryRepository;
|
||||
|
||||
public int awardForPost(String userName, Long postId) {
|
||||
User user = userRepository.findByUsername(userName).orElseThrow();
|
||||
PointLog log = getTodayLog(user);
|
||||
if (log.getPostCount() > 1) return 0;
|
||||
log.setPostCount(log.getPostCount() + 1);
|
||||
public int awardForPost(String userName, Long postId) {
|
||||
User user = userRepository.findByUsername(userName).orElseThrow();
|
||||
PointLog log = getTodayLog(user);
|
||||
if (log.getPostCount() > 1) return 0;
|
||||
log.setPostCount(log.getPostCount() + 1);
|
||||
pointLogRepository.save(log);
|
||||
Post post = postRepository.findById(postId).orElseThrow();
|
||||
return addPoint(user, 30, PointHistoryType.POST, post, null, null);
|
||||
}
|
||||
|
||||
public int awardForInvite(String userName, String inviteeName) {
|
||||
User user = userRepository.findByUsername(userName).orElseThrow();
|
||||
User invitee = userRepository.findByUsername(inviteeName).orElseThrow();
|
||||
return addPoint(user, 500, PointHistoryType.INVITE, null, null, invitee);
|
||||
}
|
||||
|
||||
public int awardForFeatured(String userName, Long postId) {
|
||||
User user = userRepository.findByUsername(userName).orElseThrow();
|
||||
Post post = postRepository.findById(postId).orElseThrow();
|
||||
return addPoint(user, 500, PointHistoryType.FEATURE, post, null, null);
|
||||
}
|
||||
|
||||
public void processLotteryJoin(User participant, LotteryPost post) {
|
||||
int cost = post.getPointCost();
|
||||
if (cost > 0) {
|
||||
if (participant.getPoint() < cost) {
|
||||
throw new FieldException("point", "积分不足");
|
||||
}
|
||||
addPoint(participant, -cost, PointHistoryType.LOTTERY_JOIN, post, null, post.getAuthor());
|
||||
addPoint(post.getAuthor(), cost, PointHistoryType.LOTTERY_REWARD, post, null, participant);
|
||||
}
|
||||
}
|
||||
|
||||
private PointLog getTodayLog(User user) {
|
||||
LocalDate today = LocalDate.now();
|
||||
return pointLogRepository
|
||||
.findByUserAndLogDate(user, today)
|
||||
.orElseGet(() -> {
|
||||
PointLog log = new PointLog();
|
||||
log.setUser(user);
|
||||
log.setLogDate(today);
|
||||
log.setPostCount(0);
|
||||
log.setCommentCount(0);
|
||||
log.setReactionCount(0);
|
||||
return pointLogRepository.save(log);
|
||||
});
|
||||
}
|
||||
|
||||
private int addPoint(
|
||||
User user,
|
||||
int amount,
|
||||
PointHistoryType type,
|
||||
Post post,
|
||||
Comment comment,
|
||||
User fromUser
|
||||
) {
|
||||
if (pointHistoryRepository.countByUser(user) == 0) {
|
||||
recordHistory(user, PointHistoryType.SYSTEM_ONLINE, 0, null, null, null);
|
||||
}
|
||||
user.setPoint(user.getPoint() + amount);
|
||||
userRepository.save(user);
|
||||
recordHistory(user, type, amount, post, comment, fromUser);
|
||||
return amount;
|
||||
}
|
||||
|
||||
private void recordHistory(
|
||||
User user,
|
||||
PointHistoryType type,
|
||||
int amount,
|
||||
Post post,
|
||||
Comment comment,
|
||||
User fromUser
|
||||
) {
|
||||
PointHistory history = new PointHistory();
|
||||
history.setUser(user);
|
||||
history.setType(type);
|
||||
history.setAmount(amount);
|
||||
history.setBalance(user.getPoint());
|
||||
history.setPost(post);
|
||||
history.setComment(comment);
|
||||
history.setFromUser(fromUser);
|
||||
history.setCreatedAt(java.time.LocalDateTime.now());
|
||||
pointHistoryRepository.save(history);
|
||||
}
|
||||
|
||||
// 同时为评论者和发帖人增加积分,返回值为评论者增加的积分数
|
||||
// 注意需要考虑发帖和回复是同一人的场景
|
||||
public int awardForComment(String commenterName, Long postId, Long commentId) {
|
||||
// 标记评论者是否已达到积分奖励上限
|
||||
boolean isTheRewardCapped = false;
|
||||
|
||||
// 根据帖子id找到发帖人
|
||||
Post post = postRepository.findById(postId).orElseThrow();
|
||||
User poster = post.getAuthor();
|
||||
Comment comment = commentRepository.findById(commentId).orElseThrow();
|
||||
|
||||
// 获取评论者的加分日志
|
||||
User commenter = userRepository.findByUsername(commenterName).orElseThrow();
|
||||
PointLog log = getTodayLog(commenter);
|
||||
if (log.getCommentCount() > 3) {
|
||||
isTheRewardCapped = true;
|
||||
}
|
||||
|
||||
// 如果发帖人与评论者是同一个,则只计算单次加分
|
||||
if (poster.getId().equals(commenter.getId())) {
|
||||
if (isTheRewardCapped) {
|
||||
return 0;
|
||||
} else {
|
||||
log.setCommentCount(log.getCommentCount() + 1);
|
||||
pointLogRepository.save(log);
|
||||
Post post = postRepository.findById(postId).orElseThrow();
|
||||
return addPoint(user, 30, PointHistoryType.POST, post, null, null);
|
||||
return addPoint(commenter, 10, PointHistoryType.COMMENT, post, comment, null);
|
||||
}
|
||||
} else {
|
||||
addPoint(poster, 10, PointHistoryType.COMMENT, post, comment, commenter);
|
||||
// 如果发帖人与评论者不是同一个,则根据是否达到积分上限来判断评论者加分情况
|
||||
if (isTheRewardCapped) {
|
||||
return 0;
|
||||
} else {
|
||||
return addPoint(commenter, 10, PointHistoryType.COMMENT, post, comment, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 需要考虑点赞者和发帖人是同一个的情况
|
||||
public int awardForReactionOfPost(String reactionerName, Long postId) {
|
||||
// 根据帖子id找到发帖人
|
||||
User poster = postRepository.findById(postId).orElseThrow().getAuthor();
|
||||
|
||||
// 获取点赞者信息
|
||||
User reactioner = userRepository.findByUsername(reactionerName).orElseThrow();
|
||||
|
||||
// 如果发帖人与点赞者是同一个,则不加分
|
||||
if (poster.getId().equals(reactioner.getId())) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int awardForInvite(String userName, String inviteeName) {
|
||||
User user = userRepository.findByUsername(userName).orElseThrow();
|
||||
User invitee = userRepository.findByUsername(inviteeName).orElseThrow();
|
||||
return addPoint(user, 500, PointHistoryType.INVITE, null, null, invitee);
|
||||
// 如果不是同一个,则为发帖人加分
|
||||
Post post = postRepository.findById(postId).orElseThrow();
|
||||
return addPoint(poster, 10, PointHistoryType.POST_LIKED, post, null, reactioner);
|
||||
}
|
||||
|
||||
public int deductForReactionOfPost(String reactionerName, Long postId) {
|
||||
User poster = postRepository.findById(postId).orElseThrow().getAuthor();
|
||||
User reactioner = userRepository.findByUsername(reactionerName).orElseThrow();
|
||||
if (poster.getId().equals(reactioner.getId())) {
|
||||
return 0;
|
||||
}
|
||||
Post post = postRepository.findById(postId).orElseThrow();
|
||||
return addPoint(poster, -10, PointHistoryType.POST_LIKE_CANCELLED, post, null, reactioner);
|
||||
}
|
||||
|
||||
// 考虑点赞者和评论者是同一个的情况
|
||||
public int awardForReactionOfComment(String reactionerName, Long commentId) {
|
||||
// 根据帖子id找到评论者
|
||||
User commenter = commentRepository.findById(commentId).orElseThrow().getAuthor();
|
||||
|
||||
// 获取点赞者信息
|
||||
User reactioner = userRepository.findByUsername(reactionerName).orElseThrow();
|
||||
|
||||
// 如果评论者与点赞者是同一个,则不加分
|
||||
if (commenter.getId().equals(reactioner.getId())) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int awardForFeatured(String userName, Long postId) {
|
||||
User user = userRepository.findByUsername(userName).orElseThrow();
|
||||
Post post = postRepository.findById(postId).orElseThrow();
|
||||
return addPoint(user, 500, PointHistoryType.FEATURE, post, null, null);
|
||||
// 如果不是同一个,则为发帖人加分
|
||||
Comment comment = commentRepository.findById(commentId).orElseThrow();
|
||||
Post post = comment.getPost();
|
||||
return addPoint(commenter, 10, PointHistoryType.COMMENT_LIKED, post, comment, reactioner);
|
||||
}
|
||||
|
||||
public int deductForReactionOfComment(String reactionerName, Long commentId) {
|
||||
User commenter = commentRepository.findById(commentId).orElseThrow().getAuthor();
|
||||
User reactioner = userRepository.findByUsername(reactionerName).orElseThrow();
|
||||
if (commenter.getId().equals(reactioner.getId())) {
|
||||
return 0;
|
||||
}
|
||||
Comment comment = commentRepository.findById(commentId).orElseThrow();
|
||||
Post post = comment.getPost();
|
||||
return addPoint(
|
||||
commenter,
|
||||
-10,
|
||||
PointHistoryType.COMMENT_LIKE_CANCELLED,
|
||||
post,
|
||||
comment,
|
||||
reactioner
|
||||
);
|
||||
}
|
||||
|
||||
public java.util.List<PointHistory> listHistory(String userName) {
|
||||
User user = userRepository.findByUsername(userName).orElseThrow();
|
||||
if (pointHistoryRepository.countByUser(user) == 0) {
|
||||
recordHistory(user, PointHistoryType.SYSTEM_ONLINE, 0, null, null, null);
|
||||
}
|
||||
return pointHistoryRepository.findByUserOrderByIdDesc(user);
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> trend(String userName, int days) {
|
||||
if (days < 1) days = 1;
|
||||
User user = userRepository.findByUsername(userName).orElseThrow();
|
||||
LocalDate end = LocalDate.now();
|
||||
LocalDate start = end.minusDays(days - 1L);
|
||||
var histories = pointHistoryRepository.findByUserAndCreatedAtAfterOrderByCreatedAtDesc(
|
||||
user,
|
||||
start.atStartOfDay()
|
||||
);
|
||||
int idx = 0;
|
||||
int balance = user.getPoint();
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (LocalDate day = end; !day.isBefore(start); day = day.minusDays(1)) {
|
||||
result.add(Map.of("date", day.toString(), "value", balance));
|
||||
while (
|
||||
idx < histories.size() && histories.get(idx).getCreatedAt().toLocalDate().isEqual(day)
|
||||
) {
|
||||
balance -= histories.get(idx).getAmount();
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
Collections.reverse(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新计算用户的积分总数
|
||||
* 通过累加所有积分历史记录来重新计算用户的当前积分
|
||||
*/
|
||||
public int recalculateUserPoints(User user) {
|
||||
// 获取用户所有的积分历史记录(由于@Where注解,已删除的记录会被自动过滤)
|
||||
List<PointHistory> histories = pointHistoryRepository.findByUserOrderByIdAsc(user);
|
||||
|
||||
int totalPoints = 0;
|
||||
for (PointHistory history : histories) {
|
||||
totalPoints += history.getAmount();
|
||||
// 重新计算每条历史记录的余额
|
||||
history.setBalance(totalPoints);
|
||||
}
|
||||
|
||||
public void processLotteryJoin(User participant, LotteryPost post) {
|
||||
int cost = post.getPointCost();
|
||||
if (cost > 0) {
|
||||
if (participant.getPoint() < cost) {
|
||||
throw new FieldException("point", "积分不足");
|
||||
}
|
||||
addPoint(participant, -cost, PointHistoryType.LOTTERY_JOIN, post, null, post.getAuthor());
|
||||
addPoint(post.getAuthor(), cost, PointHistoryType.LOTTERY_REWARD, post, null, participant);
|
||||
}
|
||||
}
|
||||
// 批量更新历史记录及用户积分
|
||||
pointHistoryRepository.saveAll(histories);
|
||||
user.setPoint(totalPoints);
|
||||
userRepository.save(user);
|
||||
|
||||
private PointLog getTodayLog(User user) {
|
||||
LocalDate today = LocalDate.now();
|
||||
return pointLogRepository.findByUserAndLogDate(user, today)
|
||||
.orElseGet(() -> {
|
||||
PointLog log = new PointLog();
|
||||
log.setUser(user);
|
||||
log.setLogDate(today);
|
||||
log.setPostCount(0);
|
||||
log.setCommentCount(0);
|
||||
log.setReactionCount(0);
|
||||
return pointLogRepository.save(log);
|
||||
});
|
||||
}
|
||||
|
||||
private int addPoint(User user, int amount, PointHistoryType type,
|
||||
Post post, Comment comment, User fromUser) {
|
||||
if (pointHistoryRepository.countByUser(user) == 0) {
|
||||
recordHistory(user, PointHistoryType.SYSTEM_ONLINE, 0, null, null, null);
|
||||
}
|
||||
user.setPoint(user.getPoint() + amount);
|
||||
userRepository.save(user);
|
||||
recordHistory(user, type, amount, post, comment, fromUser);
|
||||
return amount;
|
||||
}
|
||||
|
||||
private void recordHistory(User user, PointHistoryType type, int amount,
|
||||
Post post, Comment comment, User fromUser) {
|
||||
PointHistory history = new PointHistory();
|
||||
history.setUser(user);
|
||||
history.setType(type);
|
||||
history.setAmount(amount);
|
||||
history.setBalance(user.getPoint());
|
||||
history.setPost(post);
|
||||
history.setComment(comment);
|
||||
history.setFromUser(fromUser);
|
||||
history.setCreatedAt(java.time.LocalDateTime.now());
|
||||
pointHistoryRepository.save(history);
|
||||
}
|
||||
|
||||
// 同时为评论者和发帖人增加积分,返回值为评论者增加的积分数
|
||||
// 注意需要考虑发帖和回复是同一人的场景
|
||||
public int awardForComment(String commenterName, Long postId, Long commentId) {
|
||||
// 标记评论者是否已达到积分奖励上限
|
||||
boolean isTheRewardCapped = false;
|
||||
|
||||
// 根据帖子id找到发帖人
|
||||
Post post = postRepository.findById(postId).orElseThrow();
|
||||
User poster = post.getAuthor();
|
||||
Comment comment = commentRepository.findById(commentId).orElseThrow();
|
||||
|
||||
// 获取评论者的加分日志
|
||||
User commenter = userRepository.findByUsername(commenterName).orElseThrow();
|
||||
PointLog log = getTodayLog(commenter);
|
||||
if (log.getCommentCount() > 3) {
|
||||
isTheRewardCapped = true;
|
||||
}
|
||||
|
||||
// 如果发帖人与评论者是同一个,则只计算单次加分
|
||||
if (poster.getId().equals(commenter.getId())) {
|
||||
if (isTheRewardCapped) {
|
||||
return 0;
|
||||
} else {
|
||||
log.setCommentCount(log.getCommentCount() + 1);
|
||||
pointLogRepository.save(log);
|
||||
return addPoint(commenter, 10, PointHistoryType.COMMENT, post, comment, null);
|
||||
}
|
||||
} else {
|
||||
addPoint(poster, 10, PointHistoryType.COMMENT, post, comment, commenter);
|
||||
// 如果发帖人与评论者不是同一个,则根据是否达到积分上限来判断评论者加分情况
|
||||
if (isTheRewardCapped) {
|
||||
return 0;
|
||||
} else {
|
||||
return addPoint(commenter, 10, PointHistoryType.COMMENT, post, comment, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 需要考虑点赞者和发帖人是同一个的情况
|
||||
public int awardForReactionOfPost(String reactionerName, Long postId) {
|
||||
// 根据帖子id找到发帖人
|
||||
User poster = postRepository.findById(postId).orElseThrow().getAuthor();
|
||||
|
||||
// 获取点赞者信息
|
||||
User reactioner = userRepository.findByUsername(reactionerName).orElseThrow();
|
||||
|
||||
// 如果发帖人与点赞者是同一个,则不加分
|
||||
if (poster.getId().equals(reactioner.getId())) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 如果不是同一个,则为发帖人加分
|
||||
Post post = postRepository.findById(postId).orElseThrow();
|
||||
return addPoint(poster, 10, PointHistoryType.POST_LIKED, post, null, reactioner);
|
||||
}
|
||||
|
||||
public int deductForReactionOfPost(String reactionerName, Long postId) {
|
||||
User poster = postRepository.findById(postId).orElseThrow().getAuthor();
|
||||
User reactioner = userRepository.findByUsername(reactionerName).orElseThrow();
|
||||
if (poster.getId().equals(reactioner.getId())) {
|
||||
return 0;
|
||||
}
|
||||
Post post = postRepository.findById(postId).orElseThrow();
|
||||
return addPoint(poster, -10, PointHistoryType.POST_LIKE_CANCELLED, post, null, reactioner);
|
||||
}
|
||||
|
||||
// 考虑点赞者和评论者是同一个的情况
|
||||
public int awardForReactionOfComment(String reactionerName, Long commentId) {
|
||||
// 根据帖子id找到评论者
|
||||
User commenter = commentRepository.findById(commentId).orElseThrow().getAuthor();
|
||||
|
||||
// 获取点赞者信息
|
||||
User reactioner = userRepository.findByUsername(reactionerName).orElseThrow();
|
||||
|
||||
// 如果评论者与点赞者是同一个,则不加分
|
||||
if (commenter.getId().equals(reactioner.getId())) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 如果不是同一个,则为发帖人加分
|
||||
Comment comment = commentRepository.findById(commentId).orElseThrow();
|
||||
Post post = comment.getPost();
|
||||
return addPoint(commenter, 10, PointHistoryType.COMMENT_LIKED, post, comment, reactioner);
|
||||
}
|
||||
|
||||
public int deductForReactionOfComment(String reactionerName, Long commentId) {
|
||||
User commenter = commentRepository.findById(commentId).orElseThrow().getAuthor();
|
||||
User reactioner = userRepository.findByUsername(reactionerName).orElseThrow();
|
||||
if (commenter.getId().equals(reactioner.getId())) {
|
||||
return 0;
|
||||
}
|
||||
Comment comment = commentRepository.findById(commentId).orElseThrow();
|
||||
Post post = comment.getPost();
|
||||
return addPoint(commenter, -10, PointHistoryType.COMMENT_LIKE_CANCELLED, post, comment, reactioner);
|
||||
}
|
||||
|
||||
public java.util.List<PointHistory> listHistory(String userName) {
|
||||
User user = userRepository.findByUsername(userName).orElseThrow();
|
||||
if (pointHistoryRepository.countByUser(user) == 0) {
|
||||
recordHistory(user, PointHistoryType.SYSTEM_ONLINE, 0, null, null, null);
|
||||
}
|
||||
return pointHistoryRepository.findByUserOrderByIdDesc(user);
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> trend(String userName, int days) {
|
||||
if (days < 1) days = 1;
|
||||
User user = userRepository.findByUsername(userName).orElseThrow();
|
||||
LocalDate end = LocalDate.now();
|
||||
LocalDate start = end.minusDays(days - 1L);
|
||||
var histories = pointHistoryRepository.findByUserAndCreatedAtAfterOrderByCreatedAtDesc(
|
||||
user, start.atStartOfDay());
|
||||
int idx = 0;
|
||||
int balance = user.getPoint();
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (LocalDate day = end; !day.isBefore(start); day = day.minusDays(1)) {
|
||||
result.add(Map.of("date", day.toString(), "value", balance));
|
||||
while (idx < histories.size() && histories.get(idx).getCreatedAt().toLocalDate().isEqual(day)) {
|
||||
balance -= histories.get(idx).getAmount();
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
Collections.reverse(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新计算用户的积分总数
|
||||
* 通过累加所有积分历史记录来重新计算用户的当前积分
|
||||
*/
|
||||
public int recalculateUserPoints(User user) {
|
||||
// 获取用户所有的积分历史记录(由于@Where注解,已删除的记录会被自动过滤)
|
||||
List<PointHistory> histories = pointHistoryRepository.findByUserOrderByIdAsc(user);
|
||||
|
||||
int totalPoints = 0;
|
||||
for (PointHistory history : histories) {
|
||||
totalPoints += history.getAmount();
|
||||
// 重新计算每条历史记录的余额
|
||||
history.setBalance(totalPoints);
|
||||
}
|
||||
|
||||
// 批量更新历史记录及用户积分
|
||||
pointHistoryRepository.saveAll(histories);
|
||||
user.setPoint(totalPoints);
|
||||
userRepository.save(user);
|
||||
|
||||
return totalPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新计算用户的积分总数(通过用户名)
|
||||
*/
|
||||
public int recalculateUserPoints(String userName) {
|
||||
User user = userRepository.findByUsername(userName).orElseThrow();
|
||||
return recalculateUserPoints(user);
|
||||
}
|
||||
return totalPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新计算用户的积分总数(通过用户名)
|
||||
*/
|
||||
public int recalculateUserPoints(String userName) {
|
||||
User user = userRepository.findByUsername(userName).orElseThrow();
|
||||
return recalculateUserPoints(user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,118 +4,125 @@ 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;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@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"));
|
||||
}
|
||||
private final PostChangeLogRepository logRepository;
|
||||
private final PostRepository postRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
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);
|
||||
}
|
||||
private User getSystemUser() {
|
||||
return userRepository
|
||||
.findByUsername("system")
|
||||
.orElseThrow(() -> new IllegalStateException("System user not found"));
|
||||
}
|
||||
|
||||
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 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 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 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 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 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 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 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 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 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 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 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 recordVoteResult(Post post) {
|
||||
PostVoteResultChangeLog log = new PostVoteResultChangeLog();
|
||||
log.setPost(post);
|
||||
log.setUser(getSystemUser());
|
||||
log.setType(PostChangeType.VOTE_RESULT);
|
||||
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 recordLotteryResult(Post post) {
|
||||
PostLotteryResultChangeLog log = new PostLotteryResultChangeLog();
|
||||
log.setPost(post);
|
||||
log.setUser(getSystemUser());
|
||||
log.setType(PostChangeType.LOTTERY_RESULT);
|
||||
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 deleteLogsForPost(Post post) {
|
||||
logRepository.deleteByPost(post);
|
||||
}
|
||||
public void recordLotteryResult(Post post) {
|
||||
PostLotteryResultChangeLog log = new PostLotteryResultChangeLog();
|
||||
log.setPost(post);
|
||||
log.setUser(getSystemUser());
|
||||
log.setType(PostChangeType.LOTTERY_RESULT);
|
||||
logRepository.save(log);
|
||||
}
|
||||
|
||||
public List<PostChangeLog> listLogs(Long postId) {
|
||||
Post post = postRepository.findById(postId)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
return logRepository.findByPostOrderByCreatedAtAsc(post);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,44 +6,52 @@ import com.openisle.model.User;
|
||||
import com.openisle.repository.PostReadRepository;
|
||||
import com.openisle.repository.PostRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PostReadService {
|
||||
private final PostReadRepository postReadRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final PostRepository postRepository;
|
||||
|
||||
public void recordRead(String username, Long postId) {
|
||||
if (username == null) return;
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Post post = postRepository.findById(postId)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
postReadRepository.findByUserAndPost(user, post).ifPresentOrElse(pr -> {
|
||||
pr.setLastReadAt(LocalDateTime.now());
|
||||
postReadRepository.save(pr);
|
||||
}, () -> {
|
||||
PostRead pr = new PostRead();
|
||||
pr.setUser(user);
|
||||
pr.setPost(post);
|
||||
pr.setLastReadAt(LocalDateTime.now());
|
||||
postReadRepository.save(pr);
|
||||
});
|
||||
}
|
||||
private final PostReadRepository postReadRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final PostRepository postRepository;
|
||||
|
||||
public long countReads(String username) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
return postReadRepository.countByUser(user);
|
||||
}
|
||||
public void recordRead(String username, Long postId) {
|
||||
if (username == null) return;
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Post post = postRepository
|
||||
.findById(postId)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
postReadRepository
|
||||
.findByUserAndPost(user, post)
|
||||
.ifPresentOrElse(
|
||||
pr -> {
|
||||
pr.setLastReadAt(LocalDateTime.now());
|
||||
postReadRepository.save(pr);
|
||||
},
|
||||
() -> {
|
||||
PostRead pr = new PostRead();
|
||||
pr.setUser(user);
|
||||
pr.setPost(post);
|
||||
pr.setLastReadAt(LocalDateTime.now());
|
||||
postReadRepository.save(pr);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@org.springframework.transaction.annotation.Transactional
|
||||
public void deleteByPost(Post post) {
|
||||
postReadRepository.deleteByPost(post);
|
||||
}
|
||||
public long countReads(String username) {
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
return postReadRepository.countByUser(user);
|
||||
}
|
||||
|
||||
@org.springframework.transaction.annotation.Transactional
|
||||
public void deleteByPost(Post post) {
|
||||
postReadRepository.deleteByPost(post);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,10 @@ package com.openisle.service;
|
||||
import com.openisle.model.PushSubscription;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.PushSubscriptionRepository;
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.Security;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import nl.martijndwars.webpush.Notification;
|
||||
import nl.martijndwars.webpush.PushService;
|
||||
@@ -11,42 +15,51 @@ import org.jose4j.lang.JoseException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.Security;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PushNotificationService {
|
||||
private final PushSubscriptionRepository subscriptionRepository;
|
||||
private final PushService pushService;
|
||||
|
||||
public PushNotificationService(PushSubscriptionRepository subscriptionRepository,
|
||||
@Value("${app.webpush.public-key:}") String publicKey,
|
||||
@Value("${app.webpush.private-key:}") String privateKey) throws GeneralSecurityException {
|
||||
this.subscriptionRepository = subscriptionRepository;
|
||||
if (publicKey != null && !publicKey.isBlank() && privateKey != null && !privateKey.isBlank()) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
this.pushService = new PushService(publicKey, privateKey);
|
||||
} else {
|
||||
this.pushService = null;
|
||||
}
|
||||
}
|
||||
private final PushSubscriptionRepository subscriptionRepository;
|
||||
private final PushService pushService;
|
||||
|
||||
public void sendNotification(User user, String payload) {
|
||||
if (pushService == null) {
|
||||
log.warn("Push notifications are disabled because VAPID keys are not configured.");
|
||||
return;
|
||||
}
|
||||
List<PushSubscription> subs = subscriptionRepository.findByUser(user);
|
||||
for (PushSubscription sub : subs) {
|
||||
try {
|
||||
Notification notification = new Notification(sub.getEndpoint(), sub.getP256dh(), sub.getAuth(), payload);
|
||||
pushService.send(notification);
|
||||
} catch (GeneralSecurityException | IOException | JoseException | InterruptedException | java.util.concurrent.ExecutionException e) {
|
||||
log.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
public PushNotificationService(
|
||||
PushSubscriptionRepository subscriptionRepository,
|
||||
@Value("${app.webpush.public-key:}") String publicKey,
|
||||
@Value("${app.webpush.private-key:}") String privateKey
|
||||
) throws GeneralSecurityException {
|
||||
this.subscriptionRepository = subscriptionRepository;
|
||||
if (publicKey != null && !publicKey.isBlank() && privateKey != null && !privateKey.isBlank()) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
this.pushService = new PushService(publicKey, privateKey);
|
||||
} else {
|
||||
this.pushService = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void sendNotification(User user, String payload) {
|
||||
if (pushService == null) {
|
||||
log.warn("Push notifications are disabled because VAPID keys are not configured.");
|
||||
return;
|
||||
}
|
||||
List<PushSubscription> subs = subscriptionRepository.findByUser(user);
|
||||
for (PushSubscription sub : subs) {
|
||||
try {
|
||||
Notification notification = new Notification(
|
||||
sub.getEndpoint(),
|
||||
sub.getP256dh(),
|
||||
sub.getAuth(),
|
||||
payload
|
||||
);
|
||||
pushService.send(notification);
|
||||
} catch (
|
||||
GeneralSecurityException
|
||||
| IOException
|
||||
| JoseException
|
||||
| InterruptedException
|
||||
| java.util.concurrent.ExecutionException e
|
||||
) {
|
||||
log.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,32 +4,33 @@ import com.openisle.model.PushSubscription;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.PushSubscriptionRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PushSubscriptionService {
|
||||
private final PushSubscriptionRepository subscriptionRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Transactional
|
||||
public void saveSubscription(String username, String endpoint, String p256dh, String auth) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
subscriptionRepository.deleteByUserAndEndpoint(user, endpoint);
|
||||
PushSubscription sub = new PushSubscription();
|
||||
sub.setUser(user);
|
||||
sub.setEndpoint(endpoint);
|
||||
sub.setP256dh(p256dh);
|
||||
sub.setAuth(auth);
|
||||
subscriptionRepository.save(sub);
|
||||
}
|
||||
private final PushSubscriptionRepository subscriptionRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public List<PushSubscription> listByUser(User user) {
|
||||
return subscriptionRepository.findByUser(user);
|
||||
}
|
||||
@Transactional
|
||||
public void saveSubscription(String username, String endpoint, String p256dh, String auth) {
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
subscriptionRepository.deleteByUserAndEndpoint(user, endpoint);
|
||||
PushSubscription sub = new PushSubscription();
|
||||
sub.setUser(user);
|
||||
sub.setEndpoint(endpoint);
|
||||
sub.setP256dh(p256dh);
|
||||
sub.setAuth(auth);
|
||||
subscriptionRepository.save(sub);
|
||||
}
|
||||
|
||||
public List<PushSubscription> listByUser(User user) {
|
||||
return subscriptionRepository.findByUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import com.openisle.model.Comment;
|
||||
import com.openisle.model.Message;
|
||||
import com.openisle.model.NotificationType;
|
||||
import com.openisle.model.Post;
|
||||
import com.openisle.model.Reaction;
|
||||
import com.openisle.model.ReactionType;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.model.NotificationType;
|
||||
import com.openisle.model.Message;
|
||||
import com.openisle.repository.CommentRepository;
|
||||
import com.openisle.repository.MessageRepository;
|
||||
import com.openisle.repository.PostRepository;
|
||||
import com.openisle.repository.ReactionRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import com.openisle.repository.MessageRepository;
|
||||
import com.openisle.service.NotificationService;
|
||||
import com.openisle.service.EmailSender;
|
||||
import com.openisle.service.NotificationService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -22,111 +22,153 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ReactionService {
|
||||
private final ReactionRepository reactionRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final PostRepository postRepository;
|
||||
private final CommentRepository commentRepository;
|
||||
private final MessageRepository messageRepository;
|
||||
private final NotificationService notificationService;
|
||||
private final EmailSender emailSender;
|
||||
|
||||
@Value("${app.website-url}")
|
||||
private String websiteUrl;
|
||||
private final ReactionRepository reactionRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final PostRepository postRepository;
|
||||
private final CommentRepository commentRepository;
|
||||
private final MessageRepository messageRepository;
|
||||
private final NotificationService notificationService;
|
||||
private final EmailSender emailSender;
|
||||
|
||||
@Transactional
|
||||
public Reaction reactToPost(String username, Long postId, ReactionType type) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Post post = postRepository.findById(postId)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
java.util.Optional<Reaction> existing =
|
||||
reactionRepository.findByUserAndPostAndType(user, post, type);
|
||||
if (existing.isPresent()) {
|
||||
notificationService.deleteReactionNotification(user, post, null, type);
|
||||
reactionRepository.delete(existing.get());
|
||||
return null;
|
||||
}
|
||||
Reaction reaction = new Reaction();
|
||||
reaction.setUser(user);
|
||||
reaction.setPost(post);
|
||||
reaction.setType(type);
|
||||
reaction = reactionRepository.save(reaction);
|
||||
if (!user.getId().equals(post.getAuthor().getId())) {
|
||||
notificationService.createNotification(post.getAuthor(), NotificationType.REACTION, post, null, null, user, type, null);
|
||||
}
|
||||
return reaction;
|
||||
@Value("${app.website-url}")
|
||||
private String websiteUrl;
|
||||
|
||||
@Transactional
|
||||
public Reaction reactToPost(String username, Long postId, ReactionType type) {
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Post post = postRepository
|
||||
.findById(postId)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
java.util.Optional<Reaction> existing = reactionRepository.findByUserAndPostAndType(
|
||||
user,
|
||||
post,
|
||||
type
|
||||
);
|
||||
if (existing.isPresent()) {
|
||||
notificationService.deleteReactionNotification(user, post, null, type);
|
||||
reactionRepository.delete(existing.get());
|
||||
return null;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Reaction reactToComment(String username, Long commentId, ReactionType type) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Comment comment = commentRepository.findById(commentId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Comment not found"));
|
||||
java.util.Optional<Reaction> existing =
|
||||
reactionRepository.findByUserAndCommentAndType(user, comment, type);
|
||||
if (existing.isPresent()) {
|
||||
notificationService.deleteReactionNotification(user, null, comment, type);
|
||||
reactionRepository.delete(existing.get());
|
||||
return null;
|
||||
}
|
||||
Reaction reaction = new Reaction();
|
||||
reaction.setUser(user);
|
||||
reaction.setComment(comment);
|
||||
reaction.setPost(null);
|
||||
reaction.setType(type);
|
||||
reaction = reactionRepository.save(reaction);
|
||||
if (!user.getId().equals(comment.getAuthor().getId())) {
|
||||
notificationService.createNotification(comment.getAuthor(), NotificationType.REACTION, comment.getPost(), comment, null, user, type, null);
|
||||
}
|
||||
return reaction;
|
||||
Reaction reaction = new Reaction();
|
||||
reaction.setUser(user);
|
||||
reaction.setPost(post);
|
||||
reaction.setType(type);
|
||||
reaction = reactionRepository.save(reaction);
|
||||
if (!user.getId().equals(post.getAuthor().getId())) {
|
||||
notificationService.createNotification(
|
||||
post.getAuthor(),
|
||||
NotificationType.REACTION,
|
||||
post,
|
||||
null,
|
||||
null,
|
||||
user,
|
||||
type,
|
||||
null
|
||||
);
|
||||
}
|
||||
return reaction;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Reaction reactToMessage(String username, Long messageId, ReactionType type) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Message message = messageRepository.findById(messageId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Message not found"));
|
||||
java.util.Optional<Reaction> existing =
|
||||
reactionRepository.findByUserAndMessageAndType(user, message, type);
|
||||
if (existing.isPresent()) {
|
||||
reactionRepository.delete(existing.get());
|
||||
return null;
|
||||
}
|
||||
Reaction reaction = new Reaction();
|
||||
reaction.setUser(user);
|
||||
reaction.setMessage(message);
|
||||
reaction.setType(type);
|
||||
reaction = reactionRepository.save(reaction);
|
||||
return reaction;
|
||||
@Transactional
|
||||
public Reaction reactToComment(String username, Long commentId, ReactionType type) {
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Comment comment = commentRepository
|
||||
.findById(commentId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Comment not found"));
|
||||
java.util.Optional<Reaction> existing = reactionRepository.findByUserAndCommentAndType(
|
||||
user,
|
||||
comment,
|
||||
type
|
||||
);
|
||||
if (existing.isPresent()) {
|
||||
notificationService.deleteReactionNotification(user, null, comment, type);
|
||||
reactionRepository.delete(existing.get());
|
||||
return null;
|
||||
}
|
||||
Reaction reaction = new Reaction();
|
||||
reaction.setUser(user);
|
||||
reaction.setComment(comment);
|
||||
reaction.setPost(null);
|
||||
reaction.setType(type);
|
||||
reaction = reactionRepository.save(reaction);
|
||||
if (!user.getId().equals(comment.getAuthor().getId())) {
|
||||
notificationService.createNotification(
|
||||
comment.getAuthor(),
|
||||
NotificationType.REACTION,
|
||||
comment.getPost(),
|
||||
comment,
|
||||
null,
|
||||
user,
|
||||
type,
|
||||
null
|
||||
);
|
||||
}
|
||||
return reaction;
|
||||
}
|
||||
|
||||
public java.util.List<Reaction> getReactionsForPost(Long postId) {
|
||||
Post post = postRepository.findById(postId)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
return reactionRepository.findByPost(post);
|
||||
@Transactional
|
||||
public Reaction reactToMessage(String username, Long messageId, ReactionType type) {
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Message message = messageRepository
|
||||
.findById(messageId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Message not found"));
|
||||
java.util.Optional<Reaction> existing = reactionRepository.findByUserAndMessageAndType(
|
||||
user,
|
||||
message,
|
||||
type
|
||||
);
|
||||
if (existing.isPresent()) {
|
||||
reactionRepository.delete(existing.get());
|
||||
return null;
|
||||
}
|
||||
Reaction reaction = new Reaction();
|
||||
reaction.setUser(user);
|
||||
reaction.setMessage(message);
|
||||
reaction.setType(type);
|
||||
reaction = reactionRepository.save(reaction);
|
||||
return reaction;
|
||||
}
|
||||
|
||||
public java.util.List<Reaction> getReactionsForComment(Long commentId) {
|
||||
Comment comment = commentRepository.findById(commentId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Comment not found"));
|
||||
return reactionRepository.findByComment(comment);
|
||||
}
|
||||
public java.util.List<Reaction> getReactionsForPost(Long postId) {
|
||||
Post post = postRepository
|
||||
.findById(postId)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
return reactionRepository.findByPost(post);
|
||||
}
|
||||
|
||||
public java.util.List<Long> topPostIds(String username, int limit) {
|
||||
return reactionRepository.findTopPostIds(username, org.springframework.data.domain.PageRequest.of(0, limit));
|
||||
}
|
||||
public java.util.List<Reaction> getReactionsForComment(Long commentId) {
|
||||
Comment comment = commentRepository
|
||||
.findById(commentId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Comment not found"));
|
||||
return reactionRepository.findByComment(comment);
|
||||
}
|
||||
|
||||
public java.util.List<Long> topCommentIds(String username, int limit) {
|
||||
return reactionRepository.findTopCommentIds(username, org.springframework.data.domain.PageRequest.of(0, limit));
|
||||
}
|
||||
public java.util.List<Long> topPostIds(String username, int limit) {
|
||||
return reactionRepository.findTopPostIds(
|
||||
username,
|
||||
org.springframework.data.domain.PageRequest.of(0, limit)
|
||||
);
|
||||
}
|
||||
|
||||
public long countLikesSent(String username) {
|
||||
return reactionRepository.countLikesSent(username);
|
||||
}
|
||||
public java.util.List<Long> topCommentIds(String username, int limit) {
|
||||
return reactionRepository.findTopCommentIds(
|
||||
username,
|
||||
org.springframework.data.domain.PageRequest.of(0, limit)
|
||||
);
|
||||
}
|
||||
|
||||
public long countLikesReceived(String username) {
|
||||
return reactionRepository.countLikesReceived(username);
|
||||
}
|
||||
public long countLikesSent(String username) {
|
||||
return reactionRepository.countLikesSent(username);
|
||||
}
|
||||
|
||||
public long countLikesReceived(String username) {
|
||||
return reactionRepository.countLikesReceived(username);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* CaptchaService implementation using Google reCAPTCHA.
|
||||
*/
|
||||
@Service
|
||||
public class RecaptchaService extends CaptchaService {
|
||||
|
||||
@Value("${recaptcha.secret-key:}")
|
||||
private String secretKey;
|
||||
@Value("${recaptcha.secret-key:}")
|
||||
private String secretKey;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
@Override
|
||||
public boolean verify(String token) {
|
||||
if (token == null || token.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String url = "https://www.google.com/recaptcha/api/siteverify?secret={secret}&response={response}";
|
||||
try {
|
||||
ResponseEntity<Map> resp = restTemplate.postForEntity(url, null, Map.class, secretKey, token);
|
||||
Map body = resp.getBody();
|
||||
return body != null && Boolean.TRUE.equals(body.get("success"));
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public boolean verify(String token) {
|
||||
if (token == null || token.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String url =
|
||||
"https://www.google.com/recaptcha/api/siteverify?secret={secret}&response={response}";
|
||||
try {
|
||||
ResponseEntity<Map> resp = restTemplate.postForEntity(url, null, Map.class, secretKey, token);
|
||||
Map body = resp.getBody();
|
||||
return body != null && Boolean.TRUE.equals(body.get("success"));
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,17 +9,18 @@ import org.springframework.stereotype.Service;
|
||||
*/
|
||||
@Service
|
||||
public class RegisterModeService {
|
||||
private RegisterMode registerMode;
|
||||
|
||||
public RegisterModeService(@Value("${app.register.mode:WHITELIST}") RegisterMode registerMode) {
|
||||
this.registerMode = registerMode;
|
||||
}
|
||||
private RegisterMode registerMode;
|
||||
|
||||
public RegisterMode getRegisterMode() {
|
||||
return registerMode;
|
||||
}
|
||||
public RegisterModeService(@Value("${app.register.mode:WHITELIST}") RegisterMode registerMode) {
|
||||
this.registerMode = registerMode;
|
||||
}
|
||||
|
||||
public void setRegisterMode(RegisterMode mode) {
|
||||
this.registerMode = mode;
|
||||
}
|
||||
public RegisterMode getRegisterMode() {
|
||||
return registerMode;
|
||||
}
|
||||
|
||||
public void setRegisterMode(RegisterMode mode) {
|
||||
this.registerMode = mode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,43 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class ResendEmailSender extends EmailSender {
|
||||
|
||||
@Value("${resend.api.key}")
|
||||
private String apiKey;
|
||||
@Value("${resend.api.key}")
|
||||
private String apiKey;
|
||||
|
||||
@Value("${resend.from.email}")
|
||||
private String fromEmail;
|
||||
@Value("${resend.from.email}")
|
||||
private String fromEmail;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
@Override
|
||||
@Async("notificationExecutor")
|
||||
public void sendEmail(String to, String subject, String text) {
|
||||
String url = "https://api.resend.com/emails"; // hypothetical endpoint
|
||||
@Override
|
||||
@Async("notificationExecutor")
|
||||
public void sendEmail(String to, String subject, String text) {
|
||||
String url = "https://api.resend.com/emails"; // hypothetical endpoint
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
|
||||
Map<String, String> body = new HashMap<>();
|
||||
body.put("to", to);
|
||||
body.put("subject", subject);
|
||||
body.put("text", text);
|
||||
body.put("from", "openisle <" + fromEmail + ">");
|
||||
Map<String, String> body = new HashMap<>();
|
||||
body.put("to", to);
|
||||
body.put("subject", subject);
|
||||
body.put("text", text);
|
||||
body.put("from", "openisle <" + fromEmail + ">");
|
||||
|
||||
HttpEntity<Map<String, String>> entity = new HttpEntity<>(body, headers);
|
||||
restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
|
||||
}
|
||||
HttpEntity<Map<String, String>> entity = new HttpEntity<>(body, headers);
|
||||
restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,171 +1,176 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import com.openisle.model.Category;
|
||||
import com.openisle.model.Comment;
|
||||
import com.openisle.model.Post;
|
||||
import com.openisle.model.PostStatus;
|
||||
import com.openisle.model.Comment;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.model.Category;
|
||||
import com.openisle.model.Tag;
|
||||
import com.openisle.repository.PostRepository;
|
||||
import com.openisle.repository.CommentRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.CategoryRepository;
|
||||
import com.openisle.repository.CommentRepository;
|
||||
import com.openisle.repository.PostRepository;
|
||||
import com.openisle.repository.TagRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SearchService {
|
||||
private final UserRepository userRepository;
|
||||
private final PostRepository postRepository;
|
||||
private final CommentRepository commentRepository;
|
||||
private final CategoryRepository categoryRepository;
|
||||
private final TagRepository tagRepository;
|
||||
|
||||
@org.springframework.beans.factory.annotation.Value("${app.snippet-length:50}")
|
||||
private int snippetLength;
|
||||
private final UserRepository userRepository;
|
||||
private final PostRepository postRepository;
|
||||
private final CommentRepository commentRepository;
|
||||
private final CategoryRepository categoryRepository;
|
||||
private final TagRepository tagRepository;
|
||||
|
||||
public List<User> searchUsers(String keyword) {
|
||||
return userRepository.findByUsernameContainingIgnoreCase(keyword);
|
||||
@org.springframework.beans.factory.annotation.Value("${app.snippet-length:50}")
|
||||
private int snippetLength;
|
||||
|
||||
public List<User> searchUsers(String keyword) {
|
||||
return userRepository.findByUsernameContainingIgnoreCase(keyword);
|
||||
}
|
||||
|
||||
public List<Post> searchPosts(String keyword) {
|
||||
return postRepository.findByTitleContainingIgnoreCaseOrContentContainingIgnoreCaseAndStatus(
|
||||
keyword,
|
||||
keyword,
|
||||
PostStatus.PUBLISHED
|
||||
);
|
||||
}
|
||||
|
||||
public List<Post> searchPostsByContent(String keyword) {
|
||||
return postRepository.findByContentContainingIgnoreCaseAndStatus(keyword, PostStatus.PUBLISHED);
|
||||
}
|
||||
|
||||
public List<Post> searchPostsByTitle(String keyword) {
|
||||
return postRepository.findByTitleContainingIgnoreCaseAndStatus(keyword, PostStatus.PUBLISHED);
|
||||
}
|
||||
|
||||
public List<Comment> searchComments(String keyword) {
|
||||
return commentRepository.findByContentContainingIgnoreCase(keyword);
|
||||
}
|
||||
|
||||
public List<Category> searchCategories(String keyword) {
|
||||
return categoryRepository.findByNameContainingIgnoreCase(keyword);
|
||||
}
|
||||
|
||||
public List<Tag> searchTags(String keyword) {
|
||||
return tagRepository.findByNameContainingIgnoreCaseAndApprovedTrue(keyword);
|
||||
}
|
||||
|
||||
public List<SearchResult> globalSearch(String keyword) {
|
||||
Stream<SearchResult> users = searchUsers(keyword)
|
||||
.stream()
|
||||
.map(u ->
|
||||
new SearchResult("user", u.getId(), u.getUsername(), u.getIntroduction(), null, null)
|
||||
);
|
||||
|
||||
Stream<SearchResult> categories = searchCategories(keyword)
|
||||
.stream()
|
||||
.map(c ->
|
||||
new SearchResult("category", c.getId(), c.getName(), null, c.getDescription(), null)
|
||||
);
|
||||
|
||||
Stream<SearchResult> tags = searchTags(keyword)
|
||||
.stream()
|
||||
.map(t -> new SearchResult("tag", t.getId(), t.getName(), null, t.getDescription(), null));
|
||||
|
||||
// Merge post results while removing duplicates between search by content
|
||||
// and search by title
|
||||
List<SearchResult> mergedPosts = Stream.concat(
|
||||
searchPosts(keyword)
|
||||
.stream()
|
||||
.map(p ->
|
||||
new SearchResult(
|
||||
"post",
|
||||
p.getId(),
|
||||
p.getTitle(),
|
||||
p.getCategory() != null ? p.getCategory().getName() : null,
|
||||
extractSnippet(p.getContent(), keyword, false),
|
||||
null
|
||||
)
|
||||
),
|
||||
searchPostsByTitle(keyword)
|
||||
.stream()
|
||||
.map(p ->
|
||||
new SearchResult(
|
||||
"post_title",
|
||||
p.getId(),
|
||||
p.getTitle(),
|
||||
p.getCategory() != null ? p.getCategory().getName() : null,
|
||||
extractSnippet(p.getContent(), keyword, true),
|
||||
null
|
||||
)
|
||||
)
|
||||
)
|
||||
.collect(
|
||||
java.util.stream.Collectors.toMap(
|
||||
SearchResult::id,
|
||||
sr -> sr,
|
||||
(a, b) -> a,
|
||||
java.util.LinkedHashMap::new
|
||||
)
|
||||
)
|
||||
.values()
|
||||
.stream()
|
||||
.toList();
|
||||
|
||||
Stream<SearchResult> comments = searchComments(keyword)
|
||||
.stream()
|
||||
.map(c ->
|
||||
new SearchResult(
|
||||
"comment",
|
||||
c.getId(),
|
||||
c.getPost().getTitle(),
|
||||
c.getAuthor().getUsername(),
|
||||
extractSnippet(c.getContent(), keyword, false),
|
||||
c.getPost().getId()
|
||||
)
|
||||
);
|
||||
|
||||
return Stream.of(users, categories, tags, mergedPosts.stream(), comments)
|
||||
.flatMap(s -> s)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private String extractSnippet(String content, String keyword, boolean fromStart) {
|
||||
if (content == null) return "";
|
||||
int limit = snippetLength;
|
||||
if (fromStart) {
|
||||
if (limit < 0) {
|
||||
return content;
|
||||
}
|
||||
return content.length() > limit ? content.substring(0, limit) : content;
|
||||
}
|
||||
|
||||
public List<Post> searchPosts(String keyword) {
|
||||
return postRepository
|
||||
.findByTitleContainingIgnoreCaseOrContentContainingIgnoreCaseAndStatus(keyword, keyword, PostStatus.PUBLISHED);
|
||||
String lower = content.toLowerCase();
|
||||
String kw = keyword.toLowerCase();
|
||||
int idx = lower.indexOf(kw);
|
||||
if (idx == -1) {
|
||||
if (limit < 0) {
|
||||
return content;
|
||||
}
|
||||
return content.length() > limit ? content.substring(0, limit) : content;
|
||||
}
|
||||
|
||||
public List<Post> searchPostsByContent(String keyword) {
|
||||
return postRepository
|
||||
.findByContentContainingIgnoreCaseAndStatus(keyword, PostStatus.PUBLISHED);
|
||||
int start = Math.max(0, idx - 20);
|
||||
int end = Math.min(content.length(), idx + kw.length() + 20);
|
||||
String snippet = content.substring(start, end);
|
||||
if (limit >= 0 && snippet.length() > limit) {
|
||||
snippet = snippet.substring(0, limit);
|
||||
}
|
||||
return snippet;
|
||||
}
|
||||
|
||||
public List<Post> searchPostsByTitle(String keyword) {
|
||||
return postRepository
|
||||
.findByTitleContainingIgnoreCaseAndStatus(keyword, PostStatus.PUBLISHED);
|
||||
}
|
||||
|
||||
public List<Comment> searchComments(String keyword) {
|
||||
return commentRepository.findByContentContainingIgnoreCase(keyword);
|
||||
}
|
||||
|
||||
public List<Category> searchCategories(String keyword) {
|
||||
return categoryRepository.findByNameContainingIgnoreCase(keyword);
|
||||
}
|
||||
|
||||
public List<Tag> searchTags(String keyword) {
|
||||
return tagRepository.findByNameContainingIgnoreCaseAndApprovedTrue(keyword);
|
||||
}
|
||||
|
||||
public List<SearchResult> globalSearch(String keyword) {
|
||||
Stream<SearchResult> users = searchUsers(keyword).stream()
|
||||
.map(u -> new SearchResult(
|
||||
"user",
|
||||
u.getId(),
|
||||
u.getUsername(),
|
||||
u.getIntroduction(),
|
||||
null,
|
||||
null
|
||||
));
|
||||
|
||||
Stream<SearchResult> categories = searchCategories(keyword).stream()
|
||||
.map(c -> new SearchResult(
|
||||
"category",
|
||||
c.getId(),
|
||||
c.getName(),
|
||||
null,
|
||||
c.getDescription(),
|
||||
null
|
||||
));
|
||||
|
||||
Stream<SearchResult> tags = searchTags(keyword).stream()
|
||||
.map(t -> new SearchResult(
|
||||
"tag",
|
||||
t.getId(),
|
||||
t.getName(),
|
||||
null,
|
||||
t.getDescription(),
|
||||
null
|
||||
));
|
||||
|
||||
// Merge post results while removing duplicates between search by content
|
||||
// and search by title
|
||||
List<SearchResult> mergedPosts = Stream.concat(
|
||||
searchPosts(keyword).stream()
|
||||
.map(p -> new SearchResult(
|
||||
"post",
|
||||
p.getId(),
|
||||
p.getTitle(),
|
||||
p.getCategory() != null ? p.getCategory().getName() : null,
|
||||
extractSnippet(p.getContent(), keyword, false),
|
||||
null
|
||||
)),
|
||||
searchPostsByTitle(keyword).stream()
|
||||
.map(p -> new SearchResult(
|
||||
"post_title",
|
||||
p.getId(),
|
||||
p.getTitle(),
|
||||
p.getCategory() != null ? p.getCategory().getName() : null,
|
||||
extractSnippet(p.getContent(), keyword, true),
|
||||
null
|
||||
))
|
||||
)
|
||||
.collect(java.util.stream.Collectors.toMap(
|
||||
SearchResult::id,
|
||||
sr -> sr,
|
||||
(a, b) -> a,
|
||||
java.util.LinkedHashMap::new
|
||||
))
|
||||
.values()
|
||||
.stream()
|
||||
.toList();
|
||||
|
||||
Stream<SearchResult> comments = searchComments(keyword).stream()
|
||||
.map(c -> new SearchResult(
|
||||
"comment",
|
||||
c.getId(),
|
||||
c.getPost().getTitle(),
|
||||
c.getAuthor().getUsername(),
|
||||
extractSnippet(c.getContent(), keyword, false),
|
||||
c.getPost().getId()
|
||||
));
|
||||
|
||||
return Stream.of(users, categories, tags, mergedPosts.stream(), comments)
|
||||
.flatMap(s -> s)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private String extractSnippet(String content, String keyword, boolean fromStart) {
|
||||
if (content == null) return "";
|
||||
int limit = snippetLength;
|
||||
if (fromStart) {
|
||||
if (limit < 0) {
|
||||
return content;
|
||||
}
|
||||
return content.length() > limit ? content.substring(0, limit) : content;
|
||||
}
|
||||
String lower = content.toLowerCase();
|
||||
String kw = keyword.toLowerCase();
|
||||
int idx = lower.indexOf(kw);
|
||||
if (idx == -1) {
|
||||
if (limit < 0) {
|
||||
return content;
|
||||
}
|
||||
return content.length() > limit ? content.substring(0, limit) : content;
|
||||
}
|
||||
int start = Math.max(0, idx - 20);
|
||||
int end = Math.min(content.length(), idx + kw.length() + 20);
|
||||
String snippet = content.substring(start, end);
|
||||
if (limit >= 0 && snippet.length() > limit) {
|
||||
snippet = snippet.substring(0, limit);
|
||||
}
|
||||
return snippet;
|
||||
}
|
||||
|
||||
public record SearchResult(String type, Long id, String text, String subText, String extra, Long postId) {}
|
||||
public record SearchResult(
|
||||
String type,
|
||||
Long id,
|
||||
String text,
|
||||
String subText,
|
||||
String extra,
|
||||
Long postId
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,68 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import com.openisle.repository.UserRepository;
|
||||
import com.openisle.repository.PostRepository;
|
||||
import com.openisle.repository.CommentRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.openisle.repository.PostRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import java.time.LocalDate;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class StatService {
|
||||
private final UserRepository userRepository;
|
||||
private final PostRepository postRepository;
|
||||
private final CommentRepository commentRepository;
|
||||
|
||||
private Map<LocalDate, Long> toDateMap(LocalDate start, LocalDate end, java.util.List<Object[]> list) {
|
||||
Map<LocalDate, Long> result = new LinkedHashMap<>();
|
||||
for (var obj : list) {
|
||||
Object dateObj = obj[0];
|
||||
LocalDate d;
|
||||
if (dateObj instanceof java.sql.Date sqlDate) {
|
||||
d = sqlDate.toLocalDate();
|
||||
} else if (dateObj instanceof LocalDate localDate) {
|
||||
d = localDate;
|
||||
} else {
|
||||
d = LocalDate.parse(dateObj.toString());
|
||||
}
|
||||
Long c = ((Number) obj[1]).longValue();
|
||||
result.put(d, c);
|
||||
}
|
||||
for (LocalDate d = start; !d.isAfter(end); d = d.plusDays(1)) {
|
||||
result.putIfAbsent(d, 0L);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private final UserRepository userRepository;
|
||||
private final PostRepository postRepository;
|
||||
private final CommentRepository commentRepository;
|
||||
|
||||
public Map<LocalDate, Long> countNewUsersRange(LocalDate start, LocalDate end) {
|
||||
java.util.List<Object[]> list = userRepository.countDailyRange(start.atStartOfDay(), end.plusDays(1).atStartOfDay());
|
||||
return toDateMap(start, end, list);
|
||||
private Map<LocalDate, Long> toDateMap(
|
||||
LocalDate start,
|
||||
LocalDate end,
|
||||
java.util.List<Object[]> list
|
||||
) {
|
||||
Map<LocalDate, Long> result = new LinkedHashMap<>();
|
||||
for (var obj : list) {
|
||||
Object dateObj = obj[0];
|
||||
LocalDate d;
|
||||
if (dateObj instanceof java.sql.Date sqlDate) {
|
||||
d = sqlDate.toLocalDate();
|
||||
} else if (dateObj instanceof LocalDate localDate) {
|
||||
d = localDate;
|
||||
} else {
|
||||
d = LocalDate.parse(dateObj.toString());
|
||||
}
|
||||
Long c = ((Number) obj[1]).longValue();
|
||||
result.put(d, c);
|
||||
}
|
||||
for (LocalDate d = start; !d.isAfter(end); d = d.plusDays(1)) {
|
||||
result.putIfAbsent(d, 0L);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<LocalDate, Long> countPostsRange(LocalDate start, LocalDate end) {
|
||||
java.util.List<Object[]> list = postRepository.countDailyRange(start.atStartOfDay(), end.plusDays(1).atStartOfDay());
|
||||
return toDateMap(start, end, list);
|
||||
}
|
||||
public Map<LocalDate, Long> countNewUsersRange(LocalDate start, LocalDate end) {
|
||||
java.util.List<Object[]> list = userRepository.countDailyRange(
|
||||
start.atStartOfDay(),
|
||||
end.plusDays(1).atStartOfDay()
|
||||
);
|
||||
return toDateMap(start, end, list);
|
||||
}
|
||||
|
||||
public Map<LocalDate, Long> countCommentsRange(LocalDate start, LocalDate end) {
|
||||
java.util.List<Object[]> list = commentRepository.countDailyRange(start.atStartOfDay(), end.plusDays(1).atStartOfDay());
|
||||
return toDateMap(start, end, list);
|
||||
}
|
||||
public Map<LocalDate, Long> countPostsRange(LocalDate start, LocalDate end) {
|
||||
java.util.List<Object[]> list = postRepository.countDailyRange(
|
||||
start.atStartOfDay(),
|
||||
end.plusDays(1).atStartOfDay()
|
||||
);
|
||||
return toDateMap(start, end, list);
|
||||
}
|
||||
|
||||
public Map<LocalDate, Long> countCommentsRange(LocalDate start, LocalDate end) {
|
||||
java.util.List<Object[]> list = commentRepository.countDailyRange(
|
||||
start.atStartOfDay(),
|
||||
end.plusDays(1).atStartOfDay()
|
||||
);
|
||||
return toDateMap(start, end, list);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,153 +2,194 @@ package com.openisle.service;
|
||||
|
||||
import com.openisle.model.*;
|
||||
import com.openisle.repository.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SubscriptionService {
|
||||
private final PostSubscriptionRepository postSubRepo;
|
||||
private final CommentSubscriptionRepository commentSubRepo;
|
||||
private final UserSubscriptionRepository userSubRepo;
|
||||
private final UserRepository userRepo;
|
||||
private final PostRepository postRepo;
|
||||
private final CommentRepository commentRepo;
|
||||
private final NotificationService notificationService;
|
||||
|
||||
public void subscribePost(String username, Long postId) {
|
||||
User user = userRepo.findByUsername(username).orElseThrow();
|
||||
Post post = postRepo.findById(postId).orElseThrow();
|
||||
postSubRepo.findByUserAndPost(user, post).orElseGet(() -> {
|
||||
PostSubscription ps = new PostSubscription();
|
||||
ps.setUser(user);
|
||||
ps.setPost(post);
|
||||
if (!user.getId().equals(post.getAuthor().getId())) {
|
||||
notificationService.createNotification(post.getAuthor(),
|
||||
NotificationType.POST_SUBSCRIBED, post, null, null, user, null, null);
|
||||
}
|
||||
return postSubRepo.save(ps);
|
||||
});
|
||||
}
|
||||
private final PostSubscriptionRepository postSubRepo;
|
||||
private final CommentSubscriptionRepository commentSubRepo;
|
||||
private final UserSubscriptionRepository userSubRepo;
|
||||
private final UserRepository userRepo;
|
||||
private final PostRepository postRepo;
|
||||
private final CommentRepository commentRepo;
|
||||
private final NotificationService notificationService;
|
||||
|
||||
public void unsubscribePost(String username, Long postId) {
|
||||
User user = userRepo.findByUsername(username).orElseThrow();
|
||||
Post post = postRepo.findById(postId).orElseThrow();
|
||||
postSubRepo.findByUserAndPost(user, post).ifPresent(ps -> {
|
||||
postSubRepo.delete(ps);
|
||||
if (!user.getId().equals(post.getAuthor().getId())) {
|
||||
notificationService.createNotification(post.getAuthor(),
|
||||
NotificationType.POST_UNSUBSCRIBED, post, null, null, user, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void subscribeComment(String username, Long commentId) {
|
||||
User user = userRepo.findByUsername(username).orElseThrow();
|
||||
Comment comment = commentRepo.findById(commentId).orElseThrow();
|
||||
commentSubRepo.findByUserAndComment(user, comment).orElseGet(() -> {
|
||||
CommentSubscription cs = new CommentSubscription();
|
||||
cs.setUser(user);
|
||||
cs.setComment(comment);
|
||||
return commentSubRepo.save(cs);
|
||||
});
|
||||
}
|
||||
|
||||
public void unsubscribeComment(String username, Long commentId) {
|
||||
User user = userRepo.findByUsername(username).orElseThrow();
|
||||
Comment comment = commentRepo.findById(commentId).orElseThrow();
|
||||
commentSubRepo.findByUserAndComment(user, comment).ifPresent(commentSubRepo::delete);
|
||||
}
|
||||
|
||||
public void subscribeUser(String username, String targetName) {
|
||||
if (username.equals(targetName)) return;
|
||||
User subscriber = userRepo.findByUsername(username).orElseThrow();
|
||||
User target = findUser(targetName).orElseThrow();
|
||||
userSubRepo.findBySubscriberAndTarget(subscriber, target).orElseGet(() -> {
|
||||
UserSubscription us = new UserSubscription();
|
||||
us.setSubscriber(subscriber);
|
||||
us.setTarget(target);
|
||||
notificationService.createNotification(target,
|
||||
NotificationType.USER_FOLLOWED, null, null, null, subscriber, null, null);
|
||||
return userSubRepo.save(us);
|
||||
});
|
||||
}
|
||||
|
||||
public void unsubscribeUser(String username, String targetName) {
|
||||
User subscriber = userRepo.findByUsername(username).orElseThrow();
|
||||
User target = findUser(targetName).orElseThrow();
|
||||
userSubRepo.findBySubscriberAndTarget(subscriber, target).ifPresent(us -> {
|
||||
userSubRepo.delete(us);
|
||||
notificationService.createNotification(target,
|
||||
NotificationType.USER_UNFOLLOWED, null, null, null, subscriber, null, null);
|
||||
});
|
||||
}
|
||||
|
||||
public List<User> getSubscribedUsers(String username) {
|
||||
User user = userRepo.findByUsername(username).orElseThrow();
|
||||
return userSubRepo.findBySubscriber(user).stream().map(UserSubscription::getTarget).toList();
|
||||
}
|
||||
|
||||
public List<User> getSubscribers(String username) {
|
||||
User user = userRepo.findByUsername(username).orElseThrow();
|
||||
return userSubRepo.findByTarget(user).stream().map(UserSubscription::getSubscriber).toList();
|
||||
}
|
||||
|
||||
public List<User> getPostSubscribers(Long postId) {
|
||||
Post post = postRepo.findById(postId).orElseThrow();
|
||||
return postSubRepo.findByPost(post).stream().map(PostSubscription::getUser).toList();
|
||||
}
|
||||
|
||||
public List<User> getCommentSubscribers(Long commentId) {
|
||||
Comment c = commentRepo.findById(commentId).orElseThrow();
|
||||
return commentSubRepo.findByComment(c).stream().map(CommentSubscription::getUser).toList();
|
||||
}
|
||||
|
||||
public List<Post> getSubscribedPosts(String username) {
|
||||
User user = userRepo.findByUsername(username).orElseThrow();
|
||||
return postSubRepo.findByUser(user).stream().map(PostSubscription::getPost).toList();
|
||||
}
|
||||
|
||||
|
||||
public long countSubscribers(String username) {
|
||||
User user = userRepo.findByUsername(username).orElseThrow();
|
||||
return userSubRepo.countByTarget(user);
|
||||
}
|
||||
|
||||
public long countSubscribed(String username) {
|
||||
User user = userRepo.findByUsername(username).orElseThrow();
|
||||
return userSubRepo.countBySubscriber(user);
|
||||
}
|
||||
|
||||
public boolean isSubscribed(String subscriberName, String targetName) {
|
||||
if (subscriberName == null || targetName == null || subscriberName.equals(targetName)) {
|
||||
return false;
|
||||
public void subscribePost(String username, Long postId) {
|
||||
User user = userRepo.findByUsername(username).orElseThrow();
|
||||
Post post = postRepo.findById(postId).orElseThrow();
|
||||
postSubRepo
|
||||
.findByUserAndPost(user, post)
|
||||
.orElseGet(() -> {
|
||||
PostSubscription ps = new PostSubscription();
|
||||
ps.setUser(user);
|
||||
ps.setPost(post);
|
||||
if (!user.getId().equals(post.getAuthor().getId())) {
|
||||
notificationService.createNotification(
|
||||
post.getAuthor(),
|
||||
NotificationType.POST_SUBSCRIBED,
|
||||
post,
|
||||
null,
|
||||
null,
|
||||
user,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
Optional<User> subscriber = userRepo.findByUsername(subscriberName);
|
||||
Optional<User> target = findUser(targetName);
|
||||
if (subscriber.isEmpty() || target.isEmpty()) {
|
||||
// 修改个人信息会出现,先不抛出错误
|
||||
return false;
|
||||
}
|
||||
return userSubRepo.findBySubscriberAndTarget(subscriber.get(), target.get()).isPresent();
|
||||
}
|
||||
return postSubRepo.save(ps);
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isPostSubscribed(String username, Long postId) {
|
||||
if (username == null || postId == null) {
|
||||
return false;
|
||||
public void unsubscribePost(String username, Long postId) {
|
||||
User user = userRepo.findByUsername(username).orElseThrow();
|
||||
Post post = postRepo.findById(postId).orElseThrow();
|
||||
postSubRepo
|
||||
.findByUserAndPost(user, post)
|
||||
.ifPresent(ps -> {
|
||||
postSubRepo.delete(ps);
|
||||
if (!user.getId().equals(post.getAuthor().getId())) {
|
||||
notificationService.createNotification(
|
||||
post.getAuthor(),
|
||||
NotificationType.POST_UNSUBSCRIBED,
|
||||
post,
|
||||
null,
|
||||
null,
|
||||
user,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
User user = userRepo.findByUsername(username).orElseThrow();
|
||||
Post post = postRepo.findById(postId).orElseThrow();
|
||||
return postSubRepo.findByUserAndPost(user, post).isPresent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Optional<User> findUser(String identifier) {
|
||||
if (identifier.matches("\\d+")) {
|
||||
return userRepo.findById(Long.parseLong(identifier));
|
||||
}
|
||||
return userRepo.findByUsername(identifier);
|
||||
public void subscribeComment(String username, Long commentId) {
|
||||
User user = userRepo.findByUsername(username).orElseThrow();
|
||||
Comment comment = commentRepo.findById(commentId).orElseThrow();
|
||||
commentSubRepo
|
||||
.findByUserAndComment(user, comment)
|
||||
.orElseGet(() -> {
|
||||
CommentSubscription cs = new CommentSubscription();
|
||||
cs.setUser(user);
|
||||
cs.setComment(comment);
|
||||
return commentSubRepo.save(cs);
|
||||
});
|
||||
}
|
||||
|
||||
public void unsubscribeComment(String username, Long commentId) {
|
||||
User user = userRepo.findByUsername(username).orElseThrow();
|
||||
Comment comment = commentRepo.findById(commentId).orElseThrow();
|
||||
commentSubRepo.findByUserAndComment(user, comment).ifPresent(commentSubRepo::delete);
|
||||
}
|
||||
|
||||
public void subscribeUser(String username, String targetName) {
|
||||
if (username.equals(targetName)) return;
|
||||
User subscriber = userRepo.findByUsername(username).orElseThrow();
|
||||
User target = findUser(targetName).orElseThrow();
|
||||
userSubRepo
|
||||
.findBySubscriberAndTarget(subscriber, target)
|
||||
.orElseGet(() -> {
|
||||
UserSubscription us = new UserSubscription();
|
||||
us.setSubscriber(subscriber);
|
||||
us.setTarget(target);
|
||||
notificationService.createNotification(
|
||||
target,
|
||||
NotificationType.USER_FOLLOWED,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
subscriber,
|
||||
null,
|
||||
null
|
||||
);
|
||||
return userSubRepo.save(us);
|
||||
});
|
||||
}
|
||||
|
||||
public void unsubscribeUser(String username, String targetName) {
|
||||
User subscriber = userRepo.findByUsername(username).orElseThrow();
|
||||
User target = findUser(targetName).orElseThrow();
|
||||
userSubRepo
|
||||
.findBySubscriberAndTarget(subscriber, target)
|
||||
.ifPresent(us -> {
|
||||
userSubRepo.delete(us);
|
||||
notificationService.createNotification(
|
||||
target,
|
||||
NotificationType.USER_UNFOLLOWED,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
subscriber,
|
||||
null,
|
||||
null
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public List<User> getSubscribedUsers(String username) {
|
||||
User user = userRepo.findByUsername(username).orElseThrow();
|
||||
return userSubRepo.findBySubscriber(user).stream().map(UserSubscription::getTarget).toList();
|
||||
}
|
||||
|
||||
public List<User> getSubscribers(String username) {
|
||||
User user = userRepo.findByUsername(username).orElseThrow();
|
||||
return userSubRepo.findByTarget(user).stream().map(UserSubscription::getSubscriber).toList();
|
||||
}
|
||||
|
||||
public List<User> getPostSubscribers(Long postId) {
|
||||
Post post = postRepo.findById(postId).orElseThrow();
|
||||
return postSubRepo.findByPost(post).stream().map(PostSubscription::getUser).toList();
|
||||
}
|
||||
|
||||
public List<User> getCommentSubscribers(Long commentId) {
|
||||
Comment c = commentRepo.findById(commentId).orElseThrow();
|
||||
return commentSubRepo.findByComment(c).stream().map(CommentSubscription::getUser).toList();
|
||||
}
|
||||
|
||||
public List<Post> getSubscribedPosts(String username) {
|
||||
User user = userRepo.findByUsername(username).orElseThrow();
|
||||
return postSubRepo.findByUser(user).stream().map(PostSubscription::getPost).toList();
|
||||
}
|
||||
|
||||
public long countSubscribers(String username) {
|
||||
User user = userRepo.findByUsername(username).orElseThrow();
|
||||
return userSubRepo.countByTarget(user);
|
||||
}
|
||||
|
||||
public long countSubscribed(String username) {
|
||||
User user = userRepo.findByUsername(username).orElseThrow();
|
||||
return userSubRepo.countBySubscriber(user);
|
||||
}
|
||||
|
||||
public boolean isSubscribed(String subscriberName, String targetName) {
|
||||
if (subscriberName == null || targetName == null || subscriberName.equals(targetName)) {
|
||||
return false;
|
||||
}
|
||||
Optional<User> subscriber = userRepo.findByUsername(subscriberName);
|
||||
Optional<User> target = findUser(targetName);
|
||||
if (subscriber.isEmpty() || target.isEmpty()) {
|
||||
// 修改个人信息会出现,先不抛出错误
|
||||
return false;
|
||||
}
|
||||
return userSubRepo.findBySubscriberAndTarget(subscriber.get(), target.get()).isPresent();
|
||||
}
|
||||
|
||||
public boolean isPostSubscribed(String username, Long postId) {
|
||||
if (username == null || postId == null) {
|
||||
return false;
|
||||
}
|
||||
User user = userRepo.findByUsername(username).orElseThrow();
|
||||
Post post = postRepo.findById(postId).orElseThrow();
|
||||
return postSubRepo.findByUserAndPost(user, post).isPresent();
|
||||
}
|
||||
|
||||
private Optional<User> findUser(String identifier) {
|
||||
if (identifier.matches("\\d+")) {
|
||||
return userRepo.findById(Long.parseLong(identifier));
|
||||
}
|
||||
return userRepo.findByUsername(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,133 +5,152 @@ import com.openisle.model.Tag;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.TagRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TagService {
|
||||
private final TagRepository tagRepository;
|
||||
private final TagValidator tagValidator;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@CacheEvict(value = CachingConfig.TAG_CACHE_NAME, allEntries = true)
|
||||
public Tag createTag(String name, String description, String icon, String smallIcon, boolean approved, String creatorUsername) {
|
||||
tagValidator.validate(name);
|
||||
Tag tag = new Tag();
|
||||
tag.setName(name);
|
||||
tag.setDescription(description);
|
||||
tag.setIcon(icon);
|
||||
tag.setSmallIcon(smallIcon);
|
||||
tag.setApproved(approved);
|
||||
if (creatorUsername != null) {
|
||||
User creator = userRepository.findByUsername(creatorUsername)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
tag.setCreator(creator);
|
||||
}
|
||||
return tagRepository.save(tag);
|
||||
private final TagRepository tagRepository;
|
||||
private final TagValidator tagValidator;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@CacheEvict(value = CachingConfig.TAG_CACHE_NAME, allEntries = true)
|
||||
public Tag createTag(
|
||||
String name,
|
||||
String description,
|
||||
String icon,
|
||||
String smallIcon,
|
||||
boolean approved,
|
||||
String creatorUsername
|
||||
) {
|
||||
tagValidator.validate(name);
|
||||
Tag tag = new Tag();
|
||||
tag.setName(name);
|
||||
tag.setDescription(description);
|
||||
tag.setIcon(icon);
|
||||
tag.setSmallIcon(smallIcon);
|
||||
tag.setApproved(approved);
|
||||
if (creatorUsername != null) {
|
||||
User creator = userRepository
|
||||
.findByUsername(creatorUsername)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
tag.setCreator(creator);
|
||||
}
|
||||
return tagRepository.save(tag);
|
||||
}
|
||||
|
||||
public Tag createTag(
|
||||
String name,
|
||||
String description,
|
||||
String icon,
|
||||
String smallIcon,
|
||||
boolean approved
|
||||
) {
|
||||
return createTag(name, description, icon, smallIcon, approved, null);
|
||||
}
|
||||
|
||||
public Tag createTag(String name, String description, String icon, String smallIcon) {
|
||||
return createTag(name, description, icon, smallIcon, true, null);
|
||||
}
|
||||
|
||||
@CacheEvict(value = CachingConfig.TAG_CACHE_NAME, allEntries = true)
|
||||
public Tag updateTag(Long id, String name, String description, String icon, String smallIcon) {
|
||||
Tag tag = tagRepository
|
||||
.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Tag not found"));
|
||||
if (name != null) {
|
||||
tagValidator.validate(name);
|
||||
tag.setName(name);
|
||||
}
|
||||
if (description != null) {
|
||||
tag.setDescription(description);
|
||||
}
|
||||
if (icon != null) {
|
||||
tag.setIcon(icon);
|
||||
}
|
||||
if (smallIcon != null) {
|
||||
tag.setSmallIcon(smallIcon);
|
||||
}
|
||||
return tagRepository.save(tag);
|
||||
}
|
||||
|
||||
@CacheEvict(value = CachingConfig.TAG_CACHE_NAME, allEntries = true)
|
||||
public void deleteTag(Long id) {
|
||||
tagRepository.deleteById(id);
|
||||
}
|
||||
|
||||
public Tag approveTag(Long id) {
|
||||
Tag tag = tagRepository
|
||||
.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Tag not found"));
|
||||
tag.setApproved(true);
|
||||
return tagRepository.save(tag);
|
||||
}
|
||||
|
||||
public List<Tag> listPendingTags() {
|
||||
return tagRepository.findByApproved(false);
|
||||
}
|
||||
|
||||
public Tag getTag(Long id) {
|
||||
return tagRepository
|
||||
.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Tag not found"));
|
||||
}
|
||||
|
||||
public List<Tag> listTags() {
|
||||
return tagRepository.findByApprovedTrue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 该方法每次首页加载都会访问,加入缓存
|
||||
* @param keyword
|
||||
* @return
|
||||
*/
|
||||
@Cacheable(
|
||||
value = CachingConfig.TAG_CACHE_NAME,
|
||||
key = "'searchTags:' + (#keyword ?: '')" //keyword为null的场合返回空
|
||||
)
|
||||
public List<Tag> searchTags(String keyword) {
|
||||
if (keyword == null || keyword.isBlank()) {
|
||||
return tagRepository.findByApprovedTrue();
|
||||
}
|
||||
|
||||
public Tag createTag(String name, String description, String icon, String smallIcon, boolean approved) {
|
||||
return createTag(name, description, icon, smallIcon, approved, null);
|
||||
}
|
||||
return tagRepository.findByNameContainingIgnoreCaseAndApprovedTrue(keyword);
|
||||
}
|
||||
|
||||
public Tag createTag(String name, String description, String icon, String smallIcon) {
|
||||
return createTag(name, description, icon, smallIcon, true, null);
|
||||
}
|
||||
public List<Tag> getRecentTagsByUser(String username, int limit) {
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Pageable pageable = PageRequest.of(0, limit);
|
||||
return tagRepository.findByCreatorOrderByCreatedAtDesc(user, pageable);
|
||||
}
|
||||
|
||||
@CacheEvict(value = CachingConfig.TAG_CACHE_NAME, allEntries = true)
|
||||
public Tag updateTag(Long id, String name, String description, String icon, String smallIcon) {
|
||||
Tag tag = tagRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Tag not found"));
|
||||
if (name != null) {
|
||||
tagValidator.validate(name);
|
||||
tag.setName(name);
|
||||
}
|
||||
if (description != null) {
|
||||
tag.setDescription(description);
|
||||
}
|
||||
if (icon != null) {
|
||||
tag.setIcon(icon);
|
||||
}
|
||||
if (smallIcon != null) {
|
||||
tag.setSmallIcon(smallIcon);
|
||||
}
|
||||
return tagRepository.save(tag);
|
||||
}
|
||||
public List<Tag> getTagsByUser(String username) {
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
return tagRepository.findByCreator(user);
|
||||
}
|
||||
|
||||
@CacheEvict(value = CachingConfig.TAG_CACHE_NAME, allEntries = true)
|
||||
public void deleteTag(Long id) {
|
||||
tagRepository.deleteById(id);
|
||||
}
|
||||
|
||||
public Tag approveTag(Long id) {
|
||||
Tag tag = tagRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Tag not found"));
|
||||
tag.setApproved(true);
|
||||
return tagRepository.save(tag);
|
||||
}
|
||||
|
||||
public List<Tag> listPendingTags() {
|
||||
return tagRepository.findByApproved(false);
|
||||
}
|
||||
|
||||
public Tag getTag(Long id) {
|
||||
return tagRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Tag not found"));
|
||||
}
|
||||
|
||||
public List<Tag> listTags() {
|
||||
return tagRepository.findByApprovedTrue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 该方法每次首页加载都会访问,加入缓存
|
||||
* @param keyword
|
||||
* @return
|
||||
*/
|
||||
@Cacheable(
|
||||
value = CachingConfig.TAG_CACHE_NAME,
|
||||
key = "'searchTags:' + (#keyword ?: '')"//keyword为null的场合返回空
|
||||
)
|
||||
public List<Tag> searchTags(String keyword) {
|
||||
if (keyword == null || keyword.isBlank()) {
|
||||
return tagRepository.findByApprovedTrue();
|
||||
}
|
||||
|
||||
return tagRepository.findByNameContainingIgnoreCaseAndApprovedTrue(keyword);
|
||||
}
|
||||
|
||||
public List<Tag> getRecentTagsByUser(String username, int limit) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Pageable pageable = PageRequest.of(0, limit);
|
||||
return tagRepository.findByCreatorOrderByCreatedAtDesc(user, pageable);
|
||||
}
|
||||
|
||||
public List<Tag> getTagsByUser(String username) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
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;
|
||||
/**
|
||||
* 获取检索用的标签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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import com.openisle.exception.FieldException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class TagValidator {
|
||||
private static final Pattern ALLOWED = Pattern.compile("^[A-Za-z0-9\\u4e00-\\u9fa5]+$");
|
||||
|
||||
public void validate(String name) {
|
||||
if (name == null || name.isBlank()) {
|
||||
throw new FieldException("name", "Tag name cannot be empty");
|
||||
}
|
||||
if (!ALLOWED.matcher(name).matches()) {
|
||||
throw new FieldException("name", "Tag name must be letters or numbers");
|
||||
}
|
||||
private static final Pattern ALLOWED = Pattern.compile("^[A-Za-z0-9\\u4e00-\\u9fa5]+$");
|
||||
|
||||
public void validate(String name) {
|
||||
if (name == null || name.isBlank()) {
|
||||
throw new FieldException("name", "Tag name cannot be empty");
|
||||
}
|
||||
if (!ALLOWED.matcher(name).matches()) {
|
||||
throw new FieldException("name", "Tag name must be letters or numbers");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,98 +5,108 @@ import com.openisle.model.RegisterMode;
|
||||
import com.openisle.model.Role;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.*;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TelegramAuthService {
|
||||
private final UserRepository userRepository;
|
||||
private final AvatarGenerator avatarGenerator;
|
||||
|
||||
@Value("${telegram.bot-token:}")
|
||||
private String botToken;
|
||||
private final UserRepository userRepository;
|
||||
private final AvatarGenerator avatarGenerator;
|
||||
|
||||
public Optional<AuthResult> authenticate(TelegramLoginRequest req, RegisterMode mode, boolean viaInvite) {
|
||||
try {
|
||||
if (botToken == null || botToken.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String dataCheckString = buildDataCheckString(req);
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] secretKey = md.digest(botToken.getBytes(StandardCharsets.UTF_8));
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(secretKey, "HmacSHA256"));
|
||||
byte[] hash = mac.doFinal(dataCheckString.getBytes(StandardCharsets.UTF_8));
|
||||
String hex = bytesToHex(hash);
|
||||
if (!hex.equalsIgnoreCase(req.getHash())) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String username = req.getUsername();
|
||||
String email = (username != null ? username : req.getId()) + "@telegram.org";
|
||||
String avatar = req.getPhotoUrl();
|
||||
return Optional.of(processUser(email, username, avatar, mode, viaInvite));
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
@Value("${telegram.bot-token:}")
|
||||
private String botToken;
|
||||
|
||||
public Optional<AuthResult> authenticate(
|
||||
TelegramLoginRequest req,
|
||||
RegisterMode mode,
|
||||
boolean viaInvite
|
||||
) {
|
||||
try {
|
||||
if (botToken == null || botToken.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String dataCheckString = buildDataCheckString(req);
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] secretKey = md.digest(botToken.getBytes(StandardCharsets.UTF_8));
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(secretKey, "HmacSHA256"));
|
||||
byte[] hash = mac.doFinal(dataCheckString.getBytes(StandardCharsets.UTF_8));
|
||||
String hex = bytesToHex(hash);
|
||||
if (!hex.equalsIgnoreCase(req.getHash())) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String username = req.getUsername();
|
||||
String email = (username != null ? username : req.getId()) + "@telegram.org";
|
||||
String avatar = req.getPhotoUrl();
|
||||
return Optional.of(processUser(email, username, avatar, mode, viaInvite));
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private String buildDataCheckString(TelegramLoginRequest req) {
|
||||
List<String> data = new ArrayList<>();
|
||||
if (req.getAuthDate() != null) data.add("auth_date=" + req.getAuthDate());
|
||||
if (req.getFirstName() != null) data.add("first_name=" + req.getFirstName());
|
||||
if (req.getId() != null) data.add("id=" + req.getId());
|
||||
if (req.getLastName() != null) data.add("last_name=" + req.getLastName());
|
||||
if (req.getPhotoUrl() != null) data.add("photo_url=" + req.getPhotoUrl());
|
||||
if (req.getUsername() != null) data.add("username=" + req.getUsername());
|
||||
Collections.sort(data);
|
||||
return String.join("\n", data);
|
||||
private String buildDataCheckString(TelegramLoginRequest req) {
|
||||
List<String> data = new ArrayList<>();
|
||||
if (req.getAuthDate() != null) data.add("auth_date=" + req.getAuthDate());
|
||||
if (req.getFirstName() != null) data.add("first_name=" + req.getFirstName());
|
||||
if (req.getId() != null) data.add("id=" + req.getId());
|
||||
if (req.getLastName() != null) data.add("last_name=" + req.getLastName());
|
||||
if (req.getPhotoUrl() != null) data.add("photo_url=" + req.getPhotoUrl());
|
||||
if (req.getUsername() != null) data.add("username=" + req.getUsername());
|
||||
Collections.sort(data);
|
||||
return String.join("\n", data);
|
||||
}
|
||||
|
||||
private String bytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String bytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private AuthResult processUser(String email, String username, String avatar, RegisterMode mode, boolean viaInvite) {
|
||||
Optional<User> existing = userRepository.findByEmail(email);
|
||||
if (existing.isPresent()) {
|
||||
User user = existing.get();
|
||||
if (!user.isVerified()) {
|
||||
user.setVerified(true);
|
||||
user.setVerificationCode(null);
|
||||
userRepository.save(user);
|
||||
}
|
||||
return new AuthResult(user, false);
|
||||
}
|
||||
String baseUsername = username != null ? username : email.split("@")[0];
|
||||
String finalUsername = baseUsername;
|
||||
int suffix = 1;
|
||||
while (userRepository.findByUsername(finalUsername).isPresent()) {
|
||||
finalUsername = baseUsername + suffix++;
|
||||
}
|
||||
User user = new User();
|
||||
user.setUsername(finalUsername);
|
||||
user.setEmail(email);
|
||||
user.setPassword("");
|
||||
user.setRole(Role.USER);
|
||||
private AuthResult processUser(
|
||||
String email,
|
||||
String username,
|
||||
String avatar,
|
||||
RegisterMode mode,
|
||||
boolean viaInvite
|
||||
) {
|
||||
Optional<User> existing = userRepository.findByEmail(email);
|
||||
if (existing.isPresent()) {
|
||||
User user = existing.get();
|
||||
if (!user.isVerified()) {
|
||||
user.setVerified(true);
|
||||
user.setApproved(mode == RegisterMode.DIRECT || viaInvite);
|
||||
if (avatar != null) {
|
||||
user.setAvatar(avatar);
|
||||
} else {
|
||||
user.setAvatar(avatarGenerator.generate(finalUsername));
|
||||
}
|
||||
return new AuthResult(userRepository.save(user), true);
|
||||
user.setVerificationCode(null);
|
||||
userRepository.save(user);
|
||||
}
|
||||
return new AuthResult(user, false);
|
||||
}
|
||||
String baseUsername = username != null ? username : email.split("@")[0];
|
||||
String finalUsername = baseUsername;
|
||||
int suffix = 1;
|
||||
while (userRepository.findByUsername(finalUsername).isPresent()) {
|
||||
finalUsername = baseUsername + suffix++;
|
||||
}
|
||||
User user = new User();
|
||||
user.setUsername(finalUsername);
|
||||
user.setEmail(email);
|
||||
user.setPassword("");
|
||||
user.setRole(Role.USER);
|
||||
user.setVerified(true);
|
||||
user.setApproved(mode == RegisterMode.DIRECT || viaInvite);
|
||||
if (avatar != null) {
|
||||
user.setAvatar(avatar);
|
||||
} else {
|
||||
user.setAvatar(avatarGenerator.generate(finalUsername));
|
||||
}
|
||||
return new AuthResult(userRepository.save(user), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,142 +5,157 @@ import com.openisle.model.RegisterMode;
|
||||
import com.openisle.model.Role;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.Base64;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TwitterAuthService {
|
||||
private final UserRepository userRepository;
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
private static final Logger logger = LoggerFactory.getLogger(TwitterAuthService.class);
|
||||
|
||||
@Value("${twitter.client-id:}")
|
||||
private String clientId;
|
||||
private final UserRepository userRepository;
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
private static final Logger logger = LoggerFactory.getLogger(TwitterAuthService.class);
|
||||
|
||||
@Value("${twitter.client-secret:}")
|
||||
private String clientSecret;
|
||||
@Value("${twitter.client-id:}")
|
||||
private String clientId;
|
||||
|
||||
public Optional<AuthResult> authenticate(
|
||||
String code,
|
||||
String codeVerifier,
|
||||
RegisterMode mode,
|
||||
String redirectUri,
|
||||
boolean viaInvite) {
|
||||
@Value("${twitter.client-secret:}")
|
||||
private String clientSecret;
|
||||
|
||||
logger.debug("Starting authentication with code {} and verifier {}", code, codeVerifier);
|
||||
public Optional<AuthResult> authenticate(
|
||||
String code,
|
||||
String codeVerifier,
|
||||
RegisterMode mode,
|
||||
String redirectUri,
|
||||
boolean viaInvite
|
||||
) {
|
||||
logger.debug("Starting authentication with code {} and verifier {}", code, codeVerifier);
|
||||
|
||||
// 1. 交换 token
|
||||
String tokenUrl = "https://api.twitter.com/2/oauth2/token";
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
headers.setAccept(List.of(MediaType.APPLICATION_JSON));
|
||||
if (!clientId.isEmpty() && !clientSecret.isEmpty()) {
|
||||
String credentials = clientId + ":" + clientSecret;
|
||||
String authHeader = "Basic " + Base64.getEncoder()
|
||||
.encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
|
||||
headers.set(HttpHeaders.AUTHORIZATION, authHeader);
|
||||
}
|
||||
|
||||
// Twitter PKCE 要求的五个参数
|
||||
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
|
||||
body.add("client_id", clientId);
|
||||
body.add("grant_type", "authorization_code");
|
||||
body.add("code", code);
|
||||
body.add("code_verifier", codeVerifier);
|
||||
body.add("redirect_uri", redirectUri); // 一律必填
|
||||
// 如果你的 app 属于机密客户端,必须带 client_secret
|
||||
body.add("client_secret", clientSecret);
|
||||
|
||||
ResponseEntity<JsonNode> tokenRes;
|
||||
try {
|
||||
logger.debug("Requesting token from {}", tokenUrl);
|
||||
tokenRes = restTemplate.postForEntity(tokenUrl, new HttpEntity<>(body, headers), JsonNode.class);
|
||||
logger.debug("Token response: {}", tokenRes.getBody());
|
||||
} catch (HttpClientErrorException e) {
|
||||
logger.warn("Token request failed with status {} and body {}", e.getStatusCode(), e.getResponseBodyAsString());
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
JsonNode tokenJson = tokenRes.getBody();
|
||||
if (tokenJson == null || !tokenJson.hasNonNull("access_token")) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String accessToken = tokenJson.get("access_token").asText();
|
||||
|
||||
// 2. 拉取用户信息
|
||||
HttpHeaders authHeaders = new HttpHeaders();
|
||||
authHeaders.setBearerAuth(accessToken);
|
||||
ResponseEntity<JsonNode> userRes;
|
||||
try {
|
||||
logger.debug("Fetching user info with access token");
|
||||
userRes = restTemplate.exchange(
|
||||
"https://api.twitter.com/2/users/me?user.fields=profile_image_url",
|
||||
HttpMethod.GET,
|
||||
new HttpEntity<>(authHeaders),
|
||||
JsonNode.class);
|
||||
logger.debug("User info response: {}", userRes.getBody());
|
||||
} catch (HttpClientErrorException e) {
|
||||
logger.debug("User info request failed", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
JsonNode data = userRes.getBody() == null ? null : userRes.getBody().path("data");
|
||||
String username = data != null ? data.path("username").asText(null) : null;
|
||||
String avatar = data != null ? data.path("profile_image_url").asText(null) : null;
|
||||
if (username == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// Twitter v2 默认拿不到 email;如果你申请到 email.scope,可改用 /2/users/:id?user.fields=email
|
||||
String email = username + "@twitter.com";
|
||||
logger.debug("Processing user {} with email {}", username, email);
|
||||
return Optional.of(processUser(email, username, avatar, mode, viaInvite));
|
||||
// 1. 交换 token
|
||||
String tokenUrl = "https://api.twitter.com/2/oauth2/token";
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
headers.setAccept(List.of(MediaType.APPLICATION_JSON));
|
||||
if (!clientId.isEmpty() && !clientSecret.isEmpty()) {
|
||||
String credentials = clientId + ":" + clientSecret;
|
||||
String authHeader =
|
||||
"Basic " + Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
|
||||
headers.set(HttpHeaders.AUTHORIZATION, authHeader);
|
||||
}
|
||||
|
||||
private AuthResult processUser(String email, String username, String avatar, com.openisle.model.RegisterMode mode, boolean viaInvite) {
|
||||
Optional<User> existing = userRepository.findByEmail(email);
|
||||
if (existing.isPresent()) {
|
||||
User user = existing.get();
|
||||
if (!user.isVerified()) {
|
||||
user.setVerified(true);
|
||||
user.setVerificationCode(null);
|
||||
userRepository.save(user);
|
||||
}
|
||||
logger.debug("Existing user {} authenticated", user.getUsername());
|
||||
return new AuthResult(user, false);
|
||||
}
|
||||
String baseUsername = username != null ? username : email.split("@")[0];
|
||||
String finalUsername = baseUsername;
|
||||
int suffix = 1;
|
||||
while (userRepository.findByUsername(finalUsername).isPresent()) {
|
||||
finalUsername = baseUsername + suffix++;
|
||||
}
|
||||
User user = new User();
|
||||
user.setUsername(finalUsername);
|
||||
user.setEmail(email);
|
||||
user.setPassword("");
|
||||
user.setRole(Role.USER);
|
||||
// Twitter PKCE 要求的五个参数
|
||||
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
|
||||
body.add("client_id", clientId);
|
||||
body.add("grant_type", "authorization_code");
|
||||
body.add("code", code);
|
||||
body.add("code_verifier", codeVerifier);
|
||||
body.add("redirect_uri", redirectUri); // 一律必填
|
||||
// 如果你的 app 属于机密客户端,必须带 client_secret
|
||||
body.add("client_secret", clientSecret);
|
||||
|
||||
ResponseEntity<JsonNode> tokenRes;
|
||||
try {
|
||||
logger.debug("Requesting token from {}", tokenUrl);
|
||||
tokenRes = restTemplate.postForEntity(
|
||||
tokenUrl,
|
||||
new HttpEntity<>(body, headers),
|
||||
JsonNode.class
|
||||
);
|
||||
logger.debug("Token response: {}", tokenRes.getBody());
|
||||
} catch (HttpClientErrorException e) {
|
||||
logger.warn(
|
||||
"Token request failed with status {} and body {}",
|
||||
e.getStatusCode(),
|
||||
e.getResponseBodyAsString()
|
||||
);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
JsonNode tokenJson = tokenRes.getBody();
|
||||
if (tokenJson == null || !tokenJson.hasNonNull("access_token")) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String accessToken = tokenJson.get("access_token").asText();
|
||||
|
||||
// 2. 拉取用户信息
|
||||
HttpHeaders authHeaders = new HttpHeaders();
|
||||
authHeaders.setBearerAuth(accessToken);
|
||||
ResponseEntity<JsonNode> userRes;
|
||||
try {
|
||||
logger.debug("Fetching user info with access token");
|
||||
userRes = restTemplate.exchange(
|
||||
"https://api.twitter.com/2/users/me?user.fields=profile_image_url",
|
||||
HttpMethod.GET,
|
||||
new HttpEntity<>(authHeaders),
|
||||
JsonNode.class
|
||||
);
|
||||
logger.debug("User info response: {}", userRes.getBody());
|
||||
} catch (HttpClientErrorException e) {
|
||||
logger.debug("User info request failed", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
JsonNode data = userRes.getBody() == null ? null : userRes.getBody().path("data");
|
||||
String username = data != null ? data.path("username").asText(null) : null;
|
||||
String avatar = data != null ? data.path("profile_image_url").asText(null) : null;
|
||||
if (username == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// Twitter v2 默认拿不到 email;如果你申请到 email.scope,可改用 /2/users/:id?user.fields=email
|
||||
String email = username + "@twitter.com";
|
||||
logger.debug("Processing user {} with email {}", username, email);
|
||||
return Optional.of(processUser(email, username, avatar, mode, viaInvite));
|
||||
}
|
||||
|
||||
private AuthResult processUser(
|
||||
String email,
|
||||
String username,
|
||||
String avatar,
|
||||
com.openisle.model.RegisterMode mode,
|
||||
boolean viaInvite
|
||||
) {
|
||||
Optional<User> existing = userRepository.findByEmail(email);
|
||||
if (existing.isPresent()) {
|
||||
User user = existing.get();
|
||||
if (!user.isVerified()) {
|
||||
user.setVerified(true);
|
||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT || viaInvite);
|
||||
if (avatar != null) {
|
||||
user.setAvatar(avatar);
|
||||
} else {
|
||||
user.setAvatar("https://twitter.com/" + finalUsername + "/profile_image");
|
||||
}
|
||||
logger.debug("Creating new user {}", finalUsername);
|
||||
return new AuthResult(userRepository.save(user), true);
|
||||
user.setVerificationCode(null);
|
||||
userRepository.save(user);
|
||||
}
|
||||
logger.debug("Existing user {} authenticated", user.getUsername());
|
||||
return new AuthResult(user, false);
|
||||
}
|
||||
String baseUsername = username != null ? username : email.split("@")[0];
|
||||
String finalUsername = baseUsername;
|
||||
int suffix = 1;
|
||||
while (userRepository.findByUsername(finalUsername).isPresent()) {
|
||||
finalUsername = baseUsername + suffix++;
|
||||
}
|
||||
User user = new User();
|
||||
user.setUsername(finalUsername);
|
||||
user.setEmail(email);
|
||||
user.setPassword("");
|
||||
user.setRole(Role.USER);
|
||||
user.setVerified(true);
|
||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT || viaInvite);
|
||||
if (avatar != null) {
|
||||
user.setAvatar(avatar);
|
||||
} else {
|
||||
user.setAvatar("https://twitter.com/" + finalUsername + "/profile_image");
|
||||
}
|
||||
logger.debug("Creating new user {}", finalUsername);
|
||||
return new AuthResult(userRepository.save(user), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import com.openisle.config.CachingConfig;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.exception.FieldException;
|
||||
import com.openisle.model.Role;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import com.openisle.service.AvatarGenerator;
|
||||
import com.openisle.service.PasswordValidator;
|
||||
import com.openisle.service.UsernameValidator;
|
||||
import com.openisle.service.AvatarGenerator;
|
||||
import com.openisle.exception.FieldException;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import com.openisle.util.VerifyType;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
@@ -16,219 +20,230 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserService {
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordValidator passwordValidator;
|
||||
private final UsernameValidator usernameValidator;
|
||||
private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
||||
private final ImageUploader imageUploader;
|
||||
private final AvatarGenerator avatarGenerator;
|
||||
|
||||
private final RedisTemplate redisTemplate;
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordValidator passwordValidator;
|
||||
private final UsernameValidator usernameValidator;
|
||||
private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
||||
private final ImageUploader imageUploader;
|
||||
private final AvatarGenerator avatarGenerator;
|
||||
|
||||
private final EmailSender emailService;
|
||||
private final RedisTemplate redisTemplate;
|
||||
|
||||
public User register(String username, String email, String password, String reason, com.openisle.model.RegisterMode mode) {
|
||||
usernameValidator.validate(username);
|
||||
passwordValidator.validate(password);
|
||||
// ── 先按用户名查 ──────────────────────────────────────────
|
||||
Optional<User> byUsername = userRepository.findByUsername(username);
|
||||
if (byUsername.isPresent()) {
|
||||
User u = byUsername.get();
|
||||
if (u.isVerified()) { // 已验证 → 直接拒绝
|
||||
throw new FieldException("username", "User name already exists");
|
||||
}
|
||||
// 未验证 → 允许“重注册”:覆盖必要字段并重新发验证码
|
||||
u.setEmail(email); // 若不允许改邮箱可去掉
|
||||
u.setPassword(passwordEncoder.encode(password));
|
||||
// u.setVerificationCode(genCode());
|
||||
u.setRegisterReason(reason);
|
||||
u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
||||
return userRepository.save(u);
|
||||
}
|
||||
private final EmailSender emailService;
|
||||
|
||||
// ── 再按邮箱查 ───────────────────────────────────────────
|
||||
Optional<User> byEmail = userRepository.findByEmail(email);
|
||||
if (byEmail.isPresent()) {
|
||||
User u = byEmail.get();
|
||||
if (u.isVerified()) { // 已验证 → 直接拒绝
|
||||
throw new FieldException("email", "User email already exists");
|
||||
}
|
||||
// 未验证 → 允许“重注册”
|
||||
u.setUsername(username); // 若不允许改用户名可去掉
|
||||
u.setPassword(passwordEncoder.encode(password));
|
||||
// u.setVerificationCode(genCode());
|
||||
u.setRegisterReason(reason);
|
||||
u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
||||
return userRepository.save(u);
|
||||
}
|
||||
|
||||
// ── 完全新用户 ───────────────────────────────────────────
|
||||
User user = new User();
|
||||
user.setUsername(username);
|
||||
user.setEmail(email);
|
||||
user.setPassword(passwordEncoder.encode(password));
|
||||
user.setRole(Role.USER);
|
||||
user.setVerified(false);
|
||||
// user.setVerificationCode(genCode());
|
||||
user.setAvatar(avatarGenerator.generate(username));
|
||||
user.setRegisterReason(reason);
|
||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
||||
return userRepository.save(user);
|
||||
public User register(
|
||||
String username,
|
||||
String email,
|
||||
String password,
|
||||
String reason,
|
||||
com.openisle.model.RegisterMode mode
|
||||
) {
|
||||
usernameValidator.validate(username);
|
||||
passwordValidator.validate(password);
|
||||
// ── 先按用户名查 ──────────────────────────────────────────
|
||||
Optional<User> byUsername = userRepository.findByUsername(username);
|
||||
if (byUsername.isPresent()) {
|
||||
User u = byUsername.get();
|
||||
if (u.isVerified()) {
|
||||
// 已验证 → 直接拒绝
|
||||
throw new FieldException("username", "User name already exists");
|
||||
}
|
||||
// 未验证 → 允许“重注册”:覆盖必要字段并重新发验证码
|
||||
u.setEmail(email); // 若不允许改邮箱可去掉
|
||||
u.setPassword(passwordEncoder.encode(password));
|
||||
// u.setVerificationCode(genCode());
|
||||
u.setRegisterReason(reason);
|
||||
u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
||||
return userRepository.save(u);
|
||||
}
|
||||
|
||||
public User registerWithInvite(String username, String email, String password) {
|
||||
User user = register(username, email, password, "", com.openisle.model.RegisterMode.DIRECT);
|
||||
user.setVerified(true);
|
||||
// user.setVerificationCode(genCode());
|
||||
return userRepository.save(user);
|
||||
// ── 再按邮箱查 ───────────────────────────────────────────
|
||||
Optional<User> byEmail = userRepository.findByEmail(email);
|
||||
if (byEmail.isPresent()) {
|
||||
User u = byEmail.get();
|
||||
if (u.isVerified()) {
|
||||
// 已验证 → 直接拒绝
|
||||
throw new FieldException("email", "User email already exists");
|
||||
}
|
||||
// 未验证 → 允许“重注册”
|
||||
u.setUsername(username); // 若不允许改用户名可去掉
|
||||
u.setPassword(passwordEncoder.encode(password));
|
||||
// u.setVerificationCode(genCode());
|
||||
u.setRegisterReason(reason);
|
||||
u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
||||
return userRepository.save(u);
|
||||
}
|
||||
|
||||
private String genCode() {
|
||||
return String.format("%06d", new Random().nextInt(1000000));
|
||||
// ── 完全新用户 ───────────────────────────────────────────
|
||||
User user = new User();
|
||||
user.setUsername(username);
|
||||
user.setEmail(email);
|
||||
user.setPassword(passwordEncoder.encode(password));
|
||||
user.setRole(Role.USER);
|
||||
user.setVerified(false);
|
||||
// user.setVerificationCode(genCode());
|
||||
user.setAvatar(avatarGenerator.generate(username));
|
||||
user.setRegisterReason(reason);
|
||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
public User registerWithInvite(String username, String email, String password) {
|
||||
User user = register(username, email, password, "", com.openisle.model.RegisterMode.DIRECT);
|
||||
user.setVerified(true);
|
||||
// user.setVerificationCode(genCode());
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
private String genCode() {
|
||||
return String.format("%06d", new Random().nextInt(1000000));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将验证码存入缓存,并发送邮件
|
||||
* @param user
|
||||
*/
|
||||
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分钟)";
|
||||
}
|
||||
|
||||
/**
|
||||
* 将验证码存入缓存,并发送邮件
|
||||
* @param user
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证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);
|
||||
userRepository.save(user);
|
||||
}
|
||||
// 走到这里说明验证成功删除验证码
|
||||
redisTemplate.delete(key);
|
||||
return true;
|
||||
|
||||
// 注册模式需要设置已经确认
|
||||
if (VerifyType.REGISTER.equals(verifyType)) {
|
||||
user.setVerified(true);
|
||||
userRepository.save(user);
|
||||
}
|
||||
// 走到这里说明验证成功删除验证码
|
||||
redisTemplate.delete(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
public Optional<User> authenticate(String username, String password) {
|
||||
return userRepository.findByUsername(username)
|
||||
.filter(User::isVerified)
|
||||
.filter(User::isApproved)
|
||||
.filter(user -> passwordEncoder.matches(password, user.getPassword()));
|
||||
}
|
||||
public Optional<User> authenticate(String username, String password) {
|
||||
return userRepository
|
||||
.findByUsername(username)
|
||||
.filter(User::isVerified)
|
||||
.filter(User::isApproved)
|
||||
.filter(user -> passwordEncoder.matches(password, user.getPassword()));
|
||||
}
|
||||
|
||||
public boolean matchesPassword(User user, String rawPassword) {
|
||||
return passwordEncoder.matches(rawPassword, user.getPassword());
|
||||
}
|
||||
public boolean matchesPassword(User user, String rawPassword) {
|
||||
return passwordEncoder.matches(rawPassword, user.getPassword());
|
||||
}
|
||||
|
||||
public Optional<User> findByUsername(String username) {
|
||||
return userRepository.findByUsername(username);
|
||||
}
|
||||
public Optional<User> findByUsername(String username) {
|
||||
return userRepository.findByUsername(username);
|
||||
}
|
||||
|
||||
public Optional<User> findByEmail(String email) {
|
||||
return userRepository.findByEmail(email);
|
||||
}
|
||||
public Optional<User> findByEmail(String email) {
|
||||
return userRepository.findByEmail(email);
|
||||
}
|
||||
|
||||
public Optional<User> findById(Long id) {
|
||||
return userRepository.findById(id);
|
||||
}
|
||||
public Optional<User> findById(Long id) {
|
||||
return userRepository.findById(id);
|
||||
}
|
||||
|
||||
public Optional<User> findByIdentifier(String identifier) {
|
||||
if (identifier.matches("\\d+")) {
|
||||
return userRepository.findById(Long.parseLong(identifier));
|
||||
}
|
||||
return userRepository.findByUsername(identifier);
|
||||
public Optional<User> findByIdentifier(String identifier) {
|
||||
if (identifier.matches("\\d+")) {
|
||||
return userRepository.findById(Long.parseLong(identifier));
|
||||
}
|
||||
return userRepository.findByUsername(identifier);
|
||||
}
|
||||
|
||||
public User updateAvatar(String username, String avatarUrl) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
String old = user.getAvatar();
|
||||
user.setAvatar(avatarUrl);
|
||||
User saved = userRepository.save(user);
|
||||
if (old != null && !old.equals(avatarUrl)) {
|
||||
imageUploader.removeReferences(java.util.Set.of(old));
|
||||
}
|
||||
if (avatarUrl != null) {
|
||||
imageUploader.addReferences(java.util.Set.of(avatarUrl));
|
||||
}
|
||||
return saved;
|
||||
public User updateAvatar(String username, String avatarUrl) {
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
String old = user.getAvatar();
|
||||
user.setAvatar(avatarUrl);
|
||||
User saved = userRepository.save(user);
|
||||
if (old != null && !old.equals(avatarUrl)) {
|
||||
imageUploader.removeReferences(java.util.Set.of(old));
|
||||
}
|
||||
if (avatarUrl != null) {
|
||||
imageUploader.addReferences(java.util.Set.of(avatarUrl));
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
|
||||
public User updateReason(String username, String reason) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
user.setRegisterReason(reason);
|
||||
return userRepository.save(user);
|
||||
}
|
||||
public User updateReason(String username, String reason) {
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
user.setRegisterReason(reason);
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
public User updateProfile(String currentUsername, String newUsername, String introduction) {
|
||||
User user = userRepository.findByUsername(currentUsername)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
if (newUsername != null && !newUsername.equals(currentUsername)) {
|
||||
usernameValidator.validate(newUsername);
|
||||
userRepository.findByUsername(newUsername).ifPresent(u -> {
|
||||
throw new FieldException("username", "User name already exists");
|
||||
});
|
||||
user.setUsername(newUsername);
|
||||
}
|
||||
if (introduction != null) {
|
||||
user.setIntroduction(introduction);
|
||||
}
|
||||
return userRepository.save(user);
|
||||
public User updateProfile(String currentUsername, String newUsername, String introduction) {
|
||||
User user = userRepository
|
||||
.findByUsername(currentUsername)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
if (newUsername != null && !newUsername.equals(currentUsername)) {
|
||||
usernameValidator.validate(newUsername);
|
||||
userRepository
|
||||
.findByUsername(newUsername)
|
||||
.ifPresent(u -> {
|
||||
throw new FieldException("username", "User name already exists");
|
||||
});
|
||||
user.setUsername(newUsername);
|
||||
}
|
||||
if (introduction != null) {
|
||||
user.setIntroduction(introduction);
|
||||
}
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
public User updatePassword(String username, String newPassword) {
|
||||
passwordValidator.validate(newPassword);
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
user.setPassword(passwordEncoder.encode(newPassword));
|
||||
return userRepository.save(user);
|
||||
}
|
||||
public User updatePassword(String username, String newPassword) {
|
||||
passwordValidator.validate(newPassword);
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
user.setPassword(passwordEncoder.encode(newPassword));
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all administrator accounts.
|
||||
*/
|
||||
public java.util.List<User> getAdmins() {
|
||||
return userRepository.findByRole(Role.ADMIN);
|
||||
}
|
||||
/**
|
||||
* Get all administrator accounts.
|
||||
*/
|
||||
public java.util.List<User> getAdmins() {
|
||||
return userRepository.findByRole(Role.ADMIN);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,92 +5,97 @@ 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.cache.annotation.CacheConfig;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.cache.annotation.CacheConfig;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserVisitService {
|
||||
private final UserVisitRepository userVisitRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
private final RedisTemplate redisTemplate;
|
||||
private final UserVisitRepository userVisitRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public boolean recordVisit(String username) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
LocalDate today = LocalDate.now();
|
||||
return userVisitRepository.findByUserAndVisitDate(user, today).map(v -> false).orElseGet(() -> {
|
||||
UserVisit visit = new UserVisit();
|
||||
visit.setUser(user);
|
||||
visit.setVisitDate(today);
|
||||
userVisitRepository.save(visit);
|
||||
return true;
|
||||
});
|
||||
private final RedisTemplate redisTemplate;
|
||||
|
||||
public boolean recordVisit(String username) {
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
LocalDate today = LocalDate.now();
|
||||
return userVisitRepository
|
||||
.findByUserAndVisitDate(user, today)
|
||||
.map(v -> false)
|
||||
.orElseGet(() -> {
|
||||
UserVisit visit = new UserVisit();
|
||||
visit.setUser(user);
|
||||
visit.setVisitDate(today);
|
||||
userVisitRepository.save(visit);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计访问次数,改为从缓存获取/数据库获取
|
||||
* @param username
|
||||
* @return
|
||||
*/
|
||||
public long countVisits(String username) {
|
||||
User user = userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
|
||||
// 如果缓存存在就返回
|
||||
String key1 = CachingConfig.VISIT_CACHE_NAME + ":" + LocalDate.now() + ":count:" + username;
|
||||
Integer cached = (Integer) redisTemplate.opsForValue().get(key1);
|
||||
if (cached != null) {
|
||||
return cached.longValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计访问次数,改为从缓存获取/数据库获取
|
||||
* @param username
|
||||
* @return
|
||||
*/
|
||||
public long countVisits(String username) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
// Redis Set 检查今天是否访问
|
||||
String todayKey = CachingConfig.VISIT_CACHE_NAME + ":" + LocalDate.now();
|
||||
boolean todayVisited = redisTemplate.opsForSet().isMember(todayKey, username);
|
||||
|
||||
// 如果缓存存在就返回
|
||||
String key1 = CachingConfig.VISIT_CACHE_NAME + ":" +LocalDate.now() + ":count:" + username;
|
||||
Integer cached = (Integer) redisTemplate.opsForValue().get(key1);
|
||||
if (cached != null){
|
||||
return cached.longValue();
|
||||
}
|
||||
Long visitCount = userVisitRepository.countByUser(user);
|
||||
if (todayVisited) visitCount += 1;
|
||||
|
||||
// Redis Set 检查今天是否访问
|
||||
String todayKey = CachingConfig.VISIT_CACHE_NAME + ":" + LocalDate.now();
|
||||
boolean todayVisited = redisTemplate.opsForSet().isMember(todayKey, username);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime endOfDay = now.toLocalDate().atTime(23, 59, 59);
|
||||
long secondsUntilEndOfDay = Duration.between(now, endOfDay).getSeconds();
|
||||
|
||||
Long visitCount = userVisitRepository.countByUser(user);
|
||||
if (todayVisited) visitCount += 1;
|
||||
// 写入缓存,设置 TTL,当天剩余时间
|
||||
redisTemplate.opsForValue().set(key1, visitCount, Duration.ofSeconds(secondsUntilEndOfDay));
|
||||
return visitCount;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime endOfDay = now.toLocalDate().atTime(23, 59, 59);
|
||||
long secondsUntilEndOfDay = Duration.between(now, endOfDay).getSeconds();
|
||||
public long countDau(LocalDate date) {
|
||||
LocalDate d = date != null ? date : LocalDate.now();
|
||||
return userVisitRepository.countByVisitDate(d);
|
||||
}
|
||||
|
||||
// 写入缓存,设置 TTL,当天剩余时间
|
||||
redisTemplate.opsForValue().set(key1, visitCount, Duration.ofSeconds(secondsUntilEndOfDay));
|
||||
return visitCount;
|
||||
public Map<LocalDate, Long> countDauRange(LocalDate start, LocalDate end) {
|
||||
Map<LocalDate, Long> result = new LinkedHashMap<>();
|
||||
if (start == null || end == null || start.isAfter(end)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
public long countDau(LocalDate date) {
|
||||
LocalDate d = date != null ? date : LocalDate.now();
|
||||
return userVisitRepository.countByVisitDate(d);
|
||||
var list = userVisitRepository.countRange(start, end);
|
||||
for (var obj : list) {
|
||||
LocalDate d = (LocalDate) obj[0];
|
||||
Long c = (Long) obj[1];
|
||||
result.put(d, c);
|
||||
}
|
||||
|
||||
public Map<LocalDate, Long> countDauRange(LocalDate start, LocalDate end) {
|
||||
Map<LocalDate, Long> result = new LinkedHashMap<>();
|
||||
if (start == null || end == null || start.isAfter(end)) {
|
||||
return result;
|
||||
}
|
||||
var list = userVisitRepository.countRange(start, end);
|
||||
for (var obj : list) {
|
||||
LocalDate d = (LocalDate) obj[0];
|
||||
Long c = (Long) obj[1];
|
||||
result.put(d, c);
|
||||
}
|
||||
// fill zero counts for missing dates
|
||||
for (LocalDate d = start; !d.isAfter(end); d = d.plusDays(1)) {
|
||||
result.putIfAbsent(d, 0L);
|
||||
}
|
||||
return result;
|
||||
// fill zero counts for missing dates
|
||||
for (LocalDate d = start; !d.isAfter(end); d = d.plusDays(1)) {
|
||||
result.putIfAbsent(d, 0L);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,20 +9,19 @@ import org.springframework.stereotype.Service;
|
||||
*/
|
||||
@Service
|
||||
public class UsernameValidator {
|
||||
/**
|
||||
* Validate the username string.
|
||||
*
|
||||
* @param username the username to validate
|
||||
*/
|
||||
public void validate(String username) {
|
||||
if (username == null || username.isEmpty()) {
|
||||
throw new FieldException("username", "Username cannot be empty");
|
||||
}
|
||||
|
||||
if (NumberUtils.isDigits(username)) {
|
||||
throw new FieldException("username", "Username cannot be pure number");
|
||||
}
|
||||
/**
|
||||
* Validate the username string.
|
||||
*
|
||||
* @param username the username to validate
|
||||
*/
|
||||
public void validate(String username) {
|
||||
if (username == null || username.isEmpty()) {
|
||||
throw new FieldException("username", "Username cannot be empty");
|
||||
}
|
||||
|
||||
if (NumberUtils.isDigits(username)) {
|
||||
throw new FieldException("username", "Username cannot be pure number");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user