feat: Maven multimodule scaffolding for Ordinis MDM service

- 10 модулей (domain, validation, events-api, outbox, rest-api, app, read-api, projection-writer, cuod-bundle, migrations)
- Parent pom + BOM с Spring Boot 3.4.2, Spring Cloud 2024.0.0, Vault Config 4.2.0
- ordinis-app: Spring Boot main с Actuator/Prometheus/Logstash JSON encoder/OpenTelemetry/Cloud Config/Vault skeleton
- ordinis-migrations: Liquibase changelogs (dictionary_definitions/records, audit_log, outbox_events, idempotency_keys, EXCLUSION CONSTRAINT через btree_gist)
- Multi-stage Dockerfiles для app и migrations
This commit is contained in:
Zimin A.N.
2026-05-03 00:41:53 +03:00
commit a28fd0b352
26 changed files with 1130 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
target/
*.class
*.jar
*.war
*.iml
.idea/
.vscode/
.DS_Store
*.bak
# Maven
dependency-reduced-pom.xml
# Spring Boot
*.tmp
HELP.md
# Logs
*.log
# Sensitive
*.pem
*.key
+56
View File
@@ -0,0 +1,56 @@
# Ordinis
MDM сервис для НСИ ЦУОД ОДХ (Оперативное и Долговременное Хранение). Часть линейки **Terravault**. Ордо/порядок (от лат. *ordo*) — упорядоченные классификаторы и справочные данные.
ЦУОД — anchor-клиент v1, второй клиент = свой config bundle, без code change.
## Maven multimodule
```
ordinis/
├── ordinis-domain JPA entities, JSONB types, bitemporal model
├── ordinis-validation JSON Schema validator (networknt)
├── ordinis-events-api DTO для Kafka (semver shared module)
├── ordinis-outbox Outbox pattern + @Scheduled poller
├── ordinis-rest-api Admin write API (CRUD, bundle import)
├── ordinis-app Spring Boot main: writer side
├── ordinis-read-api Read service (PG replica + scope filtering)
├── ordinis-projection-writer Опц.: Kafka → Redis projection (feature-flagged)
├── ordinis-cuod-bundle ЦУОД config: JSON Schema, seed, Grafana dashboards
└── ordinis-migrations Liquibase changelogs → Helm pre-upgrade Job (1-shot)
```
## Стек
- Java 21, Spring Boot 3.4
- PostgreSQL 18 + PostGIS 3.6 + btree_gist (EXCLUSION CONSTRAINT)
- Apache Kafka 4.2 (KRaft, SASL SCRAM-SHA-512)
- Hibernate Envers (tx-time) + valid_from/valid_to (effective-time) — bitemporal
- Liquibase
- Spring Cloud Config + Spring Cloud Vault (Kubernetes auth)
- Micrometer + Prometheus
- LogstashEncoder JSON stdout → Loki через Alloy
- OpenTelemetry → Tempo (OTLP gRPC)
## Архитектурные принципы
- **Generic dictionary engine + ЦУОД config bundle** (Approach B). Engine не знает про "КА"/"наземные станции" — узнаёт через JSON Schema из bundle
- **Bitemporal**: Envers для tx-time + valid_from/valid_to для effective-time + EXCLUSION CONSTRAINT
- **3 scope-топика Kafka** (`public/internal/restricted`) — изоляция через ACL, не через filter в consumer
- **System boundary**: 3 deployable делят БД (master+replica). Внешние consumers — только REST/Kafka, никогда прямо в БД
- **Tiered performance**: 1k RPS из коробки (PG replica + Caffeine). 10k+ RPS включается per-dictionary через Redis projection
- **Audit log** в БД (compliance), технические логи в Loki
## Build
```bash
mvn clean install # все модули
mvn -pl ordinis-app -am package -DskipTests # только app + transitive
docker build -f ordinis-app/Dockerfile -t ordinis-app:0.1.0 .
docker build -f ordinis-migrations/Dockerfile -t ordinis-migrations:0.1.0 ordinis-migrations/
```
## Связанные репо
- [terravault/ordinis-infra](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis-infra) — k8s манифесты, helm values
- [terravault/ordinis-config](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis-config) — Spring Cloud Config (feature flags, business params)
+18
View File
@@ -0,0 +1,18 @@
# Multi-stage build для ordinis-app
FROM maven:3.9.9-eclipse-temurin-21 AS build
WORKDIR /build
COPY pom.xml ./
COPY ordinis-domain ./ordinis-domain
COPY ordinis-validation ./ordinis-validation
COPY ordinis-events-api ./ordinis-events-api
COPY ordinis-outbox ./ordinis-outbox
COPY ordinis-rest-api ./ordinis-rest-api
COPY ordinis-cuod-bundle ./ordinis-cuod-bundle
COPY ordinis-app ./ordinis-app
RUN mvn -B -pl ordinis-app -am package -DskipTests
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /build/ordinis-app/target/*.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-XX:+UseZGC", "-XX:MaxRAMPercentage=75", "-jar", "/app/app.jar"]
+77
View File
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>ordinis-app</artifactId>
<name>Ordinis :: App (writer)</name>
<description>Spring Boot main для writer side: REST admin + outbox publisher + ЦУОД bundle.</description>
<dependencies>
<dependency>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-rest-api</artifactId>
</dependency>
<dependency>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-cuod-bundle</artifactId>
</dependency>
<!-- Cross-cutting -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
</dependency>
<!-- Observability — OpenTelemetry -->
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-starter</artifactId>
</dependency>
<!-- Config + Vault -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-vault-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,14 @@
package cloud.nstart.terravault.ordinis.app;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication(scanBasePackages = "cloud.nstart.terravault.ordinis")
@EnableScheduling
public class OrdinisApplication {
public static void main(String[] args) {
SpringApplication.run(OrdinisApplication.class, args);
}
}
@@ -0,0 +1,26 @@
package cloud.nstart.terravault.ordinis.app.web;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class HealthController {
@Value("${spring.application.name:ordinis-app}")
private String appName;
@Value("${ENVIRONMENT:dev}")
private String environment;
@GetMapping("/")
public Map<String, String> root() {
return Map.of(
"service", appName,
"environment", environment,
"status", "ok"
);
}
}
@@ -0,0 +1,70 @@
spring:
application:
name: ordinis-app
config:
import:
- "optional:configserver:${SPRING_CLOUD_CONFIG_URI:http://localhost:8888}"
- "optional:vault://"
cloud:
vault:
enabled: ${VAULT_ENABLED:false}
uri: ${VAULT_URI:http://vault.config:8200}
authentication: KUBERNETES
kubernetes:
role: ordinis-app
service-account-token-file: /var/run/secrets/kubernetes.io/serviceaccount/token
kv:
enabled: true
backend: secret
application-name: ordinis/cuod
config:
enabled: ${CONFIG_SERVER_ENABLED:false}
server:
port: 8080
shutdown: graceful
management:
endpoints:
web:
exposure:
include: health,info,prometheus,refresh,metrics,env
base-path: /actuator
endpoint:
health:
probes:
enabled: true
show-details: when-authorized
group:
liveness:
include: livenessState
readiness:
include: readinessState,db,kafka
prometheus:
metrics:
export:
enabled: true
metrics:
tags:
application: ${spring.application.name}
environment: ${ENVIRONMENT:dev}
# Defaults; реальные значения подъедутся из Spring Cloud Config + Vault
otel:
exporter:
otlp:
endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://tempo.monitoring:4317}
protocol: grpc
resource:
attributes:
service.name: ${spring.application.name}
service.namespace: ordinis
deployment.environment: ${ENVIRONMENT:dev}
traces:
sampler: parentbased_traceidratio
sampler.arg: ${ORDINIS_TRACING_SAMPLING:0.1}
logging:
level:
root: INFO
cloud.nstart.terravault.ordinis: DEBUG
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<springProperty scope="context" name="appName" source="spring.application.name" defaultValue="ordinis-app"/>
<springProperty scope="context" name="environment" source="ENVIRONMENT" defaultValue="dev"/>
<appender name="STDOUT_JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>trace_id</includeMdcKeyName>
<includeMdcKeyName>span_id</includeMdcKeyName>
<includeMdcKeyName>scope</includeMdcKeyName>
<includeMdcKeyName>dictionary</includeMdcKeyName>
<includeMdcKeyName>request_id</includeMdcKeyName>
<includeMdcKeyName>user_id</includeMdcKeyName>
<customFields>{"service":"${appName}","environment":"${environment}"}</customFields>
</encoder>
</appender>
<appender name="STDOUT_HUMAN" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- DEV: human-readable; PROD/STAGING: JSON для Loki -->
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="STDOUT_HUMAN"/>
</root>
<logger name="cloud.nstart.terravault.ordinis" level="DEBUG"/>
</springProfile>
<springProfile name="!dev">
<root level="INFO">
<appender-ref ref="STDOUT_JSON"/>
</root>
<logger name="cloud.nstart.terravault.ordinis" level="DEBUG"/>
</springProfile>
</configuration>
+27
View File
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>ordinis-cuod-bundle</artifactId>
<name>Ordinis :: ЦУОД Bundle</name>
<description>ЦУОД-specific config bundle: JSON Schema справочников (КА, satellite_type, ground_station), seed данные, Grafana dashboards.</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-domain</artifactId>
</dependency>
</dependencies>
</project>
+40
View File
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>ordinis-domain</artifactId>
<name>Ordinis :: Domain</name>
<description>JPA entities, JSONB types, базовый bitemporal model.</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>io.hypersistence</groupId>
<artifactId>hypersistence-utils-hibernate-63</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-envers</artifactId>
</dependency>
<dependency>
<groupId>net.postgis</groupId>
<artifactId>postgis-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>ordinis-events-api</artifactId>
<name>Ordinis :: Events API</name>
<description>DTO модели Kafka событий + semver. Подключается consumers (геопортал, Альтум).</description>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
</dependencies>
</project>
+11
View File
@@ -0,0 +1,11 @@
# Liquibase migration image — запускается Helm pre-upgrade Job (1-shot, без Spring Boot)
FROM liquibase/liquibase:4.27.0
USER root
COPY src/main/resources/db/changelog /liquibase/changelog
USER liquibase
ENV LIQUIBASE_COMMAND_CHANGELOG_FILE=/liquibase/changelog/master.xml
# Helm Job переопределяет JDBC URL через ENV
# CMD: ["update"] (default из base image работает с LIQUIBASE_COMMAND_*)
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>ordinis-migrations</artifactId>
<packaging>jar</packaging>
<name>Ordinis :: Migrations</name>
<description>Liquibase changelogs для Helm pre-upgrade Job. Собирается в отдельный Docker image (без Spring Boot).</description>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
</project>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
<changeSet id="0001-1-extensions" author="ordinis" runAlways="true">
<comment>Verify required PostgreSQL extensions are present (created by CNPG initdb).</comment>
<preConditions onFail="HALT">
<sqlCheck expectedResult="t">
SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'postgis')
</sqlCheck>
<sqlCheck expectedResult="t">
SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'btree_gist')
</sqlCheck>
</preConditions>
<sql>SELECT 1;</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
<changeSet id="0002-1-dictionary-definitions" author="ordinis">
<comment>Каталог справочников: имя, JSON Schema, scope, версия.</comment>
<createTable tableName="dictionary_definitions">
<column name="id" type="UUID" defaultValueComputed="gen_random_uuid()">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="VARCHAR(128)">
<constraints nullable="false" unique="true"/>
</column>
<column name="display_name" type="VARCHAR(256)"/>
<column name="description" type="TEXT"/>
<column name="scope" type="VARCHAR(32)">
<constraints nullable="false"/>
</column>
<column name="schema_json" type="JSONB">
<constraints nullable="false"/>
</column>
<column name="schema_version" type="VARCHAR(32)" defaultValue="1.0.0">
<constraints nullable="false"/>
</column>
<column name="bundle" type="VARCHAR(64)" defaultValue="cuod">
<constraints nullable="false"/>
</column>
<column name="created_at" type="TIMESTAMPTZ" defaultValueComputed="NOW()">
<constraints nullable="false"/>
</column>
<column name="updated_at" type="TIMESTAMPTZ" defaultValueComputed="NOW()">
<constraints nullable="false"/>
</column>
<column name="created_by" type="VARCHAR(128)"/>
<column name="updated_by" type="VARCHAR(128)"/>
</createTable>
<sql>
ALTER TABLE dictionary_definitions
ADD CONSTRAINT dictionary_definitions_scope_check
CHECK (scope IN ('public', 'internal', 'restricted'));
</sql>
<createIndex indexName="idx_dict_def_bundle" tableName="dictionary_definitions">
<column name="bundle"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
<changeSet id="0003-1-dictionary-records" author="ordinis">
<comment>Bitemporal dictionary records: tx-time через Envers, effective-time через valid_from/valid_to.</comment>
<createTable tableName="dictionary_records">
<column name="id" type="UUID" defaultValueComputed="gen_random_uuid()">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="dictionary_id" type="UUID">
<constraints nullable="false" foreignKeyName="fk_record_dictionary" references="dictionary_definitions(id)"/>
</column>
<column name="business_key" type="VARCHAR(256)">
<constraints nullable="false"/>
</column>
<column name="data" type="JSONB">
<constraints nullable="false"/>
</column>
<column name="geometry" type="GEOMETRY(Geometry, 4326)"/>
<column name="data_scope" type="VARCHAR(32)">
<constraints nullable="false"/>
</column>
<column name="valid_from" type="TIMESTAMPTZ">
<constraints nullable="false"/>
</column>
<column name="valid_to" type="TIMESTAMPTZ" defaultValue="9999-12-31T23:59:59Z"/>
<column name="created_at" type="TIMESTAMPTZ" defaultValueComputed="NOW()">
<constraints nullable="false"/>
</column>
<column name="updated_at" type="TIMESTAMPTZ" defaultValueComputed="NOW()">
<constraints nullable="false"/>
</column>
<column name="created_by" type="VARCHAR(128)"/>
<column name="updated_by" type="VARCHAR(128)"/>
</createTable>
<sql>
ALTER TABLE dictionary_records
ADD CONSTRAINT dictionary_records_scope_check
CHECK (data_scope IN ('public', 'internal', 'restricted'));
</sql>
<!-- EXCLUSION CONSTRAINT: запрещает overlapping valid периоды для одного business_key в одном словаре -->
<sql>
ALTER TABLE dictionary_records
ADD CONSTRAINT dictionary_records_no_overlap
EXCLUDE USING GIST (
dictionary_id WITH =,
business_key WITH =,
tstzrange(valid_from, valid_to, '[)') WITH &amp;&amp;
);
</sql>
<createIndex indexName="idx_dict_records_dict" tableName="dictionary_records">
<column name="dictionary_id"/>
</createIndex>
<createIndex indexName="idx_dict_records_scope" tableName="dictionary_records">
<column name="data_scope"/>
</createIndex>
<createIndex indexName="idx_dict_records_business_key" tableName="dictionary_records">
<column name="dictionary_id"/>
<column name="business_key"/>
</createIndex>
<createIndex indexName="idx_dict_records_valid_to" tableName="dictionary_records">
<column name="valid_to"/>
</createIndex>
<sql>
CREATE INDEX idx_dict_records_geometry ON dictionary_records USING GIST (geometry);
CREATE INDEX idx_dict_records_data_gin ON dictionary_records USING GIN (data jsonb_path_ops);
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
<changeSet id="0004-1-audit-log" author="ordinis">
<comment>Юридически значимый audit log. Source of truth для compliance, отдельно от Loki.</comment>
<createTable tableName="audit_log">
<column name="id" type="BIGSERIAL">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="event_time" type="TIMESTAMPTZ" defaultValueComputed="NOW()">
<constraints nullable="false"/>
</column>
<column name="event_type" type="VARCHAR(64)">
<constraints nullable="false"/>
</column>
<column name="user_id" type="VARCHAR(128)"/>
<column name="user_scope" type="VARCHAR(32)"/>
<column name="dictionary_id" type="UUID"/>
<column name="dictionary_name" type="VARCHAR(128)"/>
<column name="record_id" type="UUID"/>
<column name="business_key" type="VARCHAR(256)"/>
<column name="action" type="VARCHAR(32)"/>
<column name="payload_before" type="JSONB"/>
<column name="payload_after" type="JSONB"/>
<column name="trace_id" type="VARCHAR(64)"/>
<column name="request_id" type="VARCHAR(64)"/>
<column name="ip_address" type="INET"/>
<column name="user_agent" type="TEXT"/>
</createTable>
<createIndex indexName="idx_audit_event_time" tableName="audit_log">
<column name="event_time"/>
</createIndex>
<createIndex indexName="idx_audit_user_id" tableName="audit_log">
<column name="user_id"/>
</createIndex>
<createIndex indexName="idx_audit_dictionary" tableName="audit_log">
<column name="dictionary_id"/>
</createIndex>
<createIndex indexName="idx_audit_trace" tableName="audit_log">
<column name="trace_id"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
<changeSet id="0005-1-outbox-events" author="ordinis">
<comment>Outbox pattern: события публикуются в Kafka @Scheduled poller'ом.</comment>
<createTable tableName="outbox_events">
<column name="id" type="BIGSERIAL">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="event_type" type="VARCHAR(64)">
<constraints nullable="false"/>
</column>
<column name="aggregate_type" type="VARCHAR(64)">
<constraints nullable="false"/>
</column>
<column name="aggregate_id" type="VARCHAR(256)">
<constraints nullable="false"/>
</column>
<column name="dictionary_name" type="VARCHAR(128)"/>
<column name="data_scope" type="VARCHAR(32)">
<constraints nullable="false"/>
</column>
<column name="payload" type="JSONB">
<constraints nullable="false"/>
</column>
<column name="kafka_topic" type="VARCHAR(256)">
<constraints nullable="false"/>
</column>
<column name="kafka_key" type="VARCHAR(512)">
<constraints nullable="false"/>
</column>
<column name="created_at" type="TIMESTAMPTZ" defaultValueComputed="NOW()">
<constraints nullable="false"/>
</column>
<column name="published_at" type="TIMESTAMPTZ"/>
<column name="trace_id" type="VARCHAR(64)"/>
<column name="span_id" type="VARCHAR(64)"/>
<column name="attempt_count" type="INTEGER" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="last_error" type="TEXT"/>
</createTable>
<!-- Partial index: poller сканирует только unpublished -->
<sql>
CREATE INDEX idx_outbox_unpublished ON outbox_events (created_at)
WHERE published_at IS NULL;
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
<changeSet id="0006-1-idempotency-keys" author="ordinis">
<comment>Idempotency keys для exactly-once семантики write API. Retention 30 дней через cleanup job.</comment>
<createTable tableName="idempotency_keys">
<column name="key" type="VARCHAR(128)">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="request_hash" type="VARCHAR(64)">
<constraints nullable="false"/>
</column>
<column name="response_status" type="INTEGER"/>
<column name="response_body" type="JSONB"/>
<column name="created_at" type="TIMESTAMPTZ" defaultValueComputed="NOW()">
<constraints nullable="false"/>
</column>
<column name="expires_at" type="TIMESTAMPTZ">
<constraints nullable="false"/>
</column>
</createTable>
<createIndex indexName="idx_idempotency_expires" tableName="idempotency_keys">
<column name="expires_at"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
<include file="db/changelog/changes/0001-extensions.xml"/>
<include file="db/changelog/changes/0002-dictionary-definitions.xml"/>
<include file="db/changelog/changes/0003-dictionary-records.xml"/>
<include file="db/changelog/changes/0004-audit-log.xml"/>
<include file="db/changelog/changes/0005-outbox-events.xml"/>
<include file="db/changelog/changes/0006-idempotency-keys.xml"/>
</databaseChangeLog>
+35
View File
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>ordinis-outbox</artifactId>
<name>Ordinis :: Outbox</name>
<description>Outbox pattern + @Scheduled poller для надёжной публикации в Kafka.</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-domain</artifactId>
</dependency>
<dependency>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-events-api</artifactId>
</dependency>
</dependencies>
</project>
+70
View File
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>ordinis-projection-writer</artifactId>
<name>Ordinis :: Projection Writer</name>
<description>Подписывается на Kafka scope-топики, проецирует в Redis. Опциональный сервис, feature-flagged per dictionary.</description>
<dependencies>
<dependency>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-events-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-vault-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
+66
View File
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>ordinis-read-api</artifactId>
<name>Ordinis :: Read API</name>
<description>Read service для внешних consumers (геопортал, Альтум). Читает с PG replica с scope filtering.</description>
<dependencies>
<dependency>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-domain</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-vault-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>ordinis-rest-api</artifactId>
<name>Ordinis :: REST API</name>
<description>Admin write API: CRUD справочников, импорт bundle.</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-domain</artifactId>
</dependency>
<dependency>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-validation</artifactId>
</dependency>
<dependency>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-outbox</artifactId>
</dependency>
</dependencies>
</project>
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>ordinis-validation</artifactId>
<name>Ordinis :: Validation</name>
<description>JSON Schema validation для dictionary records.</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
</dependency>
<dependency>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-domain</artifactId>
</dependency>
</dependencies>
</project>
+139
View File
@@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Ordinis (parent)</name>
<description>MDM сервис для НСИ ЦУОД ОДХ. Часть линейки Terravault.</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.2</version>
<relativePath/>
</parent>
<modules>
<module>ordinis-domain</module>
<module>ordinis-validation</module>
<module>ordinis-events-api</module>
<module>ordinis-outbox</module>
<module>ordinis-rest-api</module>
<module>ordinis-app</module>
<module>ordinis-read-api</module>
<module>ordinis-projection-writer</module>
<module>ordinis-cuod-bundle</module>
<module>ordinis-migrations</module>
</modules>
<properties>
<java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-cloud.version>2024.0.0</spring-cloud.version>
<spring-cloud-vault.version>4.2.0</spring-cloud-vault.version>
<hypersistence-utils.version>3.9.0</hypersistence-utils.version>
<json-schema-validator.version>1.5.4</json-schema-validator.version>
<postgis-jdbc.version>2024.1.0</postgis-jdbc.version>
<logstash-logback.version>8.0</logstash-logback.version>
<opentelemetry-instrumentation.version>2.10.0</opentelemetry-instrumentation.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Internal modules -->
<dependency>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-validation</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-events-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-outbox</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-rest-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cloud.nstart.terravault.ordinis</groupId>
<artifactId>ordinis-cuod-bundle</artifactId>
<version>${project.version}</version>
</dependency>
<!-- External -->
<dependency>
<groupId>io.hypersistence</groupId>
<artifactId>hypersistence-utils-hibernate-63</artifactId>
<version>${hypersistence-utils.version}</version>
</dependency>
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
<version>${json-schema-validator.version}</version>
</dependency>
<dependency>
<groupId>net.postgis</groupId>
<artifactId>postgis-jdbc</artifactId>
<version>${postgis-jdbc.version}</version>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>${logstash-logback.version}</version>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-starter</artifactId>
<version>${opentelemetry-instrumentation.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>