This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
package ru.soune.nocopy.dto.file;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class AppealInfo {
|
||||||
|
private String appealId;
|
||||||
|
private String status;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package ru.soune.nocopy.dto.file;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class AppealRequest {
|
||||||
|
private String fileId;
|
||||||
|
private String appealReason;
|
||||||
|
private String additionalInfo;
|
||||||
|
private String token;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package ru.soune.nocopy.dto.file;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class AppealResponse {
|
||||||
|
private String appealId;
|
||||||
|
private String fileId;
|
||||||
|
private String appealReason;
|
||||||
|
private String additionalInfo;
|
||||||
|
private String status;
|
||||||
|
private String adminComment;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime resolvedAt;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import ru.soune.nocopy.entity.file.FileStatus;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
@@ -44,6 +45,27 @@ public class FileEntityRequest {
|
|||||||
@JsonProperty("type")
|
@JsonProperty("type")
|
||||||
private String type;
|
private String type;
|
||||||
|
|
||||||
|
@JsonProperty("file_status")
|
||||||
|
private FileStatus fileStatus;
|
||||||
|
|
||||||
@JsonProperty("date_filter")
|
@JsonProperty("date_filter")
|
||||||
private String dateFilter;
|
private String dateFilter;
|
||||||
|
|
||||||
|
@JsonProperty("moderation_reason_id")
|
||||||
|
private Long moderationReasonId;
|
||||||
|
|
||||||
|
@JsonProperty("comment")
|
||||||
|
private String comment;
|
||||||
|
|
||||||
|
@JsonProperty("appeal_reason")
|
||||||
|
private String appealReason;
|
||||||
|
|
||||||
|
@JsonProperty("additional_info")
|
||||||
|
private String additionalInfo;
|
||||||
|
|
||||||
|
@JsonProperty("appeal_id")
|
||||||
|
private String appealId;
|
||||||
|
|
||||||
|
@JsonProperty("approve")
|
||||||
|
private Boolean approve;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package ru.soune.nocopy.dto.file;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import ru.soune.nocopy.entity.file.FileStatus;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class FileModerationInfo {
|
||||||
|
private String fileId;
|
||||||
|
private String fileName;
|
||||||
|
private String ownerName;
|
||||||
|
private String ownerEmail;
|
||||||
|
private FileStatus currentStatus;
|
||||||
|
private LocalDateTime uploadDate;
|
||||||
|
private String moderationReason;
|
||||||
|
private Boolean hasActiveAppeal;
|
||||||
|
private AppealInfo activeAppeal;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package ru.soune.nocopy.dto.file;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import ru.soune.nocopy.entity.file.FileStatus;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class ModerationActionRequest {
|
||||||
|
private String fileId;
|
||||||
|
private FileStatus newStatus;
|
||||||
|
private Long moderationReasonId;
|
||||||
|
private String comment;
|
||||||
|
private String token;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package ru.soune.nocopy.entity.file;
|
||||||
|
|
||||||
|
public enum AppealStatus {
|
||||||
|
PENDING,
|
||||||
|
IN_REVIEW,
|
||||||
|
APPROVED,
|
||||||
|
REJECTED,
|
||||||
|
EXPIRED
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package ru.soune.nocopy.entity.file;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
|
import org.springframework.data.annotation.LastModifiedDate;
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Entity
|
||||||
|
@Table(name = "file_appeals")
|
||||||
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
|
public class FileAppeal {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Column(name = "file_id", nullable = false)
|
||||||
|
private String fileId;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "file_id", insertable = false, updatable = false)
|
||||||
|
@ToString.Exclude
|
||||||
|
private FileEntity file;
|
||||||
|
|
||||||
|
@Column(name = "user_id", nullable = false)
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@Column(name = "appeal_reason", nullable = false, columnDefinition = "TEXT")
|
||||||
|
private String appealReason;
|
||||||
|
|
||||||
|
@Column(name = "additional_info", columnDefinition = "TEXT")
|
||||||
|
private String additionalInfo;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "status", nullable = false)
|
||||||
|
private AppealStatus status;
|
||||||
|
|
||||||
|
@Column(name = "admin_comment", columnDefinition = "TEXT")
|
||||||
|
private String adminComment;
|
||||||
|
|
||||||
|
@Column(name = "moderator_id")
|
||||||
|
private Long moderatorId;
|
||||||
|
|
||||||
|
@Column(name = "moderation_before_status")
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private FileStatus moderationBeforeStatus;
|
||||||
|
|
||||||
|
@Column(name = "moderation_reason_id")
|
||||||
|
private Long moderationReasonId;
|
||||||
|
|
||||||
|
@Column(name = "resolved_at")
|
||||||
|
private LocalDateTime resolvedAt;
|
||||||
|
|
||||||
|
@CreatedDate
|
||||||
|
@Column(name = "created_at", updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@LastModifiedDate
|
||||||
|
@Column(name = "updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
public void prePersist() {
|
||||||
|
if (this.status == null) {
|
||||||
|
this.status = AppealStatus.PENDING;
|
||||||
|
}
|
||||||
|
if (this.createdAt == null) {
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
public void preUpdate() {
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package ru.soune.nocopy.entity.file.moderation;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
import ru.soune.nocopy.entity.file.FileEntity;
|
||||||
|
import ru.soune.nocopy.entity.file.FileStatus;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Entity
|
||||||
|
@Table(name = "moderation_logs")
|
||||||
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
|
public class ModerationLog {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Column(name = "file_id", nullable = false)
|
||||||
|
private String fileId;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "file_id", insertable = false, updatable = false)
|
||||||
|
@ToString.Exclude
|
||||||
|
private FileEntity file;
|
||||||
|
|
||||||
|
@Column(name = "moderator_id", nullable = false)
|
||||||
|
private Long moderatorId;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "old_status", nullable = false)
|
||||||
|
private FileStatus oldStatus;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "new_status", nullable = false)
|
||||||
|
private FileStatus newStatus;
|
||||||
|
|
||||||
|
@Column(name = "reason", columnDefinition = "TEXT")
|
||||||
|
private String reason;
|
||||||
|
|
||||||
|
@Column(name = "moderation_reason_id")
|
||||||
|
private Long moderationReasonId;
|
||||||
|
|
||||||
|
@Column(name = "comment", columnDefinition = "TEXT")
|
||||||
|
private String comment;
|
||||||
|
|
||||||
|
@CreatedDate
|
||||||
|
@Column(name = "created_at", updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ru.soune.nocopy.exception;
|
||||||
|
|
||||||
|
public class InvalidAppealException extends RuntimeException {
|
||||||
|
public InvalidAppealException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,15 +3,22 @@ package ru.soune.nocopy.handler;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import ru.soune.nocopy.dto.*;
|
import ru.soune.nocopy.dto.*;
|
||||||
import ru.soune.nocopy.dto.file.*;
|
import ru.soune.nocopy.dto.file.*;
|
||||||
|
import ru.soune.nocopy.entity.file.FileAppeal;
|
||||||
import ru.soune.nocopy.entity.file.FileEntity;
|
import ru.soune.nocopy.entity.file.FileEntity;
|
||||||
import ru.soune.nocopy.entity.file.FileStatus;
|
import ru.soune.nocopy.entity.file.FileStatus;
|
||||||
import ru.soune.nocopy.entity.file.ProtectionStatus;
|
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.FileEntityNotFoundException;
|
||||||
|
import ru.soune.nocopy.exception.InvalidAppealException;
|
||||||
import ru.soune.nocopy.exception.NotFoundAuthToken;
|
import ru.soune.nocopy.exception.NotFoundAuthToken;
|
||||||
|
import ru.soune.nocopy.repository.FileAppealRepository;
|
||||||
import ru.soune.nocopy.repository.FileEntityRepository;
|
import ru.soune.nocopy.repository.FileEntityRepository;
|
||||||
|
import ru.soune.nocopy.repository.ModerationLogRepository;
|
||||||
|
import ru.soune.nocopy.service.file.moderation.ModerationService;
|
||||||
import ru.soune.nocopy.service.register.AuthService;
|
import ru.soune.nocopy.service.register.AuthService;
|
||||||
import ru.soune.nocopy.service.file.FileEntityService;
|
import ru.soune.nocopy.service.file.FileEntityService;
|
||||||
import ru.soune.nocopy.service.file.FileStatsService;
|
import ru.soune.nocopy.service.file.FileStatsService;
|
||||||
@@ -35,6 +42,12 @@ public class FileEntityHandler implements RequestHandler {
|
|||||||
|
|
||||||
private final FileEntityRepository fileEntityRepository;
|
private final FileEntityRepository fileEntityRepository;
|
||||||
|
|
||||||
|
private final ModerationService moderationService;
|
||||||
|
|
||||||
|
private final FileAppealRepository fileAppealRepository;
|
||||||
|
|
||||||
|
private final ModerationLogRepository moderationLogRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BaseResponse handle(BaseRequest request) {
|
public BaseResponse handle(BaseRequest request) {
|
||||||
try {
|
try {
|
||||||
@@ -60,6 +73,18 @@ public class FileEntityHandler implements RequestHandler {
|
|||||||
return handleProtectFile(request, fileRequest);
|
return handleProtectFile(request, fileRequest);
|
||||||
case "delete_file":
|
case "delete_file":
|
||||||
return handleDeleteFile(request, fileRequest);
|
return handleDeleteFile(request, fileRequest);
|
||||||
|
case "change_file_status":
|
||||||
|
return handleChangeStatus(request, fileRequest);
|
||||||
|
case "moderate_file":
|
||||||
|
return handleModerateFile(request, fileRequest);
|
||||||
|
case "submit_appeal":
|
||||||
|
return handleSubmitAppeal(request, fileRequest);
|
||||||
|
case "review_appeal":
|
||||||
|
return handleReviewAppeal(request, fileRequest);
|
||||||
|
case "user_appeals":
|
||||||
|
return handleGetUserAppeals(request, fileRequest);
|
||||||
|
case "files_for_moderation":
|
||||||
|
return handleGetFilesForModeration(request, fileRequest);
|
||||||
default:
|
default:
|
||||||
ActionResponse response = ActionResponse.builder()
|
ActionResponse response = ActionResponse.builder()
|
||||||
.action(action)
|
.action(action)
|
||||||
@@ -183,6 +208,23 @@ public class FileEntityHandler implements RequestHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BaseResponse handleChangeStatus(BaseRequest request, FileEntityRequest fileRequest) {
|
||||||
|
FileStatus newStatus = fileRequest.getFileStatus();
|
||||||
|
try {
|
||||||
|
fileEntityService.changeFileStatus(newStatus ,fileRequest.getFileId());
|
||||||
|
} catch (FileEntityNotFoundException e) {
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.FILE_NOT_FOUND.getCode(),
|
||||||
|
MessageCode.FILE_NOT_FOUND.getDescription(),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.SUCCESS.getCode(),
|
||||||
|
MessageCode.SUCCESS.getDescription(),
|
||||||
|
Map.of("new_status", newStatus));
|
||||||
|
}
|
||||||
|
|
||||||
private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) {
|
private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) {
|
||||||
try {
|
try {
|
||||||
Long userId = authService.useUserAuthToken(fileRequest.getToken());
|
Long userId = authService.useUserAuthToken(fileRequest.getToken());
|
||||||
@@ -568,6 +610,333 @@ public class FileEntityHandler implements RequestHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BaseResponse handleModerateFile(BaseRequest request, FileEntityRequest fileRequest) {
|
||||||
|
try {
|
||||||
|
//TODO добавить проверку на админа
|
||||||
|
// Long moderatorId = authService.useAdminAuthToken(fileRequest.getToken());
|
||||||
|
|
||||||
|
ModerationActionRequest moderationRequest = ModerationActionRequest.builder()
|
||||||
|
.fileId(fileRequest.getFileId())
|
||||||
|
.newStatus(fileRequest.getFileStatus())
|
||||||
|
.moderationReasonId(fileRequest.getModerationReasonId())
|
||||||
|
.comment(fileRequest.getComment())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
//TODO set moderator ID
|
||||||
|
moderationService.moderateFile(moderationRequest, 0L);
|
||||||
|
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.SUCCESS.getCode(),
|
||||||
|
"File moderated successfully",
|
||||||
|
null);
|
||||||
|
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.ACCESS_DENIED.getCode(),
|
||||||
|
e.getMessage(),
|
||||||
|
null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error moderating file", e);
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.INVALID_FIELD.getCode(),
|
||||||
|
"Failed to moderate file: " + e.getMessage(),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BaseResponse handleSubmitAppeal(BaseRequest request, FileEntityRequest fileRequest) {
|
||||||
|
try {
|
||||||
|
Long userId = authService.useUserAuthToken(fileRequest.getToken());
|
||||||
|
|
||||||
|
AppealRequest appealRequest = AppealRequest.builder()
|
||||||
|
.fileId(fileRequest.getFileId())
|
||||||
|
.appealReason(fileRequest.getAppealReason())
|
||||||
|
.additionalInfo(fileRequest.getAdditionalInfo())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AppealResponse response = moderationService.submitAppeal(appealRequest, userId);
|
||||||
|
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.SUCCESS.getCode(),
|
||||||
|
"Appeal submitted successfully",
|
||||||
|
response);
|
||||||
|
|
||||||
|
} catch (InvalidAppealException e) {
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.INVALID_FIELD.getCode(),
|
||||||
|
e.getMessage(),
|
||||||
|
null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error submitting appeal", e);
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.INVALID_FIELD.getCode(),
|
||||||
|
"Failed to submit appeal: " + e.getMessage(),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BaseResponse handleReviewAppeal(BaseRequest request, FileEntityRequest fileRequest) {
|
||||||
|
try {
|
||||||
|
// TODO
|
||||||
|
// Long moderatorId = authService.useAdminAuthToken(fileRequest.getToken());
|
||||||
|
|
||||||
|
String appealId = fileRequest.getAppealId();
|
||||||
|
Boolean approve = fileRequest.getApprove();
|
||||||
|
String comment = fileRequest.getComment();
|
||||||
|
|
||||||
|
if (appealId == null || approve == null) {
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.INVALID_FIELD.getCode(),
|
||||||
|
"appealId and approve are required",
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
//TODO
|
||||||
|
moderationService.reviewAppeal(appealId, approve, comment, 0L);
|
||||||
|
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.SUCCESS.getCode(),
|
||||||
|
"Appeal reviewed successfully",
|
||||||
|
Map.of(
|
||||||
|
"appealId", appealId,
|
||||||
|
"approved", approve,
|
||||||
|
"comment", comment
|
||||||
|
));
|
||||||
|
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.ACCESS_DENIED.getCode(),
|
||||||
|
"Admin access required",
|
||||||
|
null);
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.INVALID_FIELD.getCode(),
|
||||||
|
e.getMessage(),
|
||||||
|
null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error reviewing appeal", e);
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.INVALID_FIELD.getCode(),
|
||||||
|
"Failed to review appeal: " + e.getMessage(),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение списка апелляций пользователя
|
||||||
|
*/
|
||||||
|
private BaseResponse handleGetUserAppeals(BaseRequest request, FileEntityRequest fileRequest) {
|
||||||
|
try {
|
||||||
|
Long userId = authService.useUserAuthToken(fileRequest.getToken());
|
||||||
|
|
||||||
|
int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1;
|
||||||
|
int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20;
|
||||||
|
|
||||||
|
Page<AppealResponse> appeals = moderationService.getUserAppeals(userId, page - 1, pageSize);
|
||||||
|
|
||||||
|
Map<String, Object> response = Map.of(
|
||||||
|
"appeals", appeals.getContent(),
|
||||||
|
"totalCount", appeals.getTotalElements(),
|
||||||
|
"totalPages", appeals.getTotalPages(),
|
||||||
|
"currentPage", page,
|
||||||
|
"pageSize", pageSize);
|
||||||
|
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.SUCCESS.getCode(),
|
||||||
|
MessageCode.SUCCESS.getDescription(),
|
||||||
|
response);
|
||||||
|
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.INVALID_TOKEN.getCode(),
|
||||||
|
"Authentication required",
|
||||||
|
null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error getting user appeals", e);
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.INVALID_FIELD.getCode(),
|
||||||
|
"Failed to get appeals: " + e.getMessage(),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение списка файлов на модерации
|
||||||
|
*/
|
||||||
|
private BaseResponse handleGetFilesForModeration(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;
|
||||||
|
|
||||||
|
Page<FileEntity> files = moderationService.getFilesForModeration(page - 1, pageSize);
|
||||||
|
|
||||||
|
List<Map<String, Object>> filesWithInfo = files.getContent().stream()
|
||||||
|
.map(file -> {
|
||||||
|
FileModerationInfo info = moderationService.getFileModerationInfo(file.getId());
|
||||||
|
return Map.of(
|
||||||
|
"fileId", file.getId(),
|
||||||
|
"fileName", file.getOriginalFileName(),
|
||||||
|
"userId", file.getUserId(),
|
||||||
|
"status", file.getStatus().name(),
|
||||||
|
"createdAt", file.getCreatedAt(),
|
||||||
|
"fileSize", file.getFileSize(),
|
||||||
|
"formattedSize", fileEntityService.formatFileSize(file.getFileSize()),
|
||||||
|
"moderationInfo", Map.of(
|
||||||
|
"hasActiveAppeal", info.getHasActiveAppeal(),
|
||||||
|
"appealInfo", info.getActiveAppeal() != null ?
|
||||||
|
Map.of(
|
||||||
|
"appealId", info.getActiveAppeal().getAppealId(),
|
||||||
|
"status", info.getActiveAppeal().getStatus(),
|
||||||
|
"createdAt", info.getActiveAppeal().getCreatedAt()
|
||||||
|
) : null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
Map<String, Object> response = Map.of(
|
||||||
|
"files", filesWithInfo,
|
||||||
|
"totalCount", files.getTotalElements(),
|
||||||
|
"totalPages", files.getTotalPages(),
|
||||||
|
"currentPage", page,
|
||||||
|
"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 files for moderation", e);
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.INVALID_FIELD.getCode(),
|
||||||
|
"Failed to get files: " + e.getMessage(),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение детальной информации о файле для модерации
|
||||||
|
*/
|
||||||
|
private BaseResponse handleGetFileModerationInfo(BaseRequest request, FileEntityRequest fileRequest) {
|
||||||
|
try {
|
||||||
|
// TODO
|
||||||
|
// authService.useAdminAuthToken(fileRequest.getToken());
|
||||||
|
|
||||||
|
String fileId = fileRequest.getFileId();
|
||||||
|
if (fileId == null) {
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.INVALID_FIELD.getCode(),
|
||||||
|
"fileId is required",
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileModerationInfo moderationInfo = moderationService.getFileModerationInfo(fileId);
|
||||||
|
|
||||||
|
FileEntity file = fileEntityRepository.findById(fileId)
|
||||||
|
.orElseThrow(() -> new FileEntityNotFoundException(fileId));
|
||||||
|
|
||||||
|
Map<String, Object> fileInfoMap = new HashMap<>();
|
||||||
|
fileInfoMap.put("id", file.getId());
|
||||||
|
fileInfoMap.put("fileName", file.getOriginalFileName());
|
||||||
|
fileInfoMap.put("storedFileName", file.getStoredFileName());
|
||||||
|
fileInfoMap.put("fileSize", file.getFileSize());
|
||||||
|
fileInfoMap.put("formattedSize", fileEntityService.formatFileSize(file.getFileSize()));
|
||||||
|
fileInfoMap.put("mimeType", file.getMimeType());
|
||||||
|
fileInfoMap.put("fileExtension", file.getFileExtension());
|
||||||
|
fileInfoMap.put("checksum", file.getChecksum());
|
||||||
|
fileInfoMap.put("status", file.getStatus() != null ? file.getStatus().name() : null);
|
||||||
|
fileInfoMap.put("createdAt", file.getCreatedAt());
|
||||||
|
fileInfoMap.put("updatedAt", file.getUpdatedAt());
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("fileInfo", fileInfoMap);
|
||||||
|
response.put("moderationInfo", moderationInfo != null ? moderationInfo : new HashMap<>());
|
||||||
|
response.put("moderationHistory", getModerationHistory(fileId));
|
||||||
|
response.put("appeals", getFileAppeals(fileId));
|
||||||
|
|
||||||
|
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 (FileEntityNotFoundException e) {
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.FILE_NOT_FOUND.getCode(),
|
||||||
|
e.getMessage(),
|
||||||
|
null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error getting file moderation info", e);
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.INVALID_FIELD.getCode(),
|
||||||
|
"Failed to get file info: " + e.getMessage(),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение истории модерации файла
|
||||||
|
*/
|
||||||
|
private List<Map<String, Object>> getModerationHistory(String fileId) {
|
||||||
|
List<ModerationLog> logs = moderationLogRepository.findByFileIdOrderByCreatedAtDesc(fileId);
|
||||||
|
if (logs == null || logs.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs.stream()
|
||||||
|
.map(log -> {
|
||||||
|
Map<String, Object> logMap = new HashMap<>();
|
||||||
|
logMap.put("id", log.getId());
|
||||||
|
logMap.put("moderatorId", log.getModeratorId());
|
||||||
|
logMap.put("oldStatus", log.getOldStatus() != null ? log.getOldStatus().name() : null);
|
||||||
|
logMap.put("newStatus", log.getNewStatus() != null ? log.getNewStatus().name() : null);
|
||||||
|
logMap.put("reason", log.getReason());
|
||||||
|
logMap.put("comment", log.getComment());
|
||||||
|
logMap.put("createdAt", log.getCreatedAt());
|
||||||
|
return logMap;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение всех апелляций файла
|
||||||
|
*/
|
||||||
|
private List<Map<String, Object>> getFileAppeals(String fileId) {
|
||||||
|
List<FileAppeal> appeals = fileAppealRepository.findByFileId(fileId);
|
||||||
|
if (appeals == null || appeals.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return appeals.stream()
|
||||||
|
.map(appeal -> {
|
||||||
|
Map<String, Object> appealMap = new HashMap<>();
|
||||||
|
appealMap.put("id", appeal.getId());
|
||||||
|
appealMap.put("userId", appeal.getUserId());
|
||||||
|
appealMap.put("appealReason", appeal.getAppealReason());
|
||||||
|
appealMap.put("additionalInfo", appeal.getAdditionalInfo());
|
||||||
|
appealMap.put("status", appeal.getStatus() != null ? appeal.getStatus().name() : null);
|
||||||
|
appealMap.put("adminComment", appeal.getAdminComment());
|
||||||
|
appealMap.put("moderatorId", appeal.getModeratorId());
|
||||||
|
appealMap.put("createdAt", appeal.getCreatedAt());
|
||||||
|
appealMap.put("resolvedAt", appeal.getResolvedAt());
|
||||||
|
return appealMap;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
private String formatFileSize(long size) {
|
private String formatFileSize(long size) {
|
||||||
if (size < 1024) return size + " B";
|
if (size < 1024) return size + " B";
|
||||||
int exp = (int) (Math.log(size) / Math.log(1024));
|
int exp = (int) (Math.log(size) / Math.log(1024));
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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.FileAppeal;
|
||||||
|
import ru.soune.nocopy.entity.file.AppealStatus;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface FileAppealRepository extends JpaRepository<FileAppeal, String> {
|
||||||
|
|
||||||
|
Optional<FileAppeal> findByFileIdAndStatus(String fileId, AppealStatus status);
|
||||||
|
|
||||||
|
List<FileAppeal> findByFileId(String fileId);
|
||||||
|
|
||||||
|
Page<FileAppeal> findByStatus(AppealStatus status, Pageable pageable);
|
||||||
|
|
||||||
|
Page<FileAppeal> findByUserId(Long userId, Pageable pageable);
|
||||||
|
|
||||||
|
@Query("SELECT fa FROM FileAppeal fa WHERE fa.status = :status ORDER BY fa.createdAt ASC")
|
||||||
|
Page<FileAppeal> findPendingAppeals(@Param("status") AppealStatus status, Pageable pageable);
|
||||||
|
|
||||||
|
boolean existsByFileIdAndStatusIn(String fileId, List<AppealStatus> statuses);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package ru.soune.nocopy.repository;
|
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.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
@@ -66,4 +68,8 @@ public interface FileEntityRepository extends JpaRepository<FileEntity, String>
|
|||||||
Integer findMaxSupportId();
|
Integer findMaxSupportId();
|
||||||
|
|
||||||
List<FileEntity> findByThumbnailPathIsNull();
|
List<FileEntity> findByThumbnailPathIsNull();
|
||||||
|
|
||||||
|
Page<FileEntity> findByStatusIn(List<FileStatus> statuses, Pageable pageable);
|
||||||
|
|
||||||
|
List<FileEntity> findByUserIdAndStatusIn(Long userId, List<FileStatus> statuses);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package ru.soune.nocopy.repository;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import ru.soune.nocopy.entity.file.moderation.ModerationLog;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ModerationLogRepository extends JpaRepository<ModerationLog, String> {
|
||||||
|
|
||||||
|
List<ModerationLog> findByFileIdOrderByCreatedAtDesc(String fileId);
|
||||||
|
|
||||||
|
List<ModerationLog> findByModeratorId(Long moderatorId);
|
||||||
|
}
|
||||||
@@ -16,7 +16,6 @@ import ru.soune.nocopy.entity.monitoring.MonitoringType;
|
|||||||
import ru.soune.nocopy.entity.user.User;
|
import ru.soune.nocopy.entity.user.User;
|
||||||
import ru.soune.nocopy.exception.DuplicateImageException;
|
import ru.soune.nocopy.exception.DuplicateImageException;
|
||||||
import ru.soune.nocopy.exception.FileEntityNotFoundException;
|
import ru.soune.nocopy.exception.FileEntityNotFoundException;
|
||||||
import ru.soune.nocopy.handler.MonitoringHandler;
|
|
||||||
import ru.soune.nocopy.repository.FileEntityRepository;
|
import ru.soune.nocopy.repository.FileEntityRepository;
|
||||||
import ru.soune.nocopy.repository.FileMonitoringRepository;
|
import ru.soune.nocopy.repository.FileMonitoringRepository;
|
||||||
import ru.soune.nocopy.repository.SimilarImageProjection;
|
import ru.soune.nocopy.repository.SimilarImageProjection;
|
||||||
@@ -252,7 +251,7 @@ public class FileEntityService {
|
|||||||
return totalSize != null ? totalSize : 0L;
|
return totalSize != null ? totalSize : 0L;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void changeStatus(ProtectionStatus newStatus, String fileId) {
|
public void changeProtectionStatus(ProtectionStatus newStatus, String fileId) {
|
||||||
FileEntity fileEntity = fileEntityRepository.findById(fileId)
|
FileEntity fileEntity = fileEntityRepository.findById(fileId)
|
||||||
.orElseThrow(() -> new FileEntityNotFoundException(fileId));
|
.orElseThrow(() -> new FileEntityNotFoundException(fileId));
|
||||||
fileEntity.setProtectionStatus(newStatus);
|
fileEntity.setProtectionStatus(newStatus);
|
||||||
@@ -260,6 +259,14 @@ public class FileEntityService {
|
|||||||
fileEntityRepository.save(fileEntity);
|
fileEntityRepository.save(fileEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void changeFileStatus(FileStatus newStatus, String fileId) {
|
||||||
|
FileEntity fileEntity = fileEntityRepository.findById(fileId)
|
||||||
|
.orElseThrow(() -> new FileEntityNotFoundException(fileId));
|
||||||
|
fileEntity.setStatus(newStatus);
|
||||||
|
|
||||||
|
fileEntityRepository.save(fileEntity);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void writeProtectedFile(String id, byte[] data, String fileExt) throws IOException {
|
public void writeProtectedFile(String id, byte[] data, String fileExt) throws IOException {
|
||||||
FileEntity fileEntity = fileEntityRepository.findById(id)
|
FileEntity fileEntity = fileEntityRepository.findById(id)
|
||||||
|
|||||||
@@ -16,21 +16,21 @@ public class NoCopyProcessingListenerImpl implements FileProtector.ProcessingLis
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStartProcessing(FileProtector.FileInfo fileInfo) {
|
public void onStartProcessing(FileProtector.FileInfo fileInfo) {
|
||||||
fileEntityService.changeStatus(ProtectionStatus.PROCESSING, fileInfo.getId());
|
fileEntityService.changeProtectionStatus(ProtectionStatus.PROCESSING, fileInfo.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onProcessingFailed(FileProtector.FileInfo fileInfo) {
|
public void onProcessingFailed(FileProtector.FileInfo fileInfo) {
|
||||||
fileEntityService.changeStatus(ProtectionStatus.FAILED, fileInfo.getId());
|
fileEntityService.changeProtectionStatus(ProtectionStatus.FAILED, fileInfo.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSavingFailed(FileProtector.FileInfo fileInfo) {
|
public void onSavingFailed(FileProtector.FileInfo fileInfo) {
|
||||||
fileEntityService.changeStatus(ProtectionStatus.FAILED_SAVE, fileInfo.getId());
|
fileEntityService.changeProtectionStatus(ProtectionStatus.FAILED_SAVE, fileInfo.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFinish(FileProtector.FileInfo fileInfo) {
|
public void onFinish(FileProtector.FileInfo fileInfo) {
|
||||||
fileEntityService.changeStatus(ProtectionStatus.PROTECTED, fileInfo.getId());;
|
fileEntityService.changeProtectionStatus(ProtectionStatus.PROTECTED, fileInfo.getId());;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
package ru.soune.nocopy.service.file.moderation;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
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.*;
|
||||||
|
import ru.soune.nocopy.entity.file.*;
|
||||||
|
import ru.soune.nocopy.entity.file.moderation.ModerationLog;
|
||||||
|
import ru.soune.nocopy.exception.FileEntityNotFoundException;
|
||||||
|
|
||||||
|
import ru.soune.nocopy.exception.InvalidAppealException;
|
||||||
|
import ru.soune.nocopy.repository.FileAppealRepository;
|
||||||
|
import ru.soune.nocopy.repository.FileEntityRepository;
|
||||||
|
import ru.soune.nocopy.repository.ModerationLogRepository;
|
||||||
|
import ru.soune.nocopy.service.file.FileEntityService;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ModerationService {
|
||||||
|
private final FileEntityRepository fileEntityRepository;
|
||||||
|
|
||||||
|
private final FileAppealRepository fileAppealRepository;
|
||||||
|
|
||||||
|
private final ModerationLogRepository moderationLogRepository;
|
||||||
|
|
||||||
|
private final FileEntityService fileEntityService;
|
||||||
|
|
||||||
|
private static final List<FileStatus> MODERATION_NEEDED_STATUSES = Arrays.asList(
|
||||||
|
FileStatus.MODERATION,
|
||||||
|
FileStatus.BLOCKED
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Модерация файла администратором
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void moderateFile(ModerationActionRequest request, Long moderatorId) {
|
||||||
|
FileEntity file = fileEntityRepository.findById(request.getFileId())
|
||||||
|
.orElseThrow(() -> new FileEntityNotFoundException(request.getFileId()));
|
||||||
|
|
||||||
|
if (!MODERATION_NEEDED_STATUSES.contains(file.getStatus())) {
|
||||||
|
throw new IllegalStateException("File is not in moderation state. Current status: " + file.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
ModerationLog log = ModerationLog.builder()
|
||||||
|
.fileId(file.getId())
|
||||||
|
.moderatorId(moderatorId)
|
||||||
|
.oldStatus(file.getStatus())
|
||||||
|
.newStatus(request.getNewStatus())
|
||||||
|
.reason(request.getComment())
|
||||||
|
.moderationReasonId(request.getModerationReasonId())
|
||||||
|
.comment(request.getComment())
|
||||||
|
.build();
|
||||||
|
moderationLogRepository.save(log);
|
||||||
|
|
||||||
|
file.setStatus(request.getNewStatus());
|
||||||
|
file.setModerationReasonId(request.getModerationReasonId());
|
||||||
|
fileEntityRepository.save(file);
|
||||||
|
|
||||||
|
if (request.getNewStatus() == FileStatus.BLOCKED ||
|
||||||
|
request.getNewStatus() == FileStatus.VIOLATION) {
|
||||||
|
|
||||||
|
fileAppealRepository.findByFileIdAndStatus(file.getId(), AppealStatus.PENDING)
|
||||||
|
.ifPresent(appeal -> {
|
||||||
|
appeal.setStatus(AppealStatus.REJECTED);
|
||||||
|
appeal.setAdminComment("Апелляция отклонена в процессе модерации");
|
||||||
|
appeal.setResolvedAt(LocalDateTime.now());
|
||||||
|
fileAppealRepository.save(appeal);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Подача апелляции пользователем
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public AppealResponse submitAppeal(AppealRequest request, Long userId) {
|
||||||
|
FileEntity file = fileEntityRepository.findById(request.getFileId())
|
||||||
|
.orElseThrow(() -> new FileEntityNotFoundException(request.getFileId()));
|
||||||
|
|
||||||
|
if (!file.getUserId().equals(userId)) {
|
||||||
|
throw new SecurityException("User is not the owner of this file");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canAppeal(file)) {
|
||||||
|
throw new InvalidAppealException("Cannot appeal this file. Current status: " + file.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasActiveAppeal(file.getId())) {
|
||||||
|
throw new InvalidAppealException("Active appeal already exists for this file");
|
||||||
|
}
|
||||||
|
|
||||||
|
FileAppeal appeal = FileAppeal.builder()
|
||||||
|
.fileId(file.getId())
|
||||||
|
.userId(userId)
|
||||||
|
.appealReason(request.getAppealReason())
|
||||||
|
.additionalInfo(request.getAdditionalInfo())
|
||||||
|
.status(AppealStatus.PENDING)
|
||||||
|
.moderationBeforeStatus(file.getStatus())
|
||||||
|
.moderationReasonId(file.getModerationReasonId())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
FileAppeal saved = fileAppealRepository.save(appeal);
|
||||||
|
|
||||||
|
return convertToResponse(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Рассмотрение апелляции администратором
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void reviewAppeal(String appealId, boolean approve, String comment, Long moderatorId) {
|
||||||
|
FileAppeal appeal = fileAppealRepository.findById(appealId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Appeal not found: " + appealId));
|
||||||
|
|
||||||
|
if (appeal.getStatus() != AppealStatus.PENDING) {
|
||||||
|
throw new IllegalStateException("Appeal already processed");
|
||||||
|
}
|
||||||
|
|
||||||
|
FileEntity file = fileEntityRepository.findById(appeal.getFileId())
|
||||||
|
.orElseThrow(() -> new FileEntityNotFoundException(appeal.getFileId()));
|
||||||
|
|
||||||
|
appeal.setStatus(approve ? AppealStatus.APPROVED : AppealStatus.REJECTED);
|
||||||
|
appeal.setAdminComment(comment);
|
||||||
|
appeal.setModeratorId(moderatorId);
|
||||||
|
appeal.setResolvedAt(LocalDateTime.now());
|
||||||
|
|
||||||
|
if (approve) {
|
||||||
|
file.setStatus(FileStatus.ACTIVE);
|
||||||
|
file.setModerationReasonId(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
ModerationLog log = ModerationLog.builder()
|
||||||
|
.fileId(file.getId())
|
||||||
|
.moderatorId(moderatorId)
|
||||||
|
.oldStatus(file.getStatus())
|
||||||
|
.newStatus(file.getStatus())
|
||||||
|
.comment("Appeal review: " + comment)
|
||||||
|
.build();
|
||||||
|
moderationLogRepository.save(log);
|
||||||
|
|
||||||
|
fileEntityRepository.save(file);
|
||||||
|
fileAppealRepository.save(appeal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка, можно ли подать апелляцию на файл
|
||||||
|
*/
|
||||||
|
public boolean canAppeal(FileEntity file) {
|
||||||
|
if (file.getStatus() != FileStatus.BLOCKED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка наличия активной апелляции
|
||||||
|
*/
|
||||||
|
public boolean hasActiveAppeal(String fileId) {
|
||||||
|
return fileAppealRepository.existsByFileIdAndStatusIn(
|
||||||
|
fileId,
|
||||||
|
Arrays.asList(AppealStatus.PENDING, AppealStatus.IN_REVIEW)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение информации о файле для модерации
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public FileModerationInfo getFileModerationInfo(String fileId) {
|
||||||
|
FileEntity file = fileEntityRepository.findById(fileId)
|
||||||
|
.orElseThrow(() -> new FileEntityNotFoundException(fileId));
|
||||||
|
|
||||||
|
FileAppeal activeAppeal = fileAppealRepository.findByFileIdAndStatus(
|
||||||
|
fileId, AppealStatus.PENDING).orElse(null);
|
||||||
|
|
||||||
|
return FileModerationInfo.builder()
|
||||||
|
.fileId(file.getId())
|
||||||
|
.fileName(file.getOriginalFileName())
|
||||||
|
.currentStatus(file.getStatus())
|
||||||
|
.uploadDate(file.getCreatedAt())
|
||||||
|
.hasActiveAppeal(activeAppeal != null)
|
||||||
|
.activeAppeal(activeAppeal != null ?
|
||||||
|
AppealInfo.builder()
|
||||||
|
.appealId(activeAppeal.getId())
|
||||||
|
.status(activeAppeal.getStatus().name())
|
||||||
|
.createdAt(activeAppeal.getCreatedAt())
|
||||||
|
.build() : null)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение списка файлов на модерации
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public Page<FileEntity> getFilesForModeration(int page, int size) {
|
||||||
|
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
|
||||||
|
return fileEntityRepository.findByStatusIn(
|
||||||
|
Arrays.asList(FileStatus.MODERATION, FileStatus.BLOCKED),
|
||||||
|
pageable
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение апелляций пользователя
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public Page<AppealResponse> getUserAppeals(Long userId, int page, int size) {
|
||||||
|
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
|
||||||
|
return fileAppealRepository.findByUserId(userId, pageable)
|
||||||
|
.map(this::convertToResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AppealResponse convertToResponse(FileAppeal appeal) {
|
||||||
|
return AppealResponse.builder()
|
||||||
|
.appealId(appeal.getId())
|
||||||
|
.fileId(appeal.getFileId())
|
||||||
|
.appealReason(appeal.getAppealReason())
|
||||||
|
.additionalInfo(appeal.getAdditionalInfo())
|
||||||
|
.status(appeal.getStatus().name())
|
||||||
|
.adminComment(appeal.getAdminComment())
|
||||||
|
.createdAt(appeal.getCreatedAt())
|
||||||
|
.resolvedAt(appeal.getResolvedAt())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user