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
+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>