11. Спроектировать и реализовать REST API для управления договорами
Описание
Спроектировать REST Controller, который:
- создаёт новый договор
- возвращает договор по номеру договора
В описании задачи предоставить модель Contract.
Модель Contract:
public class Contract {
private Long id;
private String number; // номер договора (уникальный)
private String clientId; // идентификатор клиента
private String status; // например: NEW, ACTIVE, CLOSED
private java.time.Instant createdAt;
// getters/setters/constructors
}
Спойлеры к решению
Подсказки
💡 Для создания договора обычно используют
💡 Получение договора по номеру удобно сделать как
💡 Лучше принимать на вход
💡 Номер договора стоит валидировать (
💡 Если договор не найден — возвращать
POST /api/contracts.💡 Получение договора по номеру удобно сделать как
GET /api/contracts/{number}.💡 Лучше принимать на вход
ContractCreateDto, а наружу отдавать ContractResponseDto.💡 Номер договора стоит валидировать (
@NotBlank) и сделать уникальным (если есть БД — unique constraint).💡 Если договор не найден — возвращать
404 Not Found.Решение
DTO:
import jakarta.validation.constraints.NotBlank;
public class ContractCreateDto {
@NotBlank
private String number;
@NotBlank
private String clientId;
// getters/setters
}
import java.time.Instant;
public class ContractResponseDto {
private Long id;
private String number;
private String clientId;
private String status;
private Instant createdAt;
// getters/setters/constructors
}
Service (in-memory пример):
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class ContractService {
private final AtomicLong idSeq = new AtomicLong(0);
private final Map<String, Contract> byNumber = new ConcurrentHashMap<>();
public Contract create(ContractCreateDto dto) {
if (byNumber.containsKey(dto.getNumber())) {
throw new IllegalArgumentException("Contract number already exists: " + dto.getNumber());
}
Contract contract = new Contract();
contract.setId(idSeq.incrementAndGet());
contract.setNumber(dto.getNumber());
contract.setClientId(dto.getClientId());
contract.setStatus("NEW");
contract.setCreatedAt(Instant.now());
byNumber.put(contract.getNumber(), contract);
return contract;
}
public Contract getByNumber(String number) {
Contract c = byNumber.get(number);
if (c == null) {
throw new NoSuchElementException("Contract not found: " + number);
}
return c;
}
}
Controller:
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.NoSuchElementException;
@RestController
@RequestMapping("/api/contracts")
public class ContractController {
private final ContractService service;
public ContractController(ContractService service) {
this.service = service;
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ContractResponseDto create(@Valid @RequestBody ContractCreateDto dto) {
Contract c = service.create(dto);
return toResponse(c);
}
@GetMapping("/{number}")
public ContractResponseDto getByNumber(@PathVariable String number) {
Contract c = service.getByNumber(number);
return toResponse(c);
}
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(NoSuchElementException.class)
public String notFound(NoSuchElementException e) {
return e.getMessage();
}
@ResponseStatus(HttpStatus.CONFLICT)
@ExceptionHandler(IllegalArgumentException.class)
public String conflict(IllegalArgumentException e) {
return e.getMessage();
}
private static ContractResponseDto toResponse(Contract c) {
ContractResponseDto dto = new ContractResponseDto();
dto.setId(c.getId());
dto.setNumber(c.getNumber());
dto.setClientId(c.getClientId());
dto.setStatus(c.getStatus());
dto.setCreatedAt(c.getCreatedAt());
return dto;
}
}
.http ручные тесты:
contracts-create.http
POST http://localhost:8080/api/contracts
Content-Type: application/json
{
"number": "CNTR-2026-0001",
"clientId": "client-123"
}
contracts-get.http
GET http://localhost:8080/api/contracts/CNTR-2026-0001
Ключевые моменты:
POST /api/contractsсоздаёт договор и возвращает201 Created.GET /api/contracts/{number}возвращает договор по номеру, иначе404.DTO отделяют вход/выход от внутренней модели.
Проверка уникальности номера даёт
409 Conflict.