dev add reason entity
Test Workflow / test (push) Successful in 2s

This commit is contained in:
vladp
2026-03-27 14:20:09 +07:00
parent 57dcc12cf2
commit 4e0579c952
9 changed files with 494 additions and 3 deletions
@@ -68,4 +68,10 @@ public class FileEntityRequest {
@JsonProperty("approve")
private Boolean approve;
@JsonProperty("reason_text")
private String reasonText;
@JsonProperty("reason_text")
private String searchQuery;
}
@@ -0,0 +1,16 @@
package ru.soune.nocopy.dto.file;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class ModerationReasonRequest {
private String action;
private String token;
private Long reasonId;
private String reasonText;
private Integer page;
private Integer pageSize;
private String searchQuery;
}
@@ -0,0 +1,15 @@
package ru.soune.nocopy.dto.file;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Builder
public class ModerationReasonResponse {
private Long id;
private String reasonText;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private Boolean isUsed;
private Long usageCount;
}
@@ -0,0 +1,17 @@
package ru.soune.nocopy.exception;
import lombok.Getter;
@Getter
public class ModerationReasonInUseException extends RuntimeException {
private final Long reasonId;
private final Long usageCount;
public ModerationReasonInUseException(Long reasonId, Long usageCount) {
super(String.format("Cannot delete moderation reason %d because it is used by %d files",
reasonId, usageCount));
this.reasonId = reasonId;
this.usageCount = usageCount;
}
}
@@ -0,0 +1,7 @@
package ru.soune.nocopy.exception;
public class ModerationReasonNotFoundException extends RuntimeException {
public ModerationReasonNotFoundException(Long id) {
super("Moderation reason not found with id: " + id);
}
}
@@ -12,12 +12,11 @@ import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.ProtectionStatus;
import ru.soune.nocopy.entity.file.moderation.ModerationLog;
import ru.soune.nocopy.exception.FileEntityNotFoundException;
import ru.soune.nocopy.exception.InvalidAppealException;
import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.exception.*;
import ru.soune.nocopy.repository.FileAppealRepository;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.ModerationLogRepository;
import ru.soune.nocopy.service.file.moderation.ModerationReasonService;
import ru.soune.nocopy.service.file.moderation.ModerationService;
import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.file.FileEntityService;
@@ -48,6 +47,8 @@ public class FileEntityHandler implements RequestHandler {
private final ModerationLogRepository moderationLogRepository;
private final ModerationReasonService moderationReasonService;
@Override
public BaseResponse handle(BaseRequest request) {
try {
@@ -85,6 +86,14 @@ public class FileEntityHandler implements RequestHandler {
return handleGetUserAppeals(request, fileRequest);
case "files_for_moderation":
return handleGetFilesForModeration(request, fileRequest);
case "get_moderation_reason":
return handleGetModerationReason(request, fileRequest);
case "create_moderation_reason":
return handleCreateModerationReason(request, fileRequest);
case "update_moderation_reason":
return handleUpdateModerationReason(request, fileRequest);
case "delete_moderation_reason":
return handleDeleteModerationReason(request, fileRequest);
default:
ActionResponse response = ActionResponse.builder()
.action(action)
@@ -937,6 +946,214 @@ public class FileEntityHandler implements RequestHandler {
.collect(Collectors.toList());
}
/**
* Получение списка причин модерации
*/
private BaseResponse handleGetModerationReasons(BaseRequest request, FileEntityRequest fileRequest) {
try {
// TODO
// authService.useAdminAuthToken(fileRequest.getToken());
int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1;
int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20;
String searchQuery = fileRequest.getSearchQuery();
Page<ModerationReasonResponse> reasons = moderationReasonService.getAllReasons(
page - 1, pageSize, searchQuery);
Map<String, Object> response = new HashMap<>();
response.put("reasons", reasons.getContent());
response.put("totalCount", reasons.getTotalElements());
response.put("totalPages", reasons.getTotalPages());
response.put("currentPage", page);
response.put("pageSize", pageSize);
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
response);
} catch (SecurityException e) {
return new BaseResponse(request.getMsgId(),
MessageCode.ACCESS_DENIED.getCode(),
"Admin access required",
null);
} catch (Exception e) {
log.error("Error getting moderation reasons", e);
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
"Failed to get reasons: " + e.getMessage(),
null);
}
}
/**
* Получение конкретной причины модерации
*/
private BaseResponse handleGetModerationReason(BaseRequest request, FileEntityRequest fileRequest) {
try {
// TODO
// authService.useAdminAuthToken(fileRequest.getToken());
Long reasonId = fileRequest.getModerationReasonId();
if (reasonId == null) {
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
"reasonId is required",
null);
}
ModerationReasonResponse reason = moderationReasonService.getReasonById(reasonId);
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
reason);
} catch (ModerationReasonNotFoundException e) {
return new BaseResponse(request.getMsgId(),
MessageCode.NOT_FOUND.getCode(),
e.getMessage(),
null);
} catch (Exception e) {
log.error("Error getting moderation reason", e);
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
"Failed to get reason: " + e.getMessage(),
null);
}
}
/**
* Создание новой причины модерации
*/
private BaseResponse handleCreateModerationReason(BaseRequest request, FileEntityRequest fileRequest) {
try {
// TODO
// authService.useAdminAuthToken(fileRequest.getToken());
String reasonText = fileRequest.getReasonText();
if (reasonText == null || reasonText.trim().isEmpty()) {
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
"reasonText is required",
null);
}
ModerationReasonResponse reason = moderationReasonService.createReason(reasonText);
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
"Moderation reason created successfully",
reason);
} catch (IllegalArgumentException e) {
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
e.getMessage(),
null);
} catch (Exception e) {
log.error("Error creating moderation reason", e);
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
"Failed to create reason: " + e.getMessage(),
null);
}
}
/**
* Обновление причины модерации
*/
private BaseResponse handleUpdateModerationReason(BaseRequest request, FileEntityRequest fileRequest) {
try {
// TODO
// authService.useAdminAuthToken(fileRequest.getToken());
Long reasonId = fileRequest.getModerationReasonId();
String reasonText = fileRequest.getReasonText();
if (reasonId == null) {
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
"reasonId is required",
null);
}
if (reasonText == null || reasonText.trim().isEmpty()) {
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
"reasonText is required",
null);
}
ModerationReasonResponse reason = moderationReasonService.updateReason(reasonId, reasonText);
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
"Moderation reason updated successfully",
reason);
} catch (ModerationReasonNotFoundException e) {
return new BaseResponse(request.getMsgId(),
MessageCode.NOT_FOUND.getCode(),
e.getMessage(),
null);
} catch (IllegalArgumentException e) {
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
e.getMessage(),
null);
} catch (Exception e) {
log.error("Error updating moderation reason", e);
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
"Failed to update reason: " + e.getMessage(),
null);
}
}
/**
* Удаление причины модерации
*/
private BaseResponse handleDeleteModerationReason(BaseRequest request, FileEntityRequest fileRequest) {
try {
// TODO
// authService.useAdminAuthToken(fileRequest.getToken());
Long reasonId = fileRequest.getModerationReasonId();
if (reasonId == null) {
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
"reasonId is required",
null);
}
moderationReasonService.deleteReason(reasonId);
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
"Moderation reason deleted successfully",
Map.of("reasonId", reasonId));
} catch (ModerationReasonNotFoundException e) {
return new BaseResponse(request.getMsgId(),
MessageCode.NOT_FOUND.getCode(),
e.getMessage(),
null);
} catch (ModerationReasonInUseException e) {
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
e.getMessage(),
Map.of("usageCount", e.getUsageCount()));
} catch (Exception e) {
log.error("Error deleting moderation reason", e);
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
"Failed to delete reason: " + e.getMessage(),
null);
}
}
private String formatFileSize(long size) {
if (size < 1024) return size + " B";
int exp = (int) (Math.log(size) / Math.log(1024));
@@ -72,4 +72,8 @@ public interface FileEntityRepository extends JpaRepository<FileEntity, String>
Page<FileEntity> findByStatusIn(List<FileStatus> statuses, Pageable pageable);
List<FileEntity> findByUserIdAndStatusIn(Long userId, List<FileStatus> statuses);
@Query("SELECT COUNT(f) FROM FileEntity f WHERE f.moderationReasonId = :reasonId AND f.status IN :statuses")
long countByModerationReasonIdAndStatusIn(@Param("reasonId") Long reasonId,
@Param("statuses") List<FileStatus> statuses);
}
@@ -0,0 +1,22 @@
package ru.soune.nocopy.repository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import ru.soune.nocopy.entity.file.moderation.ModerationReason;
import java.util.Optional;
public interface ModerationReasonRepository extends JpaRepository<ModerationReason, Long> {
Optional<ModerationReason> findByReasonText(String reasonText);
Page<ModerationReason> findAll(Pageable pageable);
@Query("SELECT mr FROM ModerationReason mr WHERE mr.reasonText LIKE %:search%")
Page<ModerationReason> searchByText(@Param("search") String search, Pageable pageable);
@Query("SELECT COUNT(mr) FROM ModerationReason mr")
long countAllReasons();
}
@@ -0,0 +1,187 @@
package ru.soune.nocopy.service.file.moderation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.file.ModerationReasonResponse;
import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.moderation.ModerationReason;
import ru.soune.nocopy.exception.ModerationReasonInUseException;
import ru.soune.nocopy.exception.ModerationReasonNotFoundException;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.ModerationReasonRepository;
import java.util.Arrays;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class ModerationReasonService {
private final ModerationReasonRepository moderationReasonRepository;
private final FileEntityRepository fileEntityRepository;
private static final List<FileStatus> BLOCKED_STATUSES = Arrays.asList(
FileStatus.BLOCKED,
FileStatus.VIOLATION,
FileStatus.MODERATION
);
/**
* Создание новой причины модерации
*/
@Transactional
public ModerationReasonResponse createReason(String reasonText) {
if (reasonText == null || reasonText.trim().isEmpty()) {
throw new IllegalArgumentException("Reason text cannot be empty");
}
// Проверяем на дубликат
if (moderationReasonRepository.findByReasonText(reasonText.trim()).isPresent()) {
throw new IllegalArgumentException("Reason with this text already exists");
}
ModerationReason reason = ModerationReason.builder()
.reasonText(reasonText.trim())
.build();
ModerationReason saved = moderationReasonRepository.save(reason);
log.info("Created new moderation reason: id={}, text={}", saved.getId(), saved.getReasonText());
return convertToResponse(saved, false, 0L);
}
/**
* Обновление причины модерации
*/
@Transactional
public ModerationReasonResponse updateReason(Long reasonId, String newReasonText) {
ModerationReason reason = moderationReasonRepository.findById(reasonId)
.orElseThrow(() -> new ModerationReasonNotFoundException(reasonId));
if (newReasonText == null || newReasonText.trim().isEmpty()) {
throw new IllegalArgumentException("Reason text cannot be empty");
}
// Проверяем на дубликат (исключая текущую запись)
moderationReasonRepository.findByReasonText(newReasonText.trim())
.ifPresent(existingReason -> {
if (!existingReason.getId().equals(reasonId)) {
throw new IllegalArgumentException("Reason with this text already exists");
}
});
String oldText = reason.getReasonText();
reason.setReasonText(newReasonText.trim());
ModerationReason updated = moderationReasonRepository.save(reason);
log.info("Updated moderation reason: id={}, oldText={}, newText={}",
reasonId, oldText, newReasonText);
long usageCount = getReasonUsageCount(reasonId);
return convertToResponse(updated, usageCount > 0, usageCount);
}
/**
* Удаление причины модерации
*/
@Transactional
public void deleteReason(Long reasonId) {
ModerationReason reason = moderationReasonRepository.findById(reasonId)
.orElseThrow(() -> new ModerationReasonNotFoundException(reasonId));
// Проверяем, используется ли причина в активных блокировках
long usageCount = getReasonUsageCount(reasonId);
if (usageCount > 0) {
throw new ModerationReasonInUseException(reasonId, usageCount);
}
moderationReasonRepository.delete(reason);
log.info("Deleted moderation reason: id={}, text={}", reasonId, reason.getReasonText());
}
/**
* Получение причины по ID
*/
@Transactional(readOnly = true)
public ModerationReasonResponse getReasonById(Long reasonId) {
ModerationReason reason = moderationReasonRepository.findById(reasonId)
.orElseThrow(() -> new ModerationReasonNotFoundException(reasonId));
long usageCount = getReasonUsageCount(reasonId);
return convertToResponse(reason, usageCount > 0, usageCount);
}
/**
* Получение всех причин с пагинацией
*/
@Transactional(readOnly = true)
public Page<ModerationReasonResponse> getAllReasons(int page, int size, String searchQuery) {
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
Page<ModerationReason> reasonsPage;
if (searchQuery != null && !searchQuery.trim().isEmpty()) {
reasonsPage = moderationReasonRepository.searchByText(searchQuery.trim(), pageable);
} else {
reasonsPage = moderationReasonRepository.findAll(pageable);
}
return reasonsPage.map(reason -> {
long usageCount = getReasonUsageCount(reason.getId());
return convertToResponse(reason, usageCount > 0, usageCount);
});
}
/**
* Получение списка всех причин (без пагинации, для выпадающих списков)
*/
@Transactional(readOnly = true)
public List<ModerationReasonResponse> getAllReasonsList() {
List<ModerationReason> reasons = moderationReasonRepository.findAll(Sort.by("reasonText").ascending());
return reasons.stream()
.map(reason -> {
long usageCount = getReasonUsageCount(reason.getId());
return convertToResponse(reason, usageCount > 0, usageCount);
})
.collect(java.util.stream.Collectors.toList());
}
/**
* Подсчет количества файлов, заблокированных по данной причине
*/
private long getReasonUsageCount(Long reasonId) {
return fileEntityRepository.countByModerationReasonIdAndStatusIn(reasonId, BLOCKED_STATUSES);
}
/**
* Проверка, используется ли причина в активных блокировках
*/
public boolean isReasonInUse(Long reasonId) {
return getReasonUsageCount(reasonId) > 0;
}
private ModerationReasonResponse convertToResponse(ModerationReason reason, boolean isUsed, long usageCount) {
return ModerationReasonResponse.builder()
.id(reason.getId())
.reasonText(reason.getReasonText())
.createdAt(reason.getCreatedAt())
.updatedAt(reason.getUpdatedAt())
.isUsed(isUsed)
.usageCount(usageCount)
.build();
}
}