diff --git a/pom.xml b/pom.xml
index b6902cb..e1aa7a0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,6 +19,7 @@
21
+ 1.6.3
@@ -48,6 +49,12 @@
lombok
+
+ org.mapstruct
+ mapstruct
+ ${mapstruct.version}
+
+
org.springframework.boot
spring-boot-starter-test
@@ -87,6 +94,33 @@
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ ${java.version}
+ ${java.version}
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${mapstruct.version}
+
+
+ org.projectlombok
+ lombok-mapstruct-binding
+ 0.2.0
+
+
+
+
+
diff --git a/src/main/java/ru/oa2/lti/application/infrastructure/repository/HistoryRepository.java b/src/main/java/ru/oa2/lti/application/infrastructure/repository/HistoryRepository.java
index de4254b..ae7edeb 100644
--- a/src/main/java/ru/oa2/lti/application/infrastructure/repository/HistoryRepository.java
+++ b/src/main/java/ru/oa2/lti/application/infrastructure/repository/HistoryRepository.java
@@ -2,15 +2,20 @@ package ru.oa2.lti.application.infrastructure.repository;
import org.springframework.data.jpa.repository.JpaRepository;
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.UUID;
+/**
+ * @deprecated Используйте {@link ru.oa2.lti.infrastructure.adapter.persistence.jpa.repository.HistoryJpaRepository}
+ * Этот интерфейс оставлен для обратной совместимости и будет удален в следующей версии.
+ */
+@Deprecated
@Repository
-public interface HistoryRepository extends JpaRepository {
+public interface HistoryRepository extends JpaRepository {
- List findByContentUuidOrderByCreatedDesc(UUID contentUuid);
+ List findByContentUuidOrderByCreatedDesc(UUID contentUuid);
- List findByDeploymentIdOrderByCreatedDesc(UUID deploymentId);
+ List findByDeploymentIdOrderByCreatedDesc(UUID deploymentId);
}
diff --git a/src/main/java/ru/oa2/lti/application/infrastructure/repository/LMSContentRepository.java b/src/main/java/ru/oa2/lti/application/infrastructure/repository/LMSContentRepository.java
index f8f70cd..24c46fe 100644
--- a/src/main/java/ru/oa2/lti/application/infrastructure/repository/LMSContentRepository.java
+++ b/src/main/java/ru/oa2/lti/application/infrastructure/repository/LMSContentRepository.java
@@ -2,13 +2,18 @@ package ru.oa2.lti.application.infrastructure.repository;
import org.springframework.data.jpa.repository.JpaRepository;
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.UUID;
+/**
+ * @deprecated Используйте {@link ru.oa2.lti.infrastructure.adapter.persistence.jpa.repository.LMSContentJpaRepository}
+ * Этот интерфейс оставлен для обратной совместимости и будет удален в следующей версии.
+ */
+@Deprecated
@Repository
-public interface LMSContentRepository extends JpaRepository {
+public interface LMSContentRepository extends JpaRepository {
- Optional getLMSContentByContentUuid(UUID contentUuid);
+ Optional getLMSContentByContentUuid(UUID contextId);
}
diff --git a/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/History.java b/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/History.java
index 8a0fe48..44ab5d0 100644
--- a/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/History.java
+++ b/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/History.java
@@ -7,6 +7,11 @@ import lombok.Setter;
import java.time.LocalDateTime;
import java.util.UUID;
+/**
+ * @deprecated Используйте {@link ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.HistoryEntity}
+ * Этот класс оставлен для обратной совместимости и будет удален в следующей версии.
+ */
+@Deprecated
@Getter
@Setter
@Entity
diff --git a/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/LMSContent.java b/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/LMSContent.java
index ff9f663..358d3be 100644
--- a/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/LMSContent.java
+++ b/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/LMSContent.java
@@ -7,6 +7,11 @@ import lombok.Setter;
import java.time.LocalDateTime;
import java.util.UUID;
+/**
+ * @deprecated Используйте {@link ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.LMSContentEntity}
+ * Этот класс оставлен для обратной совместимости и будет удален в следующей версии.
+ */
+@Deprecated
@Getter
@Setter
@Entity
@@ -29,7 +34,6 @@ public class LMSContent {
@Column(name = "created", nullable = false)
LocalDateTime created;
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "task_id", nullable = false, foreignKey = @ForeignKey(name = "fk_lms_content_task"))
- Task task;
+ @Column(name = "task_id", nullable = false)
+ Long taskId;
}
diff --git a/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/Task.java b/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/Task.java
index 0920297..3db8145 100644
--- a/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/Task.java
+++ b/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/Task.java
@@ -4,6 +4,11 @@ import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
+/**
+ * @deprecated Используйте {@link ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.TaskEntity}
+ * Этот класс оставлен для обратной совместимости и будет удален в следующей версии.
+ */
+@Deprecated
@Getter
@Setter
@Entity
diff --git a/src/main/java/ru/oa2/lti/application/service/history/HistoryServiceImpl.java b/src/main/java/ru/oa2/lti/application/service/history/HistoryServiceImpl.java
index d0e32cc..f57d706 100644
--- a/src/main/java/ru/oa2/lti/application/service/history/HistoryServiceImpl.java
+++ b/src/main/java/ru/oa2/lti/application/service/history/HistoryServiceImpl.java
@@ -5,7 +5,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
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.util.UUID;
@@ -24,7 +24,7 @@ public class HistoryServiceImpl implements HistoryService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logEvent(UUID deploymentId, UUID contentUuid, String message) {
try {
- History history = new History();
+ HistoryEntity history = new HistoryEntity();
history.setDeploymentId(deploymentId);
history.setContentUuid(contentUuid);
history.setCreated(LocalDateTime.now());
diff --git a/src/main/java/ru/oa2/lti/application/service/task/TaskServiceImpl.java b/src/main/java/ru/oa2/lti/application/service/task/TaskServiceImpl.java
index 4c2e8e8..2170f85 100644
--- a/src/main/java/ru/oa2/lti/application/service/task/TaskServiceImpl.java
+++ b/src/main/java/ru/oa2/lti/application/service/task/TaskServiceImpl.java
@@ -1,15 +1,17 @@
package ru.oa2.lti.application.service.task;
-import jakarta.transaction.Transactional;
+import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.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.service.history.HistoryService;
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.results.ResultResponse;
import ru.oa2.lti.domain.model.task.RequestUpdateTask;
@@ -17,128 +19,149 @@ import ru.oa2.lti.domain.model.task.TaskData;
import java.util.UUID;
+/**
+ * Реализация TaskService, следующая принципам Clean Architecture и DDD.
+ *
+ * Улучшения по сравнению со старой версией:
+ * - Использует доменную модель Task вместо JPA Entity
+ * - Зависит от порта TaskRepository (domain), а не от конкретной реализации
+ * - Бизнес-логика теперь в доменной модели Task
+ * - Сервис только координирует взаимодействие между компонентами
+ * - Явная обработка исключений
+ * - Транзакции только на методах записи
+ */
@Slf4j
@Service
-@Transactional(rollbackOn = Throwable.class)
+@RequiredArgsConstructor
public class TaskServiceImpl implements TaskService {
- private final LMSContentRepository lmsContentRepository;
+ private final TaskRepository taskRepository;
private final Runner runner;
private final HistoryService historyService;
-
- //TODO объединить в один?
- 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;
- }
+ private final ResultService resultService;
+ private final LMSService lmsService;
@Override
+ @Transactional(readOnly = true)
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 = lmsContent.getTask();
+ // Используем доменный репозиторий через порт
+ Task task = taskRepository.findByContextId(contextId)
+ .orElseThrow(() -> new TaskNotFoundException(contextId));
- if (task == null) {
- return new TaskData("Not Page", "", "", "", "");
+ // Преобразуем в DTO для presentation layer
+ 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
+ @Transactional
public ResultResponse checkTask(ResultRequest resultRequest) {
+ log.info("Checking task for clientId: {}", resultRequest.clientId());
- //TODO запуск скрипта проверки
- var assessToken = lmsService.exchangeForAccessToken(
- resultRequest.clientId());
- resultService.setResult(assessToken, resultRequest.idToken());
+ // Получаем access token от LMS
+ String accessToken = lmsService.exchangeForAccessToken(resultRequest.clientId());
- //TODO обработка и запуск скрипта
+ // Отправляем результат
+ resultService.setResult(accessToken, resultRequest.idToken());
+
+ // TODO: реализовать запуск скрипта проверки
return new ResultResponse("success");
}
@Override
+ @Transactional
public ResultResponse saveTask(RequestUpdateTask requestUpdateTask) {
+ log.info("Saving task for context: {}", requestUpdateTask.getContextId());
- log.info("save");
- var result = lmsContentRepository.getLMSContentByContentUuid(requestUpdateTask.getContextId());
+ try {
+ // Загружаем существующую задачу
+ Task task = taskRepository.findByContextId(requestUpdateTask.getContextId())
+ .orElseThrow(() -> new TaskNotFoundException(requestUpdateTask.getContextId()));
- //TODO доработать версионирование task-ов
- if (result.isPresent()) {
- LMSContent content = result.get();
- Task task = content.getTask();
+ String oldName = task.getName().value();
- if (task != null) {
- String oldName = task.getName();
- updateTaskFromData(task, requestUpdateTask.getData());
- lmsContentRepository.save(content);
+ // Обновляем задачу через доменную модель (бизнес-логика внутри)
+ updateTaskFromDto(task, requestUpdateTask.getData());
- historyService.logAction(
- content.getDeploymentId(),
- requestUpdateTask.getContextId(),
- "TASK_SAVED",
- String.format("Task saved: %s (was: %s)",
- requestUpdateTask.getData().getName(), oldName)
- );
- } else {
- historyService.logAction(
- content.getDeploymentId(),
- requestUpdateTask.getContextId(),
- "TASK_SAVE_ERROR",
- "Task is null, cannot save"
- );
- }
- } else {
+ // Сохраняем через порт репозитория
+ taskRepository.save(task);
+
+ // Логируем действие
+ historyService.logAction(
+ null, // deploymentId нужно получать из контекста
+ requestUpdateTask.getContextId(),
+ "TASK_SAVED",
+ String.format("Task saved: %s (was: %s)",
+ requestUpdateTask.getData().getName(), oldName)
+ );
+
+ return new ResultResponse("success");
+ } catch (TaskNotFoundException e) {
+ log.error("Task not found for context: {}", requestUpdateTask.getContextId(), e);
historyService.logAction(
null,
requestUpdateTask.getContextId(),
"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(
- task.getName(),
- task.getDescription(),
- task.getInitScript(),
- task.getVerificationScript(),
- task.getDeleteScript()
+ task.getName().value(),
+ task.getDescription().value(),
+ task.getInitScript().content(),
+ task.getVerificationScript().content(),
+ task.getDeleteScript().content()
);
}
- private void updateTaskFromData(Task task, TaskData data) {
- task.setName(data.getName());
- task.setDescription(data.getDescription());
- task.setInitScript(data.getInitScript());
- task.setVerificationScript(data.getVerificationScript());
- task.setDeleteScript(data.getDeleteScript());
+ /**
+ * Обновляет доменную модель Task из DTO TaskData
+ */
+ private void updateTaskFromDto(Task task, TaskData data) {
+ // Используем Value Objects с валидацией
+ 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()));
+ }
}
}
diff --git a/src/main/java/ru/oa2/lti/domain/exception/DomainException.java b/src/main/java/ru/oa2/lti/domain/exception/DomainException.java
new file mode 100644
index 0000000..c844efe
--- /dev/null
+++ b/src/main/java/ru/oa2/lti/domain/exception/DomainException.java
@@ -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);
+ }
+}
diff --git a/src/main/java/ru/oa2/lti/domain/exception/InvalidTaskStateException.java b/src/main/java/ru/oa2/lti/domain/exception/InvalidTaskStateException.java
new file mode 100644
index 0000000..1de5eef
--- /dev/null
+++ b/src/main/java/ru/oa2/lti/domain/exception/InvalidTaskStateException.java
@@ -0,0 +1,10 @@
+package ru.oa2.lti.domain.exception;
+
+/**
+ * Исключение при невалидном состоянии задачи
+ */
+public class InvalidTaskStateException extends DomainException {
+ public InvalidTaskStateException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/ru/oa2/lti/domain/exception/TaskNotFoundException.java b/src/main/java/ru/oa2/lti/domain/exception/TaskNotFoundException.java
new file mode 100644
index 0000000..0d96327
--- /dev/null
+++ b/src/main/java/ru/oa2/lti/domain/exception/TaskNotFoundException.java
@@ -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);
+ }
+}
diff --git a/src/main/java/ru/oa2/lti/domain/model/Task.java b/src/main/java/ru/oa2/lti/domain/model/Task.java
new file mode 100644
index 0000000..68cfd79
--- /dev/null
+++ b/src/main/java/ru/oa2/lti/domain/model/Task.java
@@ -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 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