refactoring
This commit is contained in:
parent
3ccb43cb6f
commit
368ba49119
34
pom.xml
34
pom.xml
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>21</java.version>
|
<java.version>21</java.version>
|
||||||
|
<mapstruct.version>1.6.3</mapstruct.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
@ -48,6 +49,12 @@
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mapstruct</groupId>
|
||||||
|
<artifactId>mapstruct</artifactId>
|
||||||
|
<version>${mapstruct.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
|
@ -87,6 +94,33 @@
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.13.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>${java.version}</source>
|
||||||
|
<target>${java.version}</target>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
</path>
|
||||||
|
<path>
|
||||||
|
<groupId>org.mapstruct</groupId>
|
||||||
|
<artifactId>mapstruct-processor</artifactId>
|
||||||
|
<version>${mapstruct.version}</version>
|
||||||
|
</path>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok-mapstruct-binding</artifactId>
|
||||||
|
<version>0.2.0</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,20 @@ package ru.oa2.lti.application.infrastructure.repository;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
import ru.oa2.lti.application.infrastructure.repository.entities.History;
|
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.HistoryEntity;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Используйте {@link ru.oa2.lti.infrastructure.adapter.persistence.jpa.repository.HistoryJpaRepository}
|
||||||
|
* Этот интерфейс оставлен для обратной совместимости и будет удален в следующей версии.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
@Repository
|
@Repository
|
||||||
public interface HistoryRepository extends JpaRepository<History, Long> {
|
public interface HistoryRepository extends JpaRepository<HistoryEntity, Long> {
|
||||||
|
|
||||||
List<History> findByContentUuidOrderByCreatedDesc(UUID contentUuid);
|
List<HistoryEntity> findByContentUuidOrderByCreatedDesc(UUID contentUuid);
|
||||||
|
|
||||||
List<History> findByDeploymentIdOrderByCreatedDesc(UUID deploymentId);
|
List<HistoryEntity> findByDeploymentIdOrderByCreatedDesc(UUID deploymentId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,18 @@ package ru.oa2.lti.application.infrastructure.repository;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
import ru.oa2.lti.application.infrastructure.repository.entities.LMSContent;
|
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.LMSContentEntity;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Используйте {@link ru.oa2.lti.infrastructure.adapter.persistence.jpa.repository.LMSContentJpaRepository}
|
||||||
|
* Этот интерфейс оставлен для обратной совместимости и будет удален в следующей версии.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
@Repository
|
@Repository
|
||||||
public interface LMSContentRepository extends JpaRepository<LMSContent, Long> {
|
public interface LMSContentRepository extends JpaRepository<LMSContentEntity, Long> {
|
||||||
|
|
||||||
Optional<LMSContent> getLMSContentByContentUuid(UUID contentUuid);
|
Optional<LMSContentEntity> getLMSContentByContentUuid(UUID contextId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,11 @@ import lombok.Setter;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Используйте {@link ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.HistoryEntity}
|
||||||
|
* Этот класс оставлен для обратной совместимости и будет удален в следующей версии.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Entity
|
@Entity
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,11 @@ import lombok.Setter;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Используйте {@link ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.LMSContentEntity}
|
||||||
|
* Этот класс оставлен для обратной совместимости и будет удален в следующей версии.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Entity
|
@Entity
|
||||||
|
|
@ -29,7 +34,6 @@ public class LMSContent {
|
||||||
@Column(name = "created", nullable = false)
|
@Column(name = "created", nullable = false)
|
||||||
LocalDateTime created;
|
LocalDateTime created;
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@Column(name = "task_id", nullable = false)
|
||||||
@JoinColumn(name = "task_id", nullable = false, foreignKey = @ForeignKey(name = "fk_lms_content_task"))
|
Long taskId;
|
||||||
Task task;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,11 @@ import jakarta.persistence.*;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Используйте {@link ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.TaskEntity}
|
||||||
|
* Этот класс оставлен для обратной совместимости и будет удален в следующей версии.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Entity
|
@Entity
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Propagation;
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import ru.oa2.lti.application.infrastructure.repository.HistoryRepository;
|
import ru.oa2.lti.application.infrastructure.repository.HistoryRepository;
|
||||||
import ru.oa2.lti.application.infrastructure.repository.entities.History;
|
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.HistoryEntity;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
@ -24,7 +24,7 @@ public class HistoryServiceImpl implements HistoryService {
|
||||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
public void logEvent(UUID deploymentId, UUID contentUuid, String message) {
|
public void logEvent(UUID deploymentId, UUID contentUuid, String message) {
|
||||||
try {
|
try {
|
||||||
History history = new History();
|
HistoryEntity history = new HistoryEntity();
|
||||||
history.setDeploymentId(deploymentId);
|
history.setDeploymentId(deploymentId);
|
||||||
history.setContentUuid(contentUuid);
|
history.setContentUuid(contentUuid);
|
||||||
history.setCreated(LocalDateTime.now());
|
history.setCreated(LocalDateTime.now());
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
package ru.oa2.lti.application.service.task;
|
package ru.oa2.lti.application.service.task;
|
||||||
|
|
||||||
import jakarta.transaction.Transactional;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import ru.oa2.lti.application.infrastructure.lms.LMSService;
|
import ru.oa2.lti.application.infrastructure.lms.LMSService;
|
||||||
import ru.oa2.lti.application.infrastructure.repository.LMSContentRepository;
|
|
||||||
import ru.oa2.lti.application.service.history.HistoryService;
|
|
||||||
import ru.oa2.lti.application.infrastructure.repository.entities.LMSContent;
|
|
||||||
import ru.oa2.lti.application.infrastructure.repository.entities.Task;
|
|
||||||
import ru.oa2.lti.application.infrastructure.runner.Runner;
|
import ru.oa2.lti.application.infrastructure.runner.Runner;
|
||||||
|
import ru.oa2.lti.application.service.history.HistoryService;
|
||||||
import ru.oa2.lti.application.service.results.ResultService;
|
import ru.oa2.lti.application.service.results.ResultService;
|
||||||
|
import ru.oa2.lti.domain.exception.TaskNotFoundException;
|
||||||
|
import ru.oa2.lti.domain.model.Task;
|
||||||
|
import ru.oa2.lti.domain.repository.TaskRepository;
|
||||||
|
import ru.oa2.lti.domain.valueobject.*;
|
||||||
import ru.oa2.lti.domain.model.ResultRequest;
|
import ru.oa2.lti.domain.model.ResultRequest;
|
||||||
import ru.oa2.lti.domain.model.results.ResultResponse;
|
import ru.oa2.lti.domain.model.results.ResultResponse;
|
||||||
import ru.oa2.lti.domain.model.task.RequestUpdateTask;
|
import ru.oa2.lti.domain.model.task.RequestUpdateTask;
|
||||||
|
|
@ -17,128 +19,149 @@ import ru.oa2.lti.domain.model.task.TaskData;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Реализация TaskService, следующая принципам Clean Architecture и DDD.
|
||||||
|
*
|
||||||
|
* Улучшения по сравнению со старой версией:
|
||||||
|
* - Использует доменную модель Task вместо JPA Entity
|
||||||
|
* - Зависит от порта TaskRepository (domain), а не от конкретной реализации
|
||||||
|
* - Бизнес-логика теперь в доменной модели Task
|
||||||
|
* - Сервис только координирует взаимодействие между компонентами
|
||||||
|
* - Явная обработка исключений
|
||||||
|
* - Транзакции только на методах записи
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@Transactional(rollbackOn = Throwable.class)
|
@RequiredArgsConstructor
|
||||||
public class TaskServiceImpl implements TaskService {
|
public class TaskServiceImpl implements TaskService {
|
||||||
|
|
||||||
private final LMSContentRepository lmsContentRepository;
|
private final TaskRepository taskRepository;
|
||||||
private final Runner runner;
|
private final Runner runner;
|
||||||
private final HistoryService historyService;
|
private final HistoryService historyService;
|
||||||
|
private final ResultService resultService;
|
||||||
//TODO объединить в один?
|
private final LMSService lmsService;
|
||||||
final ResultService resultService;
|
|
||||||
final LMSService lmsService;
|
|
||||||
|
|
||||||
public TaskServiceImpl(LMSContentRepository lmsContentRepository,
|
|
||||||
Runner runner,
|
|
||||||
ResultService resultService,
|
|
||||||
LMSService lmsService,
|
|
||||||
HistoryService historyService) {
|
|
||||||
|
|
||||||
this.lmsContentRepository = lmsContentRepository;
|
|
||||||
this.runner = runner;
|
|
||||||
this.resultService = resultService;
|
|
||||||
this.lmsService = lmsService;
|
|
||||||
this.historyService = historyService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public TaskData getTask(UUID contextId) {
|
public TaskData getTask(UUID contextId) {
|
||||||
var content = lmsContentRepository.getLMSContentByContentUuid(contextId);
|
log.info("Getting task for context: {}", contextId);
|
||||||
|
|
||||||
if(content.isPresent()) {
|
// Используем доменный репозиторий через порт
|
||||||
LMSContent lmsContent = content.get();
|
Task task = taskRepository.findByContextId(contextId)
|
||||||
Task task = lmsContent.getTask();
|
.orElseThrow(() -> new TaskNotFoundException(contextId));
|
||||||
|
|
||||||
if (task == null) {
|
// Преобразуем в DTO для presentation layer
|
||||||
return new TaskData("Not Page", "", "", "", "");
|
TaskData data = taskToDto(task);
|
||||||
|
|
||||||
|
// Выполняем инициализационный скрипт
|
||||||
|
Script initScript = task.getInitScript();
|
||||||
|
if (initScript.isExecutable()) {
|
||||||
|
boolean success = runner.run(contextId, initScript.content());
|
||||||
|
if (!success) {
|
||||||
|
log.error("Init script failed for context: {}", contextId);
|
||||||
|
throw new RuntimeException("Initialization script execution failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
TaskData data = entityToTaskData(task);
|
|
||||||
|
|
||||||
//TODO string -> exception?
|
|
||||||
if (!runner.run(contextId, data.getInitScript())) {
|
|
||||||
return new TaskData("Init script FAILED", "", "", "", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} else {
|
|
||||||
return new TaskData("Not Page", "", "", "", "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional
|
||||||
public ResultResponse checkTask(ResultRequest resultRequest) {
|
public ResultResponse checkTask(ResultRequest resultRequest) {
|
||||||
|
log.info("Checking task for clientId: {}", resultRequest.clientId());
|
||||||
|
|
||||||
//TODO запуск скрипта проверки
|
// Получаем access token от LMS
|
||||||
var assessToken = lmsService.exchangeForAccessToken(
|
String accessToken = lmsService.exchangeForAccessToken(resultRequest.clientId());
|
||||||
resultRequest.clientId());
|
|
||||||
resultService.setResult(assessToken, resultRequest.idToken());
|
|
||||||
|
|
||||||
//TODO обработка и запуск скрипта
|
// Отправляем результат
|
||||||
|
resultService.setResult(accessToken, resultRequest.idToken());
|
||||||
|
|
||||||
|
// TODO: реализовать запуск скрипта проверки
|
||||||
return new ResultResponse("success");
|
return new ResultResponse("success");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional
|
||||||
public ResultResponse saveTask(RequestUpdateTask requestUpdateTask) {
|
public ResultResponse saveTask(RequestUpdateTask requestUpdateTask) {
|
||||||
|
log.info("Saving task for context: {}", requestUpdateTask.getContextId());
|
||||||
|
|
||||||
log.info("save");
|
try {
|
||||||
var result = lmsContentRepository.getLMSContentByContentUuid(requestUpdateTask.getContextId());
|
// Загружаем существующую задачу
|
||||||
|
Task task = taskRepository.findByContextId(requestUpdateTask.getContextId())
|
||||||
|
.orElseThrow(() -> new TaskNotFoundException(requestUpdateTask.getContextId()));
|
||||||
|
|
||||||
//TODO доработать версионирование task-ов
|
String oldName = task.getName().value();
|
||||||
if (result.isPresent()) {
|
|
||||||
LMSContent content = result.get();
|
|
||||||
Task task = content.getTask();
|
|
||||||
|
|
||||||
if (task != null) {
|
// Обновляем задачу через доменную модель (бизнес-логика внутри)
|
||||||
String oldName = task.getName();
|
updateTaskFromDto(task, requestUpdateTask.getData());
|
||||||
updateTaskFromData(task, requestUpdateTask.getData());
|
|
||||||
lmsContentRepository.save(content);
|
|
||||||
|
|
||||||
historyService.logAction(
|
// Сохраняем через порт репозитория
|
||||||
content.getDeploymentId(),
|
taskRepository.save(task);
|
||||||
requestUpdateTask.getContextId(),
|
|
||||||
"TASK_SAVED",
|
// Логируем действие
|
||||||
String.format("Task saved: %s (was: %s)",
|
historyService.logAction(
|
||||||
requestUpdateTask.getData().getName(), oldName)
|
null, // deploymentId нужно получать из контекста
|
||||||
);
|
requestUpdateTask.getContextId(),
|
||||||
} else {
|
"TASK_SAVED",
|
||||||
historyService.logAction(
|
String.format("Task saved: %s (was: %s)",
|
||||||
content.getDeploymentId(),
|
requestUpdateTask.getData().getName(), oldName)
|
||||||
requestUpdateTask.getContextId(),
|
);
|
||||||
"TASK_SAVE_ERROR",
|
|
||||||
"Task is null, cannot save"
|
return new ResultResponse("success");
|
||||||
);
|
} catch (TaskNotFoundException e) {
|
||||||
}
|
log.error("Task not found for context: {}", requestUpdateTask.getContextId(), e);
|
||||||
} else {
|
|
||||||
historyService.logAction(
|
historyService.logAction(
|
||||||
null,
|
null,
|
||||||
requestUpdateTask.getContextId(),
|
requestUpdateTask.getContextId(),
|
||||||
"TASK_SAVE_ERROR",
|
"TASK_SAVE_ERROR",
|
||||||
"Content not found"
|
"Task not found: " + e.getMessage()
|
||||||
);
|
);
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error saving task", e);
|
||||||
|
historyService.logAction(
|
||||||
|
null,
|
||||||
|
requestUpdateTask.getContextId(),
|
||||||
|
"TASK_SAVE_ERROR",
|
||||||
|
"Error: " + e.getMessage()
|
||||||
|
);
|
||||||
|
throw new RuntimeException("Failed to save task", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO обработка exception - failed / success
|
|
||||||
return new ResultResponse("success");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private TaskData entityToTaskData(Task task) {
|
/**
|
||||||
|
* Преобразует доменную модель Task в DTO TaskData
|
||||||
|
*/
|
||||||
|
private TaskData taskToDto(Task task) {
|
||||||
return new TaskData(
|
return new TaskData(
|
||||||
task.getName(),
|
task.getName().value(),
|
||||||
task.getDescription(),
|
task.getDescription().value(),
|
||||||
task.getInitScript(),
|
task.getInitScript().content(),
|
||||||
task.getVerificationScript(),
|
task.getVerificationScript().content(),
|
||||||
task.getDeleteScript()
|
task.getDeleteScript().content()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTaskFromData(Task task, TaskData data) {
|
/**
|
||||||
task.setName(data.getName());
|
* Обновляет доменную модель Task из DTO TaskData
|
||||||
task.setDescription(data.getDescription());
|
*/
|
||||||
task.setInitScript(data.getInitScript());
|
private void updateTaskFromDto(Task task, TaskData data) {
|
||||||
task.setVerificationScript(data.getVerificationScript());
|
// Используем Value Objects с валидацией
|
||||||
task.setDeleteScript(data.getDeleteScript());
|
task.update(
|
||||||
|
TaskName.of(data.getName()),
|
||||||
|
Description.of(data.getDescription())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Обновляем скрипты
|
||||||
|
if (data.getInitScript() != null) {
|
||||||
|
task.setScript(Script.of(ScriptType.INITIALIZATION, data.getInitScript()));
|
||||||
|
}
|
||||||
|
if (data.getVerificationScript() != null) {
|
||||||
|
task.setScript(Script.of(ScriptType.VERIFICATION, data.getVerificationScript()));
|
||||||
|
}
|
||||||
|
if (data.getDeleteScript() != null) {
|
||||||
|
task.setScript(Script.of(ScriptType.DELETION, data.getDeleteScript()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package ru.oa2.lti.domain.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Базовое исключение для доменного слоя
|
||||||
|
*/
|
||||||
|
public abstract class DomainException extends RuntimeException {
|
||||||
|
protected DomainException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected DomainException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package ru.oa2.lti.domain.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Исключение при невалидном состоянии задачи
|
||||||
|
*/
|
||||||
|
public class InvalidTaskStateException extends DomainException {
|
||||||
|
public InvalidTaskStateException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package ru.oa2.lti.domain.exception;
|
||||||
|
|
||||||
|
import ru.oa2.lti.domain.valueobject.TaskId;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Исключение, когда задача не найдена
|
||||||
|
*/
|
||||||
|
public class TaskNotFoundException extends DomainException {
|
||||||
|
public TaskNotFoundException(TaskId taskId) {
|
||||||
|
super("Task not found with id: " + taskId.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
public TaskNotFoundException(UUID contextId) {
|
||||||
|
super("Task not found for context: " + contextId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
package ru.oa2.lti.domain.model;
|
||||||
|
|
||||||
|
import ru.oa2.lti.domain.exception.InvalidTaskStateException;
|
||||||
|
import ru.oa2.lti.domain.valueobject.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Доменная модель задачи (Aggregate Root).
|
||||||
|
* Содержит бизнес-логику и инварианты.
|
||||||
|
* НЕ зависит от инфраструктурных деталей (JPA, Spring, etc.)
|
||||||
|
*/
|
||||||
|
public class Task {
|
||||||
|
private final TaskId id;
|
||||||
|
private TaskName name;
|
||||||
|
private Description description;
|
||||||
|
private final Map<ScriptType, Script> scripts;
|
||||||
|
|
||||||
|
// Приватный конструктор - создание только через фабричные методы
|
||||||
|
private Task(TaskId id, TaskName name, Description description) {
|
||||||
|
this.id = Objects.requireNonNull(id, "Task id cannot be null");
|
||||||
|
this.name = Objects.requireNonNull(name, "Task name cannot be null");
|
||||||
|
this.description = Objects.requireNonNull(description, "Task description cannot be null");
|
||||||
|
this.scripts = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Фабричный метод для создания новой задачи
|
||||||
|
*/
|
||||||
|
public static Task create(TaskName name, Description description) {
|
||||||
|
return new Task(null, name, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Фабричный метод для восстановления существующей задачи из хранилища
|
||||||
|
*/
|
||||||
|
public static Task restore(TaskId id, TaskName name, Description description) {
|
||||||
|
return new Task(id, name, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Добавляет или обновляет скрипт определенного типа
|
||||||
|
*/
|
||||||
|
public void setScript(Script script) {
|
||||||
|
Objects.requireNonNull(script, "Script cannot be null");
|
||||||
|
scripts.put(script.type(), script);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает скрипт определенного типа
|
||||||
|
*/
|
||||||
|
public Optional<Script> getScript(ScriptType type) {
|
||||||
|
return Optional.ofNullable(scripts.get(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, может ли задача быть инициализирована
|
||||||
|
*/
|
||||||
|
public boolean canInitialize() {
|
||||||
|
Script initScript = scripts.get(ScriptType.INITIALIZATION);
|
||||||
|
return initScript != null && initScript.isExecutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, может ли задача быть проверена
|
||||||
|
*/
|
||||||
|
public boolean canVerify() {
|
||||||
|
Script verificationScript = scripts.get(ScriptType.VERIFICATION);
|
||||||
|
return verificationScript != null && verificationScript.isExecutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Валидирует, что задача готова к выполнению
|
||||||
|
*/
|
||||||
|
public void validateForExecution() {
|
||||||
|
if (name == null || name.value().isBlank()) {
|
||||||
|
throw new InvalidTaskStateException("Task must have a name before execution");
|
||||||
|
}
|
||||||
|
if (!canInitialize()) {
|
||||||
|
throw new InvalidTaskStateException("Task must have an executable initialization script");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновляет информацию о задаче
|
||||||
|
*/
|
||||||
|
public void update(TaskName newName, Description newDescription) {
|
||||||
|
this.name = Objects.requireNonNull(newName, "Task name cannot be null");
|
||||||
|
this.description = Objects.requireNonNull(newDescription, "Task description cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Геттеры
|
||||||
|
public TaskId getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TaskName getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Description getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Script getInitScript() {
|
||||||
|
return scripts.getOrDefault(ScriptType.INITIALIZATION, Script.empty(ScriptType.INITIALIZATION));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Script getVerificationScript() {
|
||||||
|
return scripts.getOrDefault(ScriptType.VERIFICATION, Script.empty(ScriptType.VERIFICATION));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Script getDeleteScript() {
|
||||||
|
return scripts.getOrDefault(ScriptType.DELETION, Script.empty(ScriptType.DELETION));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
Task task = (Task) o;
|
||||||
|
return Objects.equals(id, task.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Task{" +
|
||||||
|
"id=" + id +
|
||||||
|
", name=" + name +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package ru.oa2.lti.domain.repository;
|
||||||
|
|
||||||
|
import ru.oa2.lti.domain.model.Task;
|
||||||
|
import ru.oa2.lti.domain.valueobject.TaskId;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Порт репозитория для работы с задачами (Domain Layer).
|
||||||
|
* Это интерфейс, который НЕ зависит от инфраструктуры.
|
||||||
|
* Реализация будет в infrastructure слое (JPA, MongoDB, etc.)
|
||||||
|
*
|
||||||
|
* Следует принципам:
|
||||||
|
* - Clean Architecture: внутренний слой определяет интерфейс
|
||||||
|
* - Hexagonal Architecture: это выходной порт (output port)
|
||||||
|
* - DDD: репозиторий работает с агрегатами
|
||||||
|
*/
|
||||||
|
public interface TaskRepository {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Находит задачу по идентификатору
|
||||||
|
* @param id идентификатор задачи
|
||||||
|
* @return Optional с задачей или пустой Optional
|
||||||
|
*/
|
||||||
|
Optional<Task> findById(TaskId id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Находит задачу по UUID контекста LMS
|
||||||
|
* @param contextId UUID контекста
|
||||||
|
* @return Optional с задачей или пустой Optional
|
||||||
|
*/
|
||||||
|
Optional<Task> findByContextId(UUID contextId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохраняет задачу (create или update)
|
||||||
|
* @param task задача для сохранения
|
||||||
|
* @return сохраненная задача с присвоенным ID
|
||||||
|
*/
|
||||||
|
Task save(Task task);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удаляет задачу
|
||||||
|
* @param task задача для удаления
|
||||||
|
*/
|
||||||
|
void delete(Task task);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, существует ли задача с данным ID
|
||||||
|
* @param id идентификатор задачи
|
||||||
|
* @return true если существует
|
||||||
|
*/
|
||||||
|
boolean existsById(TaskId id);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package ru.oa2.lti.domain.valueobject;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value Object для описания задачи
|
||||||
|
*/
|
||||||
|
public record Description(String value) {
|
||||||
|
public Description {
|
||||||
|
Objects.requireNonNull(value, "Description cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Description of(String value) {
|
||||||
|
return new Description(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Description empty() {
|
||||||
|
return new Description("");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return value.isBlank();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package ru.oa2.lti.domain.valueobject;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import ru.oa2.lti.domain.valueobject.ScriptType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value Object для скрипта с типом и содержимым
|
||||||
|
*/
|
||||||
|
public record Script(ScriptType type, String content) {
|
||||||
|
public Script {
|
||||||
|
Objects.requireNonNull(type, "Script type cannot be null");
|
||||||
|
Objects.requireNonNull(content, "Script content cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Script of(ScriptType type, String content) {
|
||||||
|
return new Script(type, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Script empty(ScriptType type) {
|
||||||
|
return new Script(type, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return content.isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExecutable() {
|
||||||
|
return !isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package ru.oa2.lti.domain.valueobject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Типы скриптов для задачи
|
||||||
|
*/
|
||||||
|
public enum ScriptType {
|
||||||
|
INITIALIZATION("init_script"),
|
||||||
|
VERIFICATION("verification_script"),
|
||||||
|
DELETION("delete_script");
|
||||||
|
|
||||||
|
private final String columnName;
|
||||||
|
|
||||||
|
ScriptType(String columnName) {
|
||||||
|
this.columnName = columnName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getColumnName() {
|
||||||
|
return columnName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package ru.oa2.lti.domain.valueobject;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value Object для идентификатора задачи
|
||||||
|
*/
|
||||||
|
public record TaskId(Long value) {
|
||||||
|
public TaskId {
|
||||||
|
Objects.requireNonNull(value, "TaskId cannot be null");
|
||||||
|
if (value <= 0) {
|
||||||
|
throw new IllegalArgumentException("TaskId must be positive");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TaskId of(Long value) {
|
||||||
|
return new TaskId(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package ru.oa2.lti.domain.valueobject;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value Object для названия задачи с валидацией
|
||||||
|
*/
|
||||||
|
public record TaskName(String value) {
|
||||||
|
private static final int MAX_LENGTH = 250;
|
||||||
|
|
||||||
|
public TaskName {
|
||||||
|
Objects.requireNonNull(value, "Task name cannot be null");
|
||||||
|
if (value.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("Task name cannot be empty");
|
||||||
|
}
|
||||||
|
if (value.length() > MAX_LENGTH) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
String.format("Task name cannot exceed %d characters", MAX_LENGTH)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TaskName of(String value) {
|
||||||
|
return new TaskName(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
package ru.oa2.lti.infrastructure.adapter.persistence.jpa;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import ru.oa2.lti.domain.model.Task;
|
||||||
|
import ru.oa2.lti.domain.repository.TaskRepository;
|
||||||
|
import ru.oa2.lti.domain.valueobject.TaskId;
|
||||||
|
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.TaskEntity;
|
||||||
|
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.mapper.TaskEntityMapper;
|
||||||
|
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.repository.TaskJpaRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JPA адаптер для TaskRepository (Hexagonal Architecture - Output Adapter).
|
||||||
|
* Реализует порт из domain слоя, используя инфраструктурные детали (JPA).
|
||||||
|
*
|
||||||
|
* Принципы:
|
||||||
|
* - Находится в infrastructure слое
|
||||||
|
* - Реализует интерфейс из domain слоя
|
||||||
|
* - Использует JPA репозиторий и маппер
|
||||||
|
* - Преобразует между domain и infrastructure моделями
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TaskRepositoryAdapter implements TaskRepository {
|
||||||
|
|
||||||
|
private final TaskJpaRepository jpaRepository;
|
||||||
|
private final TaskEntityMapper mapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Task> findById(TaskId id) {
|
||||||
|
if (id == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return jpaRepository.findById(id.value())
|
||||||
|
.map(mapper::toDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Task> findByContextId(UUID contextId) {
|
||||||
|
if (contextId == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return jpaRepository.findByContextId(contextId)
|
||||||
|
.map(mapper::toDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Task save(Task task) {
|
||||||
|
if (task == null) {
|
||||||
|
throw new IllegalArgumentException("Task cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskEntity entity;
|
||||||
|
|
||||||
|
if (task.getId() != null) {
|
||||||
|
// Update existing
|
||||||
|
entity = jpaRepository.findById(task.getId().value())
|
||||||
|
.orElseGet(() -> mapper.toEntity(task));
|
||||||
|
mapper.updateEntity(entity, task);
|
||||||
|
} else {
|
||||||
|
// Create new
|
||||||
|
entity = mapper.toEntity(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskEntity savedEntity = jpaRepository.save(entity);
|
||||||
|
return mapper.toDomain(savedEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(Task task) {
|
||||||
|
if (task == null || task.getId() == null) {
|
||||||
|
throw new IllegalArgumentException("Task or Task ID cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
jpaRepository.deleteById(task.getId().value());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean existsById(TaskId id) {
|
||||||
|
if (id == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return jpaRepository.existsById(id.value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JPA Entity для истории действий.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Entity
|
||||||
|
@Table(name = "history",
|
||||||
|
indexes = {
|
||||||
|
@Index(name = "idx_history_content_uuid", columnList = "content_uuid"),
|
||||||
|
@Index(name = "idx_history_deployment_id", columnList = "deployment_id"),
|
||||||
|
@Index(name = "idx_history_created", columnList = "created")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
public class HistoryEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "deployment_id")
|
||||||
|
private UUID deploymentId;
|
||||||
|
|
||||||
|
@Column(name = "content_uuid")
|
||||||
|
private UUID contentUuid;
|
||||||
|
|
||||||
|
@Column(name = "created", nullable = false)
|
||||||
|
private LocalDateTime created;
|
||||||
|
|
||||||
|
@Column(name = "action_type", length = 50)
|
||||||
|
private String actionType;
|
||||||
|
|
||||||
|
@Column(name = "message", columnDefinition = "text")
|
||||||
|
private String message;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JPA Entity для контента LMS.
|
||||||
|
* Переименована из LMSContent для ясности, что это infrastructure entity.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Entity
|
||||||
|
@Table(name = "lms_content",
|
||||||
|
indexes = {
|
||||||
|
@Index(name = "idx_content_uuid", columnList = "content_uuid")
|
||||||
|
})
|
||||||
|
public class LMSContentEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "deployment_id", nullable = false)
|
||||||
|
private UUID deploymentId;
|
||||||
|
|
||||||
|
@Column(name = "content_uuid", nullable = false)
|
||||||
|
private UUID contentUuid;
|
||||||
|
|
||||||
|
@Column(name = "created", nullable = false)
|
||||||
|
private LocalDateTime created;
|
||||||
|
|
||||||
|
@Column(name = "task_id", nullable = false)
|
||||||
|
private Long taskId;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JPA Entity для задачи.
|
||||||
|
* Это НЕ доменная модель, а инфраструктурная деталь для персистентности.
|
||||||
|
* Находится в infrastructure слое, НЕ влияет на domain.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Entity
|
||||||
|
@Table(name = "task")
|
||||||
|
public class TaskEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "name", nullable = false, length = 250)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(name = "description", nullable = false, columnDefinition = "text")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(name = "init_script", columnDefinition = "text")
|
||||||
|
private String initScript;
|
||||||
|
|
||||||
|
@Column(name = "verification_script", columnDefinition = "text")
|
||||||
|
private String verificationScript;
|
||||||
|
|
||||||
|
@Column(name = "delete_script", columnDefinition = "text")
|
||||||
|
private String deleteScript;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
package ru.oa2.lti.infrastructure.adapter.persistence.jpa.mapper;
|
||||||
|
|
||||||
|
import org.mapstruct.*;
|
||||||
|
import ru.oa2.lti.domain.model.Task;
|
||||||
|
import ru.oa2.lti.domain.valueobject.*;
|
||||||
|
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.TaskEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MapStruct маппер между доменной моделью Task и JPA entity TaskEntity.
|
||||||
|
* Отвечает за преобразование между domain и infrastructure слоями.
|
||||||
|
*
|
||||||
|
* Принципы:
|
||||||
|
* - Domain модель НЕ знает о Entity
|
||||||
|
* - Entity НЕ знает о Domain модели
|
||||||
|
* - Mapper знает об обоих и выполняет преобразование
|
||||||
|
*/
|
||||||
|
@Mapper(
|
||||||
|
componentModel = MappingConstants.ComponentModel.SPRING,
|
||||||
|
injectionStrategy = InjectionStrategy.CONSTRUCTOR
|
||||||
|
)
|
||||||
|
public abstract class TaskEntityMapper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Преобразует JPA entity в доменную модель
|
||||||
|
*/
|
||||||
|
public Task toDomain(TaskEntity entity) {
|
||||||
|
if (entity == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task task = Task.restore(
|
||||||
|
TaskId.of(entity.getId()),
|
||||||
|
TaskName.of(entity.getName()),
|
||||||
|
Description.of(entity.getDescription())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Добавляем скрипты
|
||||||
|
if (entity.getInitScript() != null && !entity.getInitScript().isBlank()) {
|
||||||
|
task.setScript(Script.of(ScriptType.INITIALIZATION, entity.getInitScript()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.getVerificationScript() != null && !entity.getVerificationScript().isBlank()) {
|
||||||
|
task.setScript(Script.of(ScriptType.VERIFICATION, entity.getVerificationScript()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.getDeleteScript() != null && !entity.getDeleteScript().isBlank()) {
|
||||||
|
task.setScript(Script.of(ScriptType.DELETION, entity.getDeleteScript()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Преобразует доменную модель в JPA entity
|
||||||
|
*/
|
||||||
|
public TaskEntity toEntity(Task domain) {
|
||||||
|
if (domain == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskEntity entity = new TaskEntity();
|
||||||
|
|
||||||
|
if (domain.getId() != null) {
|
||||||
|
entity.setId(domain.getId().value());
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.setName(domain.getName().value());
|
||||||
|
entity.setDescription(domain.getDescription().value());
|
||||||
|
entity.setInitScript(domain.getInitScript().content());
|
||||||
|
entity.setVerificationScript(domain.getVerificationScript().content());
|
||||||
|
entity.setDeleteScript(domain.getDeleteScript().content());
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновляет существующую entity данными из domain модели
|
||||||
|
*/
|
||||||
|
public void updateEntity(TaskEntity entity, Task domain) {
|
||||||
|
if (entity == null || domain == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.setName(domain.getName().value());
|
||||||
|
entity.setDescription(domain.getDescription().value());
|
||||||
|
entity.setInitScript(domain.getInitScript().content());
|
||||||
|
entity.setVerificationScript(domain.getVerificationScript().content());
|
||||||
|
entity.setDeleteScript(domain.getDeleteScript().content());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package ru.oa2.lti.infrastructure.adapter.persistence.jpa.repository;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.HistoryEntity;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Data JPA репозиторий для HistoryEntity
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface HistoryJpaRepository extends JpaRepository<HistoryEntity, Long> {
|
||||||
|
|
||||||
|
List<HistoryEntity> findByContentUuidOrderByCreatedDesc(UUID contentUuid);
|
||||||
|
|
||||||
|
List<HistoryEntity> findByDeploymentIdOrderByCreatedDesc(UUID deploymentId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package ru.oa2.lti.infrastructure.adapter.persistence.jpa.repository;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.LMSContentEntity;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Data JPA репозиторий для LMSContentEntity
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface LMSContentJpaRepository extends JpaRepository<LMSContentEntity, Long> {
|
||||||
|
|
||||||
|
Optional<LMSContentEntity> findByContentUuid(UUID contentUuid);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package ru.oa2.lti.infrastructure.adapter.persistence.jpa.repository;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.TaskEntity;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Data JPA репозиторий для TaskEntity.
|
||||||
|
* Это инфраструктурная деталь, работает с JPA entities.
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface TaskJpaRepository extends JpaRepository<TaskEntity, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Находит задачу по UUID контекста через связь с LMSContent.
|
||||||
|
* Выбирает задачу из самой последней записи LMSContent (по дате created).
|
||||||
|
*/
|
||||||
|
@Query("""
|
||||||
|
SELECT t FROM TaskEntity t
|
||||||
|
JOIN LMSContentEntity lms ON lms.taskId = t.id
|
||||||
|
WHERE lms.contentUuid = :contextId
|
||||||
|
ORDER BY lms.created DESC
|
||||||
|
LIMIT 1
|
||||||
|
""")
|
||||||
|
Optional<TaskEntity> findByContextId(@Param("contextId") UUID contextId);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue