управление данными и жизненным циклом
Создаём полноценную CRM-модель на JMatrixPlatform: атрибуты, тип, жизненный цикл. Всё — на Java, с готовыми примерами.
JAttribute. Для "вид
договора" используем getRanges().
@JModelPart
public final class ATRContractDate extends JAttribute {
public static final ATRContractDate ATTRIBUTE = new ATRContractDate();
@Override
public JDataType getDataType() {
return new JDataType.JString();
}
@Override
public String getDescription() {
return "Дата заключения договора";
}
}
@JModelPart
public final class ATRAmount extends JAttribute {
public static final ATRAmount ATTRIBUTE = new ATRAmount();
@Override
public JDataType getDataType() {
return new JDataType.JReal();
}
@Override
public String getDescription() {
return "Сумма договора";
}
}
@JModelPart
public final class ATRDeadline extends JAttribute {
public static final ATRDeadline ATTRIBUTE = new ATRDeadline();
@Override
public JDataType getDataType() {
return new JDataType.JTimestamp();
}
@Override
public String getDescription() {
return "Срок исполнения обязательств";
}
}
@JModelPart
public final class ATRContractType extends JAttribute {
public static final ATRContractType ATTRIBUTE = new ATRContractType();
@Override
public JDataType getDataType() {
return new JDataType.JString();
}
@Override
public List<Range> getRanges() {
return List.of(
new Range(JQ.EQ, new JString("Купля-продажа")),
new Range(JQ.EQ, new JString("Услуги")),
new Range(JQ.EQ, new JString("Подряд"))
);
}
@Override
public String getDescription() {
return "Вид договора";
}
}
code (номер договора),
title (предмет договора) и description, уже присутствуют у каждого
объекта. Их не нужно создавать отдельно.
Справочно: также у каждого объекта базово доступны свойства release (version), shortCode, fullCode,
fullTitle, originated, modified и json.
@JModelPart
public final class ATPContract extends JType {
public static final ATPContract TYPE = new ATPContract();
@Override
public Set<JAttribute> getAttributes() {
return Set.of(
ATRContractDate.ATTRIBUTE,
ATRAmount.ATTRIBUTE,
ATRDeadline.ATTRIBUTE,
ATRContractType.ATTRIBUTE
);
}
@Override
public String getDescription() {
return "Тип данных «Договор» для управления договорной базой";
}
@Override
public void modifyCheck(JContext ctx, UUID oid, JTriggerChanges changes) {
// любая Java-логика: проверки, валидации и т.д.
}
//modifyAction, deleteCheck, deleteAction и т.д.
}
@JModelPart
public final class ALCContractLifecycle extends JPolicy {
public static final ALCContractLifecycle POLICY = new ALCContractLifecycle();
@Override
public Set<JType> getTypes() {
return Set.of(ATPContract.TYPE);
}
@Override
public List<JState> getStates() {
return List.of(
Draft.STATE,
Signing.STATE,
Signed.STATE
);
}
@Override
public Set<JFileType> getFileTypes() {
return Set.of(
PDF.FILE_TYPE,//pdf скан
JGeneric.FILE_TYPE//любой редактируемый формат, например docx
);
}
// Статус 1: Черновик
@JModelPart
public static final class Draft extends JState {
public static final Draft STATE = new Draft();
@Override
public Set<RoleAccess> getAccess() {
return Set.of(
new RoleAccess(ARLContractManager.ROLE, AccessType.ALL),
new RoleAccess(JOwner.ROLE, AccessType.ALL)
);
}
}
// Статус 2: На подписи
@JModelPart
public static final class Signing extends JState {
public static final Signing STATE = new Signing();
@Override
public Set<RoleAccess> getAccess() {
return Set.of(
new RoleAccess(ARLContractManager.ROLE, AccessType.READ, AccessType.PROMOTE),
new RoleAccess(JOwner.ROLE, AccessType.READ),
new RoleAccess(ARLAccountant.ROLE, AccessType.READ)
);
}
}
// Статус 3: Подписан
@JModelPart
public static final class Signed extends JState {
public static final Signed STATE = new Signed();
@Override
public Set<RoleAccess> getAccess() {
return Set.of(
new RoleAccess(ARLContractManager.ROLE, AccessType.READ, AccessType.ARCHIVE),
new RoleAccess(JPublic.ROLE, AccessType.READ),
new RoleAccess(ARLAccountant.ROLE, AccessType.READ)
);
}
}
}
ARLContractManager (менеджер по договорам),
ARLAccountant (бухгалтер), JOwner (создатель договора),
JPublic (все остальные).
code.
POST /objects/
{
"type": "ATPContract",
"policy": "ALCContractLifecycle",
"code": "Д-2026-001",
"title": "Лицензионный договор на ПО",
"attributeValues": {
"ATRContractDate": {
"value": "2026-04-01T00:00:00Z"
},
"ATRAmount": {
"value": 1250000.00
},
"ATRDeadline": {
"value": "2026-12-31T00:00:00Z"
},
"ATRContractType": {
"value": "Услуги"
}
}
}
| Номер (code) | Предмет (title) | Дата заключения | Сумма | Вид | Статус |
|---|---|---|---|---|---|
| Д-2026-001 | Лицензионный договор на ПО | 01.04.2026 | 1 250 000 | Услуги | Черновик |
| Д-2026-002 | Поставка серверного оборудования | 15.03.2026 | 4 800 000 | Купля-продажа | На подписи |
| Д-2025-189 | Консалтинг | 20.12.2025 | 960 000 | Услуги | Подписан |
@Entity
@Table(name = "contract")
public class ContractEntity {
@Id
@GeneratedValue(generator = "UUID")
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
private UUID id;
@Column(nullable = false, unique = true)
private String code; // номер договора
@Column(nullable = false)
private String title; // предмет договора
@Column(nullable = false)
private LocalDate contractDate;
@Column(nullable = false, precision = 15, scale = 2)
private BigDecimal amount;
@Column(nullable = false)
private LocalDateTime deadline;
@Column(nullable = false)
private String contractType; // Купля-продажа / Услуги / Подряд
@Column(nullable = false)
private String status; // DRAFT, SIGNING, SIGNED
// Аудит
@Column(nullable = false)
private UUID createdBy;
@Column(nullable = false)
private Instant createdAt;
private UUID modifiedBy;
private Instant modifiedAt;
// геттеры, сеттеры, конструкторы...
}
@Repository
public interface ContractRepository extends JpaRepository<ContractEntity, UUID> {
// Базовые методы поиска
Optional<ContractEntity> findByCode(String code);
List<ContractEntity> findByStatus(String status);
Page<ContractEntity> findByStatusIn(List<String> statuses, Pageable pageable);
// Поиск по датам
List<ContractEntity> findByContractDateBetween(LocalDate start, LocalDate end);
List<ContractEntity> findByDeadlineBefore(LocalDateTime date);
List<ContractEntity> findByDeadlineAfter(LocalDateTime date);
// Поиск по сумме
List<ContractEntity> findByAmountGreaterThanEqual(BigDecimal amount);
List<ContractEntity> findByAmountBetween(BigDecimal min, BigDecimal max);
// Поиск по типу договора
List<ContractEntity> findByContractType(String contractType);
Page<ContractEntity> findByContractTypeAndStatus(String contractType, String status, Pageable pageable);
// Комбинированные поиски
List<ContractEntity> findByCodeContainingAndStatus(String codePart, String status);
Page<ContractEntity> findByTitleContainingIgnoreCase(String titlePart, Pageable pageable);
// Поиск с несколькими условиями
@Query("SELECT c FROM ContractEntity c WHERE " +
"(:code IS NULL OR c.code LIKE %:code%) AND " +
"(:contractType IS NULL OR c.contractType = :contractType) AND " +
"(:status IS NULL OR c.status = :status) AND " +
"(c.amount >= :minAmount) AND " +
"(c.deadline >= :deadlineFrom)")
Page<ContractEntity> searchContracts(@Param("code") String code,
@Param("contractType") String contractType,
@Param("status") String status,
@Param("minAmount") BigDecimal minAmount,
@Param("deadlineFrom") LocalDateTime deadlineFrom,
Pageable pageable);
// Статистика
long countByStatus(String status);
@Query("SELECT c.status, COUNT(c) FROM ContractEntity c GROUP BY c.status")
List<Object[]> countByStatusGroup();
// Обновление статуса (массовое)
@Modifying
@Query("UPDATE ContractEntity c SET c.status = :newStatus WHERE c.id IN :ids AND c.status = :oldStatus")
int bulkUpdateStatus(@Param("ids") List<UUID> ids,
@Param("oldStatus") String oldStatus,
@Param("newStatus") String newStatus);
}
@Service
@Transactional
public class ContractService {
private final ContractRepository repository;
private final AuthorizationService authService;
private final AuditService auditService;
private final FileStorageService fileStorageService;
public ContractEntity create(ContractCreateDto dto, UUID userId) {
// Проверка прав
if (!authService.canCreateContract(userId)) {
throw new AccessDeniedException("Нет прав на создание договора");
}
ContractEntity entity = new ContractEntity();
entity.setCode(dto.getCode());
entity.setTitle(dto.getTitle());
entity.setContractDate(dto.getContractDate());
entity.setAmount(dto.getAmount());
entity.setDeadline(dto.getDeadline());
entity.setContractType(dto.getContractType());
entity.setStatus("DRAFT");
entity.setCreatedBy(userId);
entity.setCreatedAt(Instant.now());
ContractEntity saved = repository.save(entity);
auditService.record("CREATE", saved.getId(), userId, null);
return saved;
}
public Page<ContractDto> searchContracts(String code, String contractType,
String status, BigDecimal minAmount,
LocalDateTime deadlineFrom, UUID userId,
Pageable pageable) {
List<String> visibleStatuses = authService.getReadableStatuses(userId);
// Логика поиска с учётом видимых статусов
return repository.searchContracts(code, contractType, status, minAmount, deadlineFrom, pageable)
.map(this::toDto);
}
public ContractEntity promoteStatus(UUID contractId, UUID userId) {
ContractEntity contract = repository.findById(contractId)
.orElseThrow(() -> new NotFoundException("Договор не найден"));
String oldStatus = contract.getStatus();
// Сложная логика проверки прав в зависимости от статуса
if ("DRAFT".equals(contract.getStatus())) {
if (!authService.canPromoteFromDraft(userId, contract)) {
throw new AccessDeniedException("Нельзя перевести на подпись");
}
contract.setStatus("SIGNING");
} else if ("SIGNING".equals(contract.getStatus())) {
if (!authService.canPromoteFromSigning(userId, contract)) {
throw new AccessDeniedException("Нельзя подписать договор");
}
contract.setStatus("SIGNED");
} else {
throw new IllegalStateException("Недопустимый переход статуса");
}
contract.setModifiedBy(userId);
contract.setModifiedAt(Instant.now());
auditService.record("PROMOTE", contractId, userId,
"Статус изменён с " + oldStatus + " на " + contract.getStatus());
return repository.save(contract);
}
public Page<ContractDto> listVisibleContracts(UUID userId, Pageable pageable) {
// Ручное вычисление видимых статусов на основе ролей пользователя
List<String> visibleStatuses = authService.getReadableStatuses(userId);
return repository.findByStatusIn(visibleStatuses, pageable)
.map(this::toDto);
}
// Методы: update, delete, demote, getHistory, attachFile, getFiles...
}
@RestController
@RequestMapping("/api/contracts")
public class ContractController {
@Autowired
private ContractService contractService;
@PostMapping
public ResponseEntity<ContractDto> create(@Valid @RequestBody ContractCreateDto dto,
@RequestAttribute UUID userId) {
return ResponseEntity.ok(contractService.create(dto, userId));
}
@PostMapping("/{id}/promote")
public ResponseEntity<ContractDto> promote(@PathVariable UUID id,
@RequestAttribute UUID userId) {
return ResponseEntity.ok(contractService.promoteStatus(id, userId));
}
@PostMapping("/{id}/demote")
public ResponseEntity<ContractDto> demote(@PathVariable UUID id,
@RequestAttribute UUID userId) {
return ResponseEntity.ok(contractService.demoteStatus(id, userId));
}
@GetMapping
public ResponseEntity<Page<ContractDto>> list(@RequestAttribute UUID userId,
Pageable pageable) {
return ResponseEntity.ok(contractService.listVisibleContracts(userId, pageable));
}
@GetMapping("/search")
public ResponseEntity<Page<ContractDto>> search(@RequestParam(required = false) String code,
@RequestParam(required = false) String contractType,
@RequestParam(required = false) String status,
@RequestParam(required = false) BigDecimal minAmount,
@RequestParam(required = false) LocalDateTime deadlineFrom,
@RequestAttribute UUID userId,
Pageable pageable) {
return ResponseEntity.ok(contractService.searchContracts(code, contractType,
status, minAmount, deadlineFrom, userId, pageable));
}
@PostMapping("/{id}/files")
public ResponseEntity<Void> uploadFile(@PathVariable UUID id,
@RequestParam MultipartFile file,
@RequestAttribute UUID userId) {
contractService.attachFile(id, file, userId);
return ResponseEntity.ok().build();
}
@GetMapping("/{id}/history")
public ResponseEntity<List<AuditRecord>> getHistory(@PathVariable UUID id) {
return ResponseEntity.ok(auditService.getHistory(id));
}
// И ещё десяток endpoint'ов: update, delete, getById, downloadFile, release version...
}
public class ContractCreateDto {
@NotBlank private String code;
@NotBlank private String title;
@NotNull private LocalDate contractDate;
@NotNull @Positive private BigDecimal amount;
@NotNull private LocalDateTime deadline;
@NotBlank private String contractType;
// геттеры, сеттеры
}
public class ContractDto {
private UUID id;
private String code;
private String title;
private LocalDate contractDate;
private BigDecimal amount;
private LocalDateTime deadline;
private String contractType;
private String status;
private UUID createdBy;
private Instant createdAt;
private UUID modifiedBy;
private Instant modifiedAt;
// геттеры, сеттеры
}
// Маппер (MapStruct)
@Mapper(componentModel = "spring")
public interface ContractMapper {
ContractEntity toEntity(ContractCreateDto dto);
ContractDto toDto(ContractEntity entity);
List<ContractDto> toDtoList(List<ContractEntity> entities);
}