initial commit
This commit is contained in:
8
.claude/settings.local.json
Normal file
8
.claude/settings.local.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(quarkus dev *)",
|
||||
"Bash(bash *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
107
CLAUDE.md
Normal file
107
CLAUDE.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Dokumenten-Check** is a document pre-verification system for Frigosped (freight forwarding). It checks transport documents and general terms & conditions (AGB) against configurable question catalogs, with AI-powered document translation and evaluation. Built on **Oracle APEX** + **Oracle Database 23c**.
|
||||
|
||||
## Database Setup
|
||||
|
||||
```bash
|
||||
# Connect to Oracle DB and run scripts in order:
|
||||
sqlplus user/pass@db @"Scripts/Datenmodell DocumentCheck.sql"
|
||||
sqlplus user/pass@db @"Scripts/Fragenkatalog-AGB anlegen.sql"
|
||||
```
|
||||
|
||||
No build/compilation step — APEX applications deploy directly to Oracle instances.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Data Model (10 tables in `Scripts/Datenmodell DocumentCheck.sql`)
|
||||
|
||||
**Configuration hierarchy:**
|
||||
- `dc_document_types` → `dc_question_catalogs` → `dc_question_categories` → `dc_questions`
|
||||
|
||||
**Execution hierarchy:**
|
||||
- `dc_projects` → `dc_project_documents` → `dc_results`
|
||||
|
||||
**Supporting:** `dc_users`, `dc_user_roles`, `dc_reference_documents`
|
||||
|
||||
### Key Patterns
|
||||
|
||||
- **Audit triggers on all tables**: Every table has BEFORE INSERT/UPDATE triggers that set `created_at`, `created_by`, `updated_at`, `updated_by`, `row_version`. User identity comes from `APEX_APPLICATION.G_USER`.
|
||||
- **Oracle Identity columns**: All PKs use `GENERATED BY DEFAULT AS IDENTITY`.
|
||||
- **Config-driven evaluation**: Each question defines `evaluation_type`, `threshold`, `result_on_deviation` (ABWEICHUNG/HINWEIS), plus example answers for 0%/100% cases.
|
||||
- **Async processing**: Projects progress through `PENDING → IN_PROGRESS → COMPLETED` with a 0–100% `processing_progress` field and email notification on completion.
|
||||
- **Filestorage**: `dc_project_documents` stores the`original_file` (BLOB) with MIME type and filename metadata. The original text `original_text` (CLOB) after after OCR and conversion to Markdown and the `translated_file` after Translation to German (CLOB)
|
||||
|
||||
### Result Types
|
||||
- `OK`, `UNKLAR` (unclear), `NOK` (not OK)
|
||||
- Boolean flags: `is_deviation`, `is_warning`
|
||||
|
||||
### User Roles
|
||||
- `ADMIN` — configuration management
|
||||
- `AUDITOR` — document verification
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Scripts/Datenmodell DocumentCheck.sql` | Full DB schema: 10 tables + 8 triggers |
|
||||
| `Scripts/Fragenkatalog-AGB anlegen.sql` | Sample AGB question catalog (6 categories, 25 questions) |
|
||||
| `Scripts/ORDS REST Services.sql` | All 11 ORDS REST endpoints (idempotent, run after schema) |
|
||||
| `Doku/Projektbeschreibung.md` | German requirements specification |
|
||||
| `Doku/Dokumenten-Check.md` | Data model + UI dialog specifications |
|
||||
|
||||
## Backend Service (Quarkus)
|
||||
|
||||
Located in `dc-backend/`. Java 21, Quarkus 3.15.1, LangChain4J 0.36.2.
|
||||
|
||||
```bash
|
||||
# Dev-Modus (hot reload)
|
||||
cd dc-backend
|
||||
mvn quarkus:dev
|
||||
|
||||
# Produktion bauen
|
||||
mvn package -DskipTests
|
||||
java -jar target/quarkus-app/quarkus-run.jar
|
||||
```
|
||||
|
||||
**Trigger-Endpunkt:**
|
||||
```bash
|
||||
POST http://localhost:8090/check/{projectId}
|
||||
# → 202 Accepted, Verarbeitung läuft asynchron
|
||||
```
|
||||
|
||||
### Backend-Architektur
|
||||
|
||||
```
|
||||
CheckResource POST /check/{projectId} → 202, Fire & Forget
|
||||
CheckOrchestrationService Haupt-Ablauf (Phase A: OCR/Übersetzung, Phase B: Auswertung)
|
||||
DocumentProcessingService PDF→KI-OCR, DOCX→POI→Markdown, ODT→ODF-Toolkit→Markdown
|
||||
EvaluationService translate() + evaluate() via LangChain4J ChatLanguageModel
|
||||
OrdsClient (REST Client) MicroProfile REST Client für alle /api/dc/-Endpunkte
|
||||
AiModelProducer (CDI) @Named("main") gpt-oss:20b + @Named("ocr") quen3.5:9b
|
||||
```
|
||||
|
||||
### KI-Modelle
|
||||
|
||||
| Qualifier | Modell | Zweck |
|
||||
|-----------|--------|-------|
|
||||
| `@Named("main")` | `gpt-oss:20b` | Übersetzung + Fragenauswertung |
|
||||
| `@Named("ocr")` | `quen3.5:9b` | PDF-Seiten als Bild → Markdown-Text |
|
||||
|
||||
Beide sprechen `https://ollama.aquantico.de` an (Ollama-API `/api/chat`) mit `X-API-KEY`-Header.
|
||||
Konfiguration in `dc-backend/src/main/resources/application.properties`.
|
||||
|
||||
### Verarbeitungsablauf
|
||||
|
||||
1. ORDS: Projekt laden + `status → IN_PROGRESS`
|
||||
2. ORDS: Dokumente + Fragen laden
|
||||
3. ORDS: Alte Ergebnisse löschen (Re-Processing-fähig)
|
||||
4. **Phase A** (pro Dokument): Download → OCR/Konvertierung → Übersetzung → ORDS `PUT /texts`
|
||||
5. **Phase B** (pro Dokument × Frage): Auswertung → Abweichungs-Berechnung → ORDS `POST /results`
|
||||
6. ORDS: `status → COMPLETED`
|
||||
|
||||
Abweichung/Hinweis wird in Java berechnet (nicht durch KI): `score < threshold` → prüfe `result_handling` (ABWEICHUNG/HINWEIS).
|
||||
225
Doku/Dokumenten-Check.md
Normal file
225
Doku/Dokumenten-Check.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# Dokumenten-Check
|
||||
|
||||
Projekt: https://projects.zoho.eu/portal/aquantico#project/323095000000333012
|
||||
|
||||
## Datenmodell
|
||||
|
||||
```mermaid
|
||||
|
||||
```
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
dc_users {
|
||||
number id PK
|
||||
varchar2_50 username UK
|
||||
varchar2_100 email UK
|
||||
varchar2_255 password_hash
|
||||
varchar2_20 role
|
||||
number is_active
|
||||
date created_at
|
||||
date updated_at
|
||||
}
|
||||
dc_user_roles {
|
||||
number id PK
|
||||
varchar2_20 role_name UK
|
||||
clob description
|
||||
}
|
||||
dc_document_types {
|
||||
number id PK
|
||||
varchar2_100 name
|
||||
clob description
|
||||
}
|
||||
dc_reference_documents {
|
||||
number id PK
|
||||
number uploaded_by FK
|
||||
number document_type_id FK
|
||||
varchar2_100 name
|
||||
clob description
|
||||
varchar2_500 file_path
|
||||
date upload_date
|
||||
varchar2_100 mime_type
|
||||
}
|
||||
dc_question_catalogs {
|
||||
number id PK
|
||||
number created_by_user FK
|
||||
number based_on_reference FK
|
||||
number document_type_id FK
|
||||
varchar2_100 name
|
||||
varchar2_4000 description
|
||||
}
|
||||
dc_question_categories {
|
||||
number id PK
|
||||
number catalog_id FK
|
||||
varchar2_100 name
|
||||
clob description
|
||||
}
|
||||
dc_questions {
|
||||
number id PK
|
||||
number category_id FK
|
||||
clob question_text
|
||||
varchar2_20 evaluation_type
|
||||
number threshold
|
||||
varchar2_20 result_handling
|
||||
clob example_0_percent
|
||||
clob example_100_percent
|
||||
}
|
||||
dc_projects {
|
||||
number id PK
|
||||
number catalog_id FK
|
||||
number created_by_user FK
|
||||
varchar2_100 name
|
||||
clob description
|
||||
varchar2_20 status
|
||||
number progress
|
||||
date completed_at
|
||||
varchar2_100 notification_email
|
||||
}
|
||||
dc_project_documents {
|
||||
number id PK
|
||||
number project_id FK
|
||||
number document_type_id FK
|
||||
blob original_file
|
||||
blob translated_file
|
||||
varchar2_255 filename
|
||||
varchar2_100 mime_type
|
||||
date uploaded_at
|
||||
}
|
||||
dc_results {
|
||||
number id PK
|
||||
number project_id FK
|
||||
number question_id FK
|
||||
number doc_id FK
|
||||
clob answer
|
||||
number score
|
||||
varchar2_20 result_type
|
||||
number deviation
|
||||
number warning
|
||||
clob the_comment
|
||||
date evaluated_at
|
||||
}
|
||||
dc_users ||--o{ dc_reference_documents : "uploads"
|
||||
dc_users ||--o{ dc_question_catalogs : "creates"
|
||||
dc_users ||--o{ dc_projects : "creates"
|
||||
dc_document_types ||--o{ dc_reference_documents : "used_in"
|
||||
dc_document_types ||--o{ dc_question_catalogs : "used_in"
|
||||
dc_document_types ||--o{ dc_project_documents : "used_in"
|
||||
dc_reference_documents ||--o{ dc_question_catalogs : "based_on"
|
||||
dc_question_catalogs ||--o{ dc_question_categories : "contains"
|
||||
dc_question_categories ||--o{ dc_questions : "contains"
|
||||
dc_question_catalogs ||--o{ dc_projects : "uses"
|
||||
dc_projects ||--o{ dc_project_documents : "contains"
|
||||
dc_projects ||--o{ dc_results : "has"
|
||||
dc_questions ||--o{ dc_results : "answered_in"
|
||||
dc_project_documents ||--o{ dc_results : "evaluated_in"
|
||||
```
|
||||
|
||||
## Backend
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Browser["🌐 Browser"]
|
||||
APEX["Oracle APEX\nFrontend UI"]
|
||||
end
|
||||
|
||||
subgraph OracleDB["🗄️ Oracle Database 23c"]
|
||||
ORDS["ORDS REST Services\n/api/dc/..."]
|
||||
subgraph Schema["DB Schema"]
|
||||
Config["Konfiguration\ndc_document_types\ndc_question_catalogs\ndc_question_categories\ndc_questions"]
|
||||
Exec["Ausführung\ndc_projects\ndc_project_documents\ndc_results"]
|
||||
Users["dc_users\ndc_user_roles\ndc_reference_documents"]
|
||||
end
|
||||
ORDS <--> Schema
|
||||
end
|
||||
|
||||
subgraph Backend["☕ Quarkus Backend (Port 8090)"]
|
||||
CheckRes["CheckResource\nPOST /check/{projectId}"]
|
||||
Orch["CheckOrchestrationService\nHauptablauf (async)"]
|
||||
DocProc["DocumentProcessingService\nPDF/DOCX/ODT → Markdown"]
|
||||
EvalSvc["EvaluationService\nÜbersetzung + Auswertung"]
|
||||
OrdsClient["OrdsClient\nMicroProfile REST Client"]
|
||||
|
||||
CheckRes -->|"202 Accepted\nFire & Forget"| Orch
|
||||
Orch --> DocProc
|
||||
Orch --> EvalSvc
|
||||
Orch --> OrdsClient
|
||||
end
|
||||
|
||||
subgraph AI["🤖 Ollama (ollama.aquantico.de)"]
|
||||
MainModel["gpt-oss:20b\nÜbersetzung + Fragenauswertung"]
|
||||
OcrModel["quen3.5:9b\nPDF Vision-OCR"]
|
||||
end
|
||||
|
||||
APEX -->|"Dokument hochladen\nProjekt anlegen"| ORDS
|
||||
APEX -->|"POST /check/{id}"| CheckRes
|
||||
APEX -->|"Ergebnisse lesen"| ORDS
|
||||
|
||||
OrdsClient -->|"Projekt/Dok/Fragen laden\nErgebnisse speichern"| ORDS
|
||||
|
||||
DocProc -->|"Base64-Bild\nX-API-KEY"| OcrModel
|
||||
EvalSvc -->|"Text + Prompt\nX-API-KEY"| MainModel
|
||||
|
||||
subgraph PhaseA["Phase A (pro Dokument)"]
|
||||
A1["Download BLOB"] --> A2["OCR / Konvertierung\n→ Markdown"]
|
||||
A2 --> A3["Übersetzung → Deutsch"]
|
||||
A3 --> A4["Texte speichern (ORDS)"]
|
||||
end
|
||||
|
||||
subgraph PhaseB["Phase B (pro Dok × Frage)"]
|
||||
B1["KI-Auswertung\nscore 0–100"] --> B2["Abweichung/Hinweis\nberechnen (Java)"]
|
||||
B2 --> B3["Ergebnis speichern\n(ORDS)"]
|
||||
end
|
||||
|
||||
Orch --> PhaseA
|
||||
Orch --> PhaseB
|
||||
|
||||
style Browser fill:#dbeafe
|
||||
style OracleDB fill:#fef3c7
|
||||
style Backend fill:#dcfce7
|
||||
style AI fill:#f3e8ff
|
||||
style PhaseA fill:#f0f9ff
|
||||
style PhaseB fill:#f0f9ff
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Dialoge
|
||||
|
||||
### Fragenkatalog
|
||||
|
||||
#### Tabelle
|
||||
|
||||
* Name
|
||||
* Beschreibung
|
||||
* Referenz-Dokument
|
||||
|
||||
#### Dialog
|
||||
|
||||
* Name (Im Kopf aussuchen)
|
||||
* Referenz-Dokument Upload (Im Kopf)
|
||||
* Master/Detail
|
||||
* Fragen-Kategorien
|
||||
* Fragen
|
||||
|
||||
|
||||
|
||||
### Projekte
|
||||
|
||||
#### Tabelle
|
||||
|
||||
* Name
|
||||
* Beschreibung
|
||||
* Anz ok,nok,undefiniert
|
||||
|
||||
#### Dialog
|
||||
|
||||
* Name (Im Kopf Aussuchen)
|
||||
* Beschreibung
|
||||
* Master/Details
|
||||
* Dokumente
|
||||
* Ergebnisse
|
||||
|
||||
|
||||
|
||||
58
Doku/Projektbeschreibung.md
Normal file
58
Doku/Projektbeschreibung.md
Normal file
@@ -0,0 +1,58 @@
|
||||
### Ziel
|
||||
|
||||
Unterstützende Vorprüfung von Dokumenten
|
||||
|
||||
- TU-Dokumenten gegen eine manuell zu konfigurierenden Fragenkatalog
|
||||
- Transportbedingungen gegen die **Allgemeinen Deutschen Spediteurbedingungen (ADSp)
|
||||
**
|
||||
|
||||
|
||||
### Leistungsumfang
|
||||
|
||||
Erstellen einer APEX-Applikation mit folgenden Funktionalitäten
|
||||
|
||||
**Administration**
|
||||
|
||||
- Konfigurierbare **Fragenk****a****taloge** (TU-Dokumente / Transportbedingungen)
|
||||
- Es werden in Kategorien Fragen definiert und angegeben wie die Frage bewertet werden soll.
|
||||
- Beispiele für eine 0% Bewertung und Beispiele für eine 100% Bewertung
|
||||
- Schwellwert für OK / Unklar / Nicht OK
|
||||
- Umgang mit dem Ergebnis der Antwort
|
||||
- Markieren als Abweichung
|
||||
- Ausgabe als Hinweis (z.B. um Strafzahlungen explizit anzuzeigen)
|
||||
- Hinterlegung von Referenzdokumenten auf die sich die Fragen beziehen können. (z.B. die ADSp)
|
||||
- Benutzeradministration
|
||||
- Anlegen und Verwalten von Benutzern
|
||||
- Wer darf Konfigurationen hinterlegen?
|
||||
- Wer darf Dokumente prüfen?
|
||||
- Hinterlegen einer E-Mail für die Benachrichtigung
|
||||
|
||||
|
||||
|
||||
**Prüf-Funktionalität**
|
||||
|
||||
- Anlegen eines Prüfungsauftrags
|
||||
- Laden von Dokumenten
|
||||
- Angabe, welche Prüfung erfolgen soll
|
||||
- Start der Prüfung
|
||||
- Prüfung als Hintergrundverarbeitung
|
||||
- Dokumente ins Deutsche Übersetzen
|
||||
- Dokumente entsprechend des vorgegebenen Fragenkataloges prüfen
|
||||
- Pro Dokument ein Prüfprotokoll erstellen
|
||||
- Wenn alle Prüfungen abgeschlossen sind
|
||||
- Abschließendes Prüfungsprotokoll erstellen
|
||||
- Benutzer per Email über die abgeschlossene Prüfung Informieren
|
||||
- Alle Prüfungsprotokolle werden angehängt, auch das abschließende
|
||||
- Link, um in die Applikation zum Prüfungsauftrag zu springen um die Prüfungsergebnisse, Dokumente original und übersetzt direkt anzeigen zu lassen
|
||||
- Prüfungsübersicht
|
||||
- Anzeige der laufenden und fertiggestellten Prüfungen
|
||||
- Bei laufenden Prüfungen den Fortschritt
|
||||
- Sprung zum Prüfungsauftrag
|
||||
- Prüfungsauftrag anzeigen
|
||||
- Darstellen welcher Fragenkatalog auf die Dokumente angewendet wurde
|
||||
- Auflisten der Dokumente (original und übersetzt) mit der Möglichkeit der Anzeige und des Downloads
|
||||
- Darstellen der Prüfungsergebnisse (oder Fehler bei der Verarbeitung) pro Dokument
|
||||
|
||||
|
||||
|
||||
ACHTUNG: Eine KI kann Fehler machen. Die Ergebnisse können nicht rechtssicher sein.
|
||||
297
Scripts/DC_BACKEND_PKG.sql
Normal file
297
Scripts/DC_BACKEND_PKG.sql
Normal file
@@ -0,0 +1,297 @@
|
||||
-- =============================================================================
|
||||
-- DC_BACKEND_PKG
|
||||
-- PL/SQL-Schnittstelle zum Quarkus dc-backend (Kubernetes Service dc-backend:8090)
|
||||
--
|
||||
-- Voraussetzungen:
|
||||
-- - APEX_WEB_SERVICE-Berechtigung für das Schema:
|
||||
-- GRANT EXECUTE ON APEX_240100.APEX_WEB_SERVICE TO <schema>; -- APEX-Version anpassen
|
||||
-- - ACL für ausgehende HTTP-Verbindung im selben Namespace:
|
||||
-- DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE(
|
||||
-- host => 'dc-backend', lower_port => 8090, upper_port => 8090,
|
||||
-- ace => xs$ace_type(privilege_list => xs$name_list('connect','resolve'),
|
||||
-- principal_name => '<schema>', principal_type => xs_acl.ptype_db));
|
||||
--
|
||||
-- Deployment:
|
||||
-- sqlplus user/pass@db @"Scripts/DC_BACKEND_PKG.sql"
|
||||
-- =============================================================================
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- Package Spec
|
||||
-- =============================================================================
|
||||
CREATE OR REPLACE PACKAGE DC_BACKEND_PKG AS
|
||||
|
||||
-- Basis-URL des dc-backend Kubernetes Service
|
||||
c_base_url CONSTANT VARCHAR2(200) := 'http://dc-backend:8090';
|
||||
|
||||
-- API-Key (X-API-KEY Header).
|
||||
-- Leer = kein Auth (Dev-Modus).
|
||||
-- Zur Laufzeit setzen: DC_BACKEND_PKG.g_api_key := 'mein-key';
|
||||
g_api_key VARCHAR2(500) := '';
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Startet die KI-gestützte Dokumentenprüfung für ein Projekt (asynchron).
|
||||
-- Das Projekt muss im Status PENDING sein.
|
||||
-- POST /check/{p_project_id} → 202 Accepted
|
||||
--
|
||||
-- p_project_id ID des dc_projects-Eintrags
|
||||
-- p_status_code HTTP-Statuscode der Antwort (202 = OK, 409 = nicht PENDING)
|
||||
-- p_response JSON-Antwort des Backend
|
||||
-- -------------------------------------------------------------------------
|
||||
PROCEDURE start_check(
|
||||
p_project_id IN NUMBER,
|
||||
p_status_code OUT NUMBER,
|
||||
p_response OUT VARCHAR2
|
||||
);
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Health-Check: prüft ob der dc-backend Service erreichbar ist.
|
||||
-- GET /check/health → 200 UP
|
||||
--
|
||||
-- Rückgabe: TRUE = Service läuft, FALSE = nicht erreichbar
|
||||
-- -------------------------------------------------------------------------
|
||||
FUNCTION health_check RETURN BOOLEAN;
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Startet die Prüfung und wirft eine Exception wenn nicht 202 zurückkommt.
|
||||
-- Convenience-Wrapper für direkte APEX-Button-Aufrufe.
|
||||
-- -------------------------------------------------------------------------
|
||||
PROCEDURE start_check_or_raise(
|
||||
p_project_id IN NUMBER
|
||||
);
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Fortschritts-Record
|
||||
-- -------------------------------------------------------------------------
|
||||
TYPE t_project_status IS RECORD (
|
||||
status dc_projects.status%TYPE,
|
||||
processing_progress dc_projects.progress%TYPE,
|
||||
is_completed BOOLEAN,
|
||||
is_running BOOLEAN,
|
||||
is_pending BOOLEAN
|
||||
);
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Liest Status und Fortschritt eines Projekts direkt aus der DB.
|
||||
-- Kein REST-Aufruf – sofort und transaktionssicher.
|
||||
--
|
||||
-- Beispiel:
|
||||
-- l_s := DC_BACKEND_PKG.get_status(1);
|
||||
-- DBMS_OUTPUT.PUT_LINE(l_s.status || ' ' || l_s.processing_progress || '%');
|
||||
-- -------------------------------------------------------------------------
|
||||
FUNCTION get_status(
|
||||
p_project_id IN NUMBER
|
||||
) RETURN t_project_status;
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Wartet (polling) bis das Projekt COMPLETED oder ein Timeout erreicht ist.
|
||||
-- Wirft eine Exception bei Timeout oder wenn das Projekt nicht gefunden wird.
|
||||
--
|
||||
-- p_timeout_sec Max. Wartezeit in Sekunden (Default: 1800 = 30 Min.)
|
||||
-- p_interval_sec Polling-Intervall in Sekunden (Default: 5)
|
||||
-- -------------------------------------------------------------------------
|
||||
PROCEDURE wait_for_completion(
|
||||
p_project_id IN NUMBER,
|
||||
p_timeout_sec IN NUMBER DEFAULT 1800,
|
||||
p_interval_sec IN NUMBER DEFAULT 5
|
||||
);
|
||||
|
||||
END DC_BACKEND_PKG;
|
||||
/
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- Package Body
|
||||
-- =============================================================================
|
||||
CREATE OR REPLACE PACKAGE BODY DC_BACKEND_PKG AS
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Setzt gemeinsame Request-Header (Content-Type + optional API-Key)
|
||||
-- -------------------------------------------------------------------------
|
||||
PROCEDURE set_headers IS
|
||||
l_idx PLS_INTEGER := 1;
|
||||
BEGIN
|
||||
APEX_WEB_SERVICE.G_REQUEST_HEADERS.DELETE;
|
||||
|
||||
APEX_WEB_SERVICE.G_REQUEST_HEADERS(l_idx).NAME := 'Content-Type';
|
||||
APEX_WEB_SERVICE.G_REQUEST_HEADERS(l_idx).VALUE := 'application/json';
|
||||
l_idx := l_idx + 1;
|
||||
|
||||
IF g_api_key IS NOT NULL AND LENGTH(TRIM(g_api_key)) > 0 THEN
|
||||
APEX_WEB_SERVICE.G_REQUEST_HEADERS(l_idx).NAME := 'X-API-KEY';
|
||||
APEX_WEB_SERVICE.G_REQUEST_HEADERS(l_idx).VALUE := g_api_key;
|
||||
END IF;
|
||||
END set_headers;
|
||||
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
PROCEDURE start_check(
|
||||
p_project_id IN NUMBER,
|
||||
p_status_code OUT NUMBER,
|
||||
p_response OUT VARCHAR2
|
||||
) IS
|
||||
l_url VARCHAR2(500);
|
||||
l_response CLOB;
|
||||
BEGIN
|
||||
l_url := c_base_url || '/check/' || TO_CHAR(p_project_id);
|
||||
|
||||
set_headers();
|
||||
|
||||
l_response := APEX_WEB_SERVICE.MAKE_REST_REQUEST(
|
||||
p_url => l_url,
|
||||
p_http_method => 'POST',
|
||||
p_body => '{}' -- leerer Body, projectId ist im Pfad
|
||||
);
|
||||
|
||||
p_status_code := APEX_WEB_SERVICE.G_STATUS_CODE;
|
||||
p_response := SUBSTR(l_response, 1, 4000);
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
p_status_code := -1;
|
||||
p_response := 'Verbindungsfehler: ' || SQLERRM;
|
||||
END start_check;
|
||||
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
FUNCTION health_check RETURN BOOLEAN IS
|
||||
l_url VARCHAR2(500);
|
||||
l_response CLOB;
|
||||
l_status NUMBER;
|
||||
BEGIN
|
||||
l_url := c_base_url || '/check/health';
|
||||
|
||||
set_headers();
|
||||
|
||||
l_response := APEX_WEB_SERVICE.MAKE_REST_REQUEST(
|
||||
p_url => l_url,
|
||||
p_http_method => 'GET'
|
||||
);
|
||||
|
||||
l_status := APEX_WEB_SERVICE.G_STATUS_CODE;
|
||||
RETURN l_status = 200;
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RETURN FALSE;
|
||||
END health_check;
|
||||
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
PROCEDURE start_check_or_raise(
|
||||
p_project_id IN NUMBER
|
||||
) IS
|
||||
l_status NUMBER;
|
||||
l_response VARCHAR2(4000);
|
||||
BEGIN
|
||||
start_check(
|
||||
p_project_id => p_project_id,
|
||||
p_status_code => l_status,
|
||||
p_response => l_response
|
||||
);
|
||||
|
||||
IF l_status = 202 THEN
|
||||
-- OK: Verarbeitung wurde gestartet
|
||||
NULL;
|
||||
ELSIF l_status = 409 THEN
|
||||
RAISE_APPLICATION_ERROR(-20100,
|
||||
'Projekt ' || p_project_id || ' ist nicht im Status PENDING.');
|
||||
ELSIF l_status = 401 THEN
|
||||
RAISE_APPLICATION_ERROR(-20101,
|
||||
'Ungültiger API-Key für dc-backend.');
|
||||
ELSIF l_status = -1 THEN
|
||||
RAISE_APPLICATION_ERROR(-20102,
|
||||
'dc-backend nicht erreichbar: ' || l_response);
|
||||
ELSE
|
||||
RAISE_APPLICATION_ERROR(-20103,
|
||||
'Unerwarteter HTTP-Status ' || l_status || ': ' || l_response);
|
||||
END IF;
|
||||
END start_check_or_raise;
|
||||
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
FUNCTION get_status(
|
||||
p_project_id IN NUMBER
|
||||
) RETURN t_project_status IS
|
||||
l_rec t_project_status;
|
||||
BEGIN
|
||||
SELECT status, progress
|
||||
INTO l_rec.status, l_rec.processing_progress
|
||||
FROM dc_projects
|
||||
WHERE id = p_project_id;
|
||||
|
||||
l_rec.is_completed := l_rec.status = 'COMPLETED';
|
||||
l_rec.is_running := l_rec.status = 'IN_PROGRESS';
|
||||
l_rec.is_pending := l_rec.status = 'PENDING';
|
||||
RETURN l_rec;
|
||||
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN
|
||||
RAISE_APPLICATION_ERROR(-20110,
|
||||
'Projekt ' || p_project_id || ' nicht gefunden.');
|
||||
END get_status;
|
||||
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
PROCEDURE wait_for_completion(
|
||||
p_project_id IN NUMBER,
|
||||
p_timeout_sec IN NUMBER DEFAULT 1800,
|
||||
p_interval_sec IN NUMBER DEFAULT 5
|
||||
) IS
|
||||
l_start TIMESTAMP := SYSTIMESTAMP;
|
||||
l_status t_project_status;
|
||||
BEGIN
|
||||
LOOP
|
||||
l_status := get_status(p_project_id);
|
||||
|
||||
EXIT WHEN l_status.is_completed;
|
||||
|
||||
IF EXTRACT(SECOND FROM (SYSTIMESTAMP - l_start))
|
||||
+ EXTRACT(MINUTE FROM (SYSTIMESTAMP - l_start)) * 60
|
||||
+ EXTRACT(HOUR FROM (SYSTIMESTAMP - l_start)) * 3600
|
||||
> p_timeout_sec
|
||||
THEN
|
||||
RAISE_APPLICATION_ERROR(-20111,
|
||||
'Timeout nach ' || p_timeout_sec || 's – Projekt ' ||
|
||||
p_project_id || ' noch im Status ' || l_status.status ||
|
||||
' (' || l_status.processing_progress || '%).');
|
||||
END IF;
|
||||
|
||||
DBMS_SESSION.SLEEP(p_interval_sec);
|
||||
END LOOP;
|
||||
END wait_for_completion;
|
||||
|
||||
|
||||
END DC_BACKEND_PKG;
|
||||
/
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- Schnelltest (optional, auskommentiert)
|
||||
-- =============================================================================
|
||||
/*
|
||||
-- Health-Check
|
||||
BEGIN
|
||||
IF DC_BACKEND_PKG.health_check() THEN
|
||||
DBMS_OUTPUT.PUT_LINE('dc-backend: UP');
|
||||
ELSE
|
||||
DBMS_OUTPUT.PUT_LINE('dc-backend: NICHT ERREICHBAR');
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
-- Prüfung starten (Projekt-ID anpassen)
|
||||
DECLARE
|
||||
l_status NUMBER;
|
||||
l_response VARCHAR2(4000);
|
||||
BEGIN
|
||||
DC_BACKEND_PKG.start_check(
|
||||
p_project_id => 1,
|
||||
p_status_code => l_status,
|
||||
p_response => l_response
|
||||
);
|
||||
DBMS_OUTPUT.PUT_LINE('Status: ' || l_status);
|
||||
DBMS_OUTPUT.PUT_LINE('Response: ' || l_response);
|
||||
END;
|
||||
/
|
||||
*/
|
||||
535
Scripts/Datenmodell DocumentCheck.sql
Normal file
535
Scripts/Datenmodell DocumentCheck.sql
Normal file
@@ -0,0 +1,535 @@
|
||||
drop table dc_users cascade constraints;
|
||||
drop table dc_document_types cascade constraints;
|
||||
drop table dc_user_roles cascade constraints;
|
||||
drop table dc_question_catalogs cascade constraints;
|
||||
drop table dc_question_categories cascade constraints;
|
||||
drop table dc_questions cascade constraints;
|
||||
drop table dc_reference_documents cascade constraints;
|
||||
drop table dc_projects cascade constraints;
|
||||
drop table dc_project_documents cascade constraints;
|
||||
drop table dc_results cascade constraints;
|
||||
-- create tables
|
||||
|
||||
create table dc_users (
|
||||
id number generated by default on null as identity
|
||||
constraint dc_users_id_pk primary key,
|
||||
username varchar2(50 char)
|
||||
constraint users_username_unq unique not null,
|
||||
email varchar2(100 char)
|
||||
constraint users_email_unq unique not null,
|
||||
password_hash varchar2(255 char),
|
||||
role varchar2(20 char),
|
||||
is_active number default on null 1,
|
||||
created_at date default on null sysdate,
|
||||
updated_at date default on null sysdate,
|
||||
row_version integer not null,
|
||||
created date not null,
|
||||
created_by varchar2(255 char) not null,
|
||||
updated date not null,
|
||||
updated_by varchar2(255 char) not null
|
||||
);
|
||||
|
||||
create table dc_document_types (
|
||||
id number generated by default on null as identity
|
||||
constraint dc_document_types_id_pk primary key,
|
||||
name varchar2(100 char) not null,
|
||||
description clob,
|
||||
row_version integer not null,
|
||||
created date not null,
|
||||
created_by varchar2(255 char) not null,
|
||||
updated date not null,
|
||||
updated_by varchar2(255 char) not null
|
||||
);
|
||||
|
||||
create table dc_reference_documents (
|
||||
id number generated by default on null as identity
|
||||
constraint dc_reference_documents_id_pk primary key,
|
||||
uploaded_by number constraint dc_reference_documents_uploaded_by_fk
|
||||
references dc_users,
|
||||
document_type_id number constraint dc_reference_documents_doc_type_fk
|
||||
references dc_document_types,
|
||||
name varchar2(100 char) not null,
|
||||
description clob,
|
||||
file_path varchar2(500 char),
|
||||
upload_date date default on null sysdate,
|
||||
mime_type varchar2(100 char),
|
||||
row_version integer not null,
|
||||
created date not null,
|
||||
created_by varchar2(255 char) not null,
|
||||
updated date not null,
|
||||
updated_by varchar2(255 char) not null
|
||||
);
|
||||
|
||||
-- table index
|
||||
create index dc_reference_documents_i1 on dc_reference_documents (uploaded_by);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
create table dc_user_roles (
|
||||
id number generated by default on null as identity
|
||||
constraint dc_user_roles_id_pk primary key,
|
||||
role_name varchar2(20 char)
|
||||
constraint user_roles_role_name_unq unique not null,
|
||||
description clob,
|
||||
row_version integer not null,
|
||||
created date not null,
|
||||
created_by varchar2(255 char) not null,
|
||||
updated date not null,
|
||||
updated_by varchar2(255 char) not null
|
||||
);
|
||||
|
||||
|
||||
|
||||
create table dc_question_catalogs (
|
||||
id number generated by default on null as identity
|
||||
constraint dc_question_catalogs_id_pk primary key,
|
||||
created_by_user number constraint dc_question_catalogs_created_by_fk
|
||||
references dc_users,
|
||||
based_on_reference number constraint dc_question_catalogs_reference_document_fk
|
||||
references dc_reference_documents,
|
||||
document_type_id number constraint dc_question_catalogs_doc_type_fk
|
||||
references dc_document_types,
|
||||
name varchar2(100 char) not null,
|
||||
description varchar2(4000 char),
|
||||
row_version integer not null,
|
||||
created date not null,
|
||||
created_by varchar2(255 char) not null,
|
||||
updated date not null,
|
||||
updated_by varchar2(255 char) not null
|
||||
);
|
||||
|
||||
-- table index
|
||||
create index dc_question_catalogs_i1 on dc_question_catalogs (created_by);
|
||||
|
||||
|
||||
|
||||
create table dc_question_categories (
|
||||
id number generated by default on null as identity
|
||||
constraint dc_question_categories_id_pk primary key,
|
||||
catalog_id number constraint dc_question_categories_catalog_id_fk
|
||||
references dc_question_catalogs,
|
||||
name varchar2(100 char) not null,
|
||||
description varchar2(4000 char),
|
||||
row_version integer not null,
|
||||
created date not null,
|
||||
created_by varchar2(255 char) not null,
|
||||
updated date not null,
|
||||
updated_by varchar2(255 char) not null
|
||||
);
|
||||
|
||||
-- table index
|
||||
create index dc_question_categories_i1 on dc_question_categories (catalog_id);
|
||||
|
||||
|
||||
|
||||
create table dc_questions (
|
||||
id number generated by default on null as identity
|
||||
constraint dc_questions_id_pk primary key,
|
||||
category_id number constraint dc_questions_category_id_fk
|
||||
references dc_question_categories,
|
||||
question_text clob not null,
|
||||
evaluation_type varchar2(20 char),
|
||||
threshold number,
|
||||
result_handling varchar2(20 char),
|
||||
example_0_percent clob,
|
||||
example_100_percent clob,
|
||||
row_version integer not null,
|
||||
created date not null,
|
||||
created_by varchar2(255 char) not null,
|
||||
updated date not null,
|
||||
updated_by varchar2(255 char) not null
|
||||
);
|
||||
|
||||
-- table index
|
||||
create index dc_questions_i1 on dc_questions (category_id);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
create table dc_projects (
|
||||
id number generated by default on null as identity
|
||||
constraint dc_projects_id_pk primary key,
|
||||
catalog_id number constraint dc_projects_catalog_id_fk
|
||||
references dc_question_catalogs,
|
||||
created_by_user number constraint dc_projects_created_by_fk
|
||||
references dc_users,
|
||||
name varchar2(100 char) not null,
|
||||
description clob,
|
||||
status varchar2(20 char) constraint dc_projects_status_ck
|
||||
check (status in ('PENDING','IN_PROGRESS','COMPLETED')),
|
||||
progress number default on null 0,
|
||||
completed_at date,
|
||||
notification_email varchar2(100 char),
|
||||
row_version integer not null,
|
||||
created date not null,
|
||||
created_by varchar2(255 char) not null,
|
||||
updated date not null,
|
||||
updated_by varchar2(255 char) not null
|
||||
);
|
||||
|
||||
-- table index
|
||||
create index dc_projects_i1 on dc_projects (catalog_id);
|
||||
|
||||
create index dc_projects_i2 on dc_projects (created_by);
|
||||
|
||||
|
||||
|
||||
create table dc_project_documents (
|
||||
id number generated by default on null as identity
|
||||
constraint dc_project_documents_id_pk primary key,
|
||||
project_id number constraint dc_project_documents_project_id_fk
|
||||
references dc_projects,
|
||||
document_type_id number constraint dc_project_documents_doc_type_fk
|
||||
references dc_document_types,
|
||||
catalog_id number constraint dc_project_documents_catalog_id_fk
|
||||
references dc_question_catalogs,
|
||||
original_file blob,
|
||||
original_text clob,
|
||||
translated_text clob,
|
||||
filename varchar2(255 char) not null,
|
||||
mime_type varchar2(100 char),
|
||||
uploaded_at date default on null sysdate,
|
||||
row_version integer not null,
|
||||
created date not null,
|
||||
created_by varchar2(255 char) not null,
|
||||
updated date not null,
|
||||
updated_by varchar2(255 char) not null
|
||||
);
|
||||
|
||||
-- table index
|
||||
create index dc_project_documents_i1 on dc_project_documents (project_id);
|
||||
|
||||
|
||||
|
||||
create table dc_results (
|
||||
id number generated by default on null as identity
|
||||
constraint dc_results_id_pk primary key,
|
||||
project_id number constraint dc_results_project_id_fk
|
||||
references dc_projects,
|
||||
question_id number constraint dc_results_question_id_fk
|
||||
references dc_questions,
|
||||
doc_id number constraint dc_results_doc_id_fk
|
||||
references dc_project_documents,
|
||||
answer clob,
|
||||
score number,
|
||||
result_type varchar2(20 char) constraint dc_results_result_type_ck
|
||||
check (result_type in ('OK','UNKLAR','NOK')),
|
||||
deviation number default on null 0,
|
||||
warning number default on null 0,
|
||||
the_comment clob,
|
||||
evaluated_at date default on null sysdate,
|
||||
row_version integer not null,
|
||||
created date not null,
|
||||
created_by varchar2(255 char) not null,
|
||||
updated date not null,
|
||||
updated_by varchar2(255 char) not null
|
||||
);
|
||||
|
||||
-- table index
|
||||
create index dc_results_i1 on dc_results (project_id);
|
||||
|
||||
create index dc_results_i2 on dc_results (question_id);
|
||||
|
||||
create index dc_results_i3 on dc_results (doc_id);
|
||||
|
||||
|
||||
|
||||
-- triggers
|
||||
create or replace trigger dc_users_biu
|
||||
before insert or update
|
||||
on dc_users
|
||||
for each row
|
||||
begin
|
||||
if inserting then
|
||||
:new.row_version := 1;
|
||||
elsif updating then
|
||||
:new.row_version := NVL(:old.row_version, 0) + 1;
|
||||
end if;
|
||||
if inserting then
|
||||
:new.created := sysdate;
|
||||
:new.created_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
|
||||
end if;
|
||||
:new.updated := sysdate;
|
||||
:new.updated_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
|
||||
end dc_users_biu;
|
||||
/
|
||||
|
||||
|
||||
create or replace trigger dc_user_roles_biu
|
||||
before insert or update
|
||||
on dc_user_roles
|
||||
for each row
|
||||
begin
|
||||
if inserting then
|
||||
:new.row_version := 1;
|
||||
elsif updating then
|
||||
:new.row_version := NVL(:old.row_version, 0) + 1;
|
||||
end if;
|
||||
if inserting then
|
||||
:new.created := sysdate;
|
||||
:new.created_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
|
||||
end if;
|
||||
:new.updated := sysdate;
|
||||
:new.updated_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
|
||||
end dc_user_roles_biu;
|
||||
/
|
||||
|
||||
create or replace trigger dc_document_types_biu
|
||||
before insert or update
|
||||
on dc_document_types
|
||||
for each row
|
||||
begin
|
||||
if inserting then
|
||||
:new.row_version := 1;
|
||||
elsif updating then
|
||||
:new.row_version := NVL(:old.row_version, 0) + 1;
|
||||
end if;
|
||||
if inserting then
|
||||
:new.created := sysdate;
|
||||
:new.created_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
|
||||
end if;
|
||||
:new.updated := sysdate;
|
||||
:new.updated_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
|
||||
end dc_document_types_biu;
|
||||
/
|
||||
|
||||
create or replace trigger dc_question_catalogs_biu
|
||||
before insert or update
|
||||
on dc_question_catalogs
|
||||
for each row
|
||||
begin
|
||||
if inserting then
|
||||
:new.row_version := 1;
|
||||
elsif updating then
|
||||
:new.row_version := NVL(:old.row_version, 0) + 1;
|
||||
end if;
|
||||
if inserting then
|
||||
:new.created := sysdate;
|
||||
:new.created_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
|
||||
end if;
|
||||
:new.updated := sysdate;
|
||||
:new.updated_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
|
||||
end dc_question_catalogs_biu;
|
||||
/
|
||||
|
||||
|
||||
create or replace trigger dc_question_categories_biu
|
||||
before insert or update
|
||||
on dc_question_categories
|
||||
for each row
|
||||
begin
|
||||
if inserting then
|
||||
:new.row_version := 1;
|
||||
elsif updating then
|
||||
:new.row_version := NVL(:old.row_version, 0) + 1;
|
||||
end if;
|
||||
if inserting then
|
||||
:new.created := sysdate;
|
||||
:new.created_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
|
||||
end if;
|
||||
:new.updated := sysdate;
|
||||
:new.updated_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
|
||||
end dc_question_categories_biu;
|
||||
/
|
||||
|
||||
|
||||
create or replace trigger dc_questions_biu
|
||||
before insert or update
|
||||
on dc_questions
|
||||
for each row
|
||||
begin
|
||||
if inserting then
|
||||
:new.row_version := 1;
|
||||
elsif updating then
|
||||
:new.row_version := NVL(:old.row_version, 0) + 1;
|
||||
end if;
|
||||
if inserting then
|
||||
:new.created := sysdate;
|
||||
:new.created_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
|
||||
end if;
|
||||
:new.updated := sysdate;
|
||||
:new.updated_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
|
||||
end dc_questions_biu;
|
||||
/
|
||||
|
||||
|
||||
create or replace trigger dc_reference_documents_biu
|
||||
before insert or update
|
||||
on dc_reference_documents
|
||||
for each row
|
||||
begin
|
||||
if inserting then
|
||||
:new.row_version := 1;
|
||||
elsif updating then
|
||||
:new.row_version := NVL(:old.row_version, 0) + 1;
|
||||
end if;
|
||||
if inserting then
|
||||
:new.created := sysdate;
|
||||
:new.created_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
|
||||
end if;
|
||||
:new.updated := sysdate;
|
||||
:new.updated_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
|
||||
end dc_reference_documents_biu;
|
||||
/
|
||||
|
||||
|
||||
create or replace trigger dc_projects_biu
|
||||
before insert or update
|
||||
on dc_projects
|
||||
for each row
|
||||
begin
|
||||
if inserting then
|
||||
:new.row_version := 1;
|
||||
elsif updating then
|
||||
:new.row_version := NVL(:old.row_version, 0) + 1;
|
||||
end if;
|
||||
if inserting then
|
||||
:new.created := sysdate;
|
||||
:new.created_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
|
||||
end if;
|
||||
:new.updated := sysdate;
|
||||
:new.updated_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
|
||||
end dc_projects_biu;
|
||||
/
|
||||
|
||||
|
||||
create or replace trigger dc_project_documents_biu
|
||||
before insert or update
|
||||
on dc_project_documents
|
||||
for each row
|
||||
begin
|
||||
if inserting then
|
||||
:new.row_version := 1;
|
||||
elsif updating then
|
||||
:new.row_version := NVL(:old.row_version, 0) + 1;
|
||||
end if;
|
||||
if inserting then
|
||||
:new.created := sysdate;
|
||||
:new.created_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
|
||||
end if;
|
||||
:new.updated := sysdate;
|
||||
:new.updated_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
|
||||
end dc_project_documents_biu;
|
||||
/
|
||||
|
||||
|
||||
create or replace trigger dc_results_biu
|
||||
before insert or update
|
||||
on dc_results
|
||||
for each row
|
||||
begin
|
||||
if inserting then
|
||||
:new.row_version := 1;
|
||||
elsif updating then
|
||||
:new.row_version := NVL(:old.row_version, 0) + 1;
|
||||
end if;
|
||||
if inserting then
|
||||
:new.created := sysdate;
|
||||
:new.created_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
|
||||
end if;
|
||||
:new.updated := sysdate;
|
||||
:new.updated_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
|
||||
end dc_results_biu;
|
||||
/
|
||||
|
||||
|
||||
-- Generated by Quick SQL 1.2.9 13.4.2026, 17:07:42
|
||||
|
||||
/*
|
||||
users
|
||||
id number generated by default on null as identity constraint users_id_pk primary key
|
||||
username vc50 /nn /unique
|
||||
email vc100 /nn /unique
|
||||
password_hash vc255
|
||||
role vc20
|
||||
is_active num /default 1
|
||||
created_at date /default sysdate
|
||||
updated_at date /default sysdate
|
||||
|
||||
user_roles
|
||||
id number generated by default on null as identity constraint user_roles_id_pk primary key
|
||||
role_name vc20 /nn /unique
|
||||
description clob
|
||||
|
||||
question_catalogs
|
||||
id number generated by default on null as identity constraint question_catalogs_id_pk primary key
|
||||
name vc100 /nn
|
||||
description clob
|
||||
created_by num /references users
|
||||
created_at date /default sysdate
|
||||
updated_at date /default sysdate
|
||||
|
||||
question_categories
|
||||
id number generated by default on null as identity constraint question_categories_id_pk primary key
|
||||
catalog_id num /references question_catalogs
|
||||
name vc100 /nn
|
||||
description clob
|
||||
|
||||
questions
|
||||
id number generated by default on null as identity constraint questions_id_pk primary key
|
||||
category_id num /references question_categories
|
||||
question_text clob /nn
|
||||
evaluation_type vc20
|
||||
threshold num
|
||||
result_handling vc20
|
||||
example_0_percent clob
|
||||
example_100_percent clob
|
||||
|
||||
reference_documents
|
||||
id number generated by default on null as identity constraint reference_documents_id_pk primary key
|
||||
name vc100 /nn
|
||||
description clob
|
||||
file_path vc500
|
||||
uploaded_by num /references users
|
||||
upload_date date /default sysdate
|
||||
mime_type vc100
|
||||
|
||||
projects
|
||||
id number generated by default on null as identity constraint projects_id_pk primary key
|
||||
name vc100 /nn
|
||||
description clob
|
||||
catalog_id num /references question_catalogs
|
||||
created_by num /references users
|
||||
created_at date /default sysdate
|
||||
status vc20 /check PENDING, IN_PROGRESS, COMPLETED
|
||||
progress num /default 0
|
||||
completed_at date
|
||||
notification_email vc100
|
||||
|
||||
project_documents
|
||||
id number generated by default on null as identity constraint project_documents_id_pk primary key
|
||||
project_id num /references projects
|
||||
original_file blob
|
||||
translated_file blob
|
||||
filename vc255 /nn
|
||||
mime_type vc100
|
||||
uploaded_at date /default sysdate
|
||||
|
||||
results
|
||||
id number generated by default on null as identity constraint results_id_pk primary key
|
||||
project_id num /references projects
|
||||
question_id num /references questions
|
||||
doc_id num /references project_documents
|
||||
answer clob
|
||||
score num
|
||||
result_type vc20 /check OK, UNKLAR, NOK
|
||||
deviation num /default 0
|
||||
warning num /default 0
|
||||
comment clob
|
||||
evaluated_at date /default sysdate
|
||||
|
||||
|
||||
|
||||
Non-default options:
|
||||
# settings = {"apex":"Y","auditcols":"Y","db":"23","drop":"Y","prefix":"Dc","pk":"IDENTITY","rowversion":"Y"}
|
||||
|
||||
*/
|
||||
498
Scripts/Fragenkatalog-AGB anlegen.sql
Normal file
498
Scripts/Fragenkatalog-AGB anlegen.sql
Normal file
@@ -0,0 +1,498 @@
|
||||
DECLARE
|
||||
-- IDs
|
||||
v_doc_type_id dc_document_types.id%TYPE;
|
||||
v_catalog_id dc_question_catalogs.id%TYPE;
|
||||
v_cat1_id dc_question_categories.id%TYPE;
|
||||
v_cat2_id dc_question_categories.id%TYPE;
|
||||
v_cat3_id dc_question_categories.id%TYPE;
|
||||
v_cat4_id dc_question_categories.id%TYPE;
|
||||
v_cat5_id dc_question_categories.id%TYPE;
|
||||
v_cat6_id dc_question_categories.id%TYPE;
|
||||
|
||||
BEGIN
|
||||
|
||||
-- ============================================================
|
||||
-- 1. DOKUMENTENTYP
|
||||
-- ============================================================
|
||||
INSERT INTO dc_document_types (
|
||||
name, description,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
'AGB',
|
||||
'Allgemeine Geschäftsbedingungen – vorformulierte Vertragsbedingungen gemäß §305 BGB',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
)
|
||||
RETURNING id INTO v_doc_type_id;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 2. FRAGENKATALOG
|
||||
-- ============================================================
|
||||
INSERT INTO dc_question_catalogs (
|
||||
created_by_user, based_on_reference, document_type_id,
|
||||
name, description,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
NULL, NULL, v_doc_type_id,
|
||||
'AGB-Prüfkatalog (BGB §305–310)',
|
||||
'Prüfung von Allgemeinen Geschäftsbedingungen auf Konformität mit dem deutschen AGB-Recht. '
|
||||
|| 'Grundlage: §§305–310 BGB sowie aktuelle BGH-Rechtsprechung.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
)
|
||||
RETURNING id INTO v_catalog_id;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 3. KATEGORIEN
|
||||
-- ============================================================
|
||||
INSERT INTO dc_question_categories (
|
||||
catalog_id, name, description,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_catalog_id,
|
||||
'Einbeziehung & Transparenz',
|
||||
'Prüfung ob die AGB wirksam einbezogen wurden und dem Transparenzgebot (§307 Abs. 1 S. 2 BGB) entsprechen.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
)
|
||||
RETURNING id INTO v_cat1_id;
|
||||
|
||||
INSERT INTO dc_question_categories (
|
||||
catalog_id, name, description,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_catalog_id,
|
||||
'Haftung & Gewährleistung',
|
||||
'Prüfung von Haftungsbeschränkungen und Gewährleistungsregelungen auf Zulässigkeit nach §309 Nr. 7/8 BGB.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
)
|
||||
RETURNING id INTO v_cat2_id;
|
||||
|
||||
INSERT INTO dc_question_categories (
|
||||
catalog_id, name, description,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_catalog_id,
|
||||
'Zahlungs- & Lieferbedingungen',
|
||||
'Prüfung von Zahlungsfristen, Verzugszinsen, Lieferfristen und Eigentumsvorbehalt.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
)
|
||||
RETURNING id INTO v_cat3_id;
|
||||
|
||||
INSERT INTO dc_question_categories (
|
||||
catalog_id, name, description,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_catalog_id,
|
||||
'Laufzeit & Kündigung',
|
||||
'Prüfung von Vertragslaufzeiten, Kündigungsfristen und automatischer Verlängerung gemäß §309 Nr. 9 BGB.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
)
|
||||
RETURNING id INTO v_cat4_id;
|
||||
|
||||
INSERT INTO dc_question_categories (
|
||||
catalog_id, name, description,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_catalog_id,
|
||||
'Datenschutz & Compliance',
|
||||
'Prüfung der Datenschutzklauseln auf Konformität mit DSGVO und BDSG.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
)
|
||||
RETURNING id INTO v_cat5_id;
|
||||
|
||||
INSERT INTO dc_question_categories (
|
||||
catalog_id, name, description,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_catalog_id,
|
||||
'Verbotene Klauseln §307–309 BGB',
|
||||
'Prüfung auf explizit verbotene oder unangemessen benachteiligende Klauseln.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
)
|
||||
RETURNING id INTO v_cat6_id;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 4. FRAGEN
|
||||
-- ============================================================
|
||||
|
||||
------------------------------------------------------------
|
||||
-- KATEGORIE 1: Einbeziehung & Transparenz
|
||||
------------------------------------------------------------
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat1_id,
|
||||
'Wird dem Vertragspartner vor oder bei Vertragsschluss ausdrücklich auf die Geltung der AGB hingewiesen (§305 Abs. 2 Nr. 1 BGB)?',
|
||||
'SCORE', 50, 'ABWEICHUNG',
|
||||
'Kein Hinweis auf AGB vorhanden; AGB nur im Footer der Website verlinkt ohne ausdrücklichen Hinweis beim Vertragsschluss.',
|
||||
'AGB sind ausdrücklich im Vertragsangebot genannt, z. B.: „Es gelten unsere AGB in der jeweils gültigen Fassung, abrufbar unter [URL]."',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat1_id,
|
||||
'Wird dem Vertragspartner die zumutbare Möglichkeit verschafft, vom Inhalt der AGB Kenntnis zu nehmen (§305 Abs. 2 Nr. 2 BGB)?',
|
||||
'SCORE', 50, 'ABWEICHUNG',
|
||||
'AGB nur auf Anfrage erhältlich; kein direkter Link, kein Aushang, keine Übergabe.',
|
||||
'Vollständiger Text der AGB ist abrufbar unter einem direkt verlinkten URL oder wird als Anhang mitgesendet.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat1_id,
|
||||
'Sind alle Klauseln klar, verständlich und ohne Fachsprache formuliert (Transparenzgebot §307 Abs. 1 S. 2 BGB)?',
|
||||
'SCORE', 60, 'ABWEICHUNG',
|
||||
'Klauseln enthalten unverständliche Fachbegriffe ohne Erläuterung; Satzgefüge sind verschachtelt und mehrdeutig.',
|
||||
'Klauseln sind in einfacher Sprache formuliert, kurze Sätze, wichtige Begriffe werden erklärt.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat1_id,
|
||||
'Enthält das Dokument eine Versionsnummer oder ein Datum, das die aktuell gültige Fassung eindeutig kennzeichnet?',
|
||||
'SCORE', 70, 'HINWEIS',
|
||||
'Kein Datum, keine Versionsnummer vorhanden.',
|
||||
'Fußzeile enthält: „Stand: Januar 2025, Version 3.1"',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat1_id,
|
||||
'Wird geregelt, wie Änderungen der AGB dem Vertragspartner mitgeteilt werden und wann diese wirksam werden?',
|
||||
'SCORE', 50, 'ABWEICHUNG',
|
||||
'Keine Regelung zu AGB-Änderungen; stillschweigende Zustimmung durch Weiterbenutzung des Dienstes wird unterstellt.',
|
||||
'Änderungen werden per E-Mail angekündigt mit einer Einspruchsfrist von mindestens 6 Wochen; Widerspruchsrecht ist ausdrücklich genannt.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
-- KATEGORIE 2: Haftung & Gewährleistung
|
||||
------------------------------------------------------------
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat2_id,
|
||||
'Wird die Haftung für Vorsatz und grobe Fahrlässigkeit vollständig ausgeschlossen (§309 Nr. 7a BGB – unzulässig)?',
|
||||
'SCORE', 50, 'ABWEICHUNG',
|
||||
'„Jegliche Haftung, einschließlich Vorsatz und grober Fahrlässigkeit, wird hiermit ausgeschlossen."',
|
||||
'Haftungsausschluss gilt nur für leichte Fahrlässigkeit; Vorsatz und grobe Fahrlässigkeit bleiben ausdrücklich unberührt.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat2_id,
|
||||
'Wird die Haftung für Körper- und Gesundheitsschäden ausgeschlossen (§309 Nr. 7b BGB – unzulässig)?',
|
||||
'SCORE', 50, 'ABWEICHUNG',
|
||||
'„Eine Haftung für Personen- und Sachschäden jeglicher Art ist ausgeschlossen."',
|
||||
'Haftung für Personenschäden ist explizit ausgenommen: „Die Haftungsbeschränkung gilt nicht für Schäden an Leben, Körper oder Gesundheit."',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat2_id,
|
||||
'Werden gesetzliche Gewährleistungsrechte des Käufers vollständig ausgeschlossen (§309 Nr. 8b BGB – unzulässig bei Neuwaren)?',
|
||||
'SCORE', 50, 'ABWEICHUNG',
|
||||
'„Gewährleistungsansprüche sind vollständig ausgeschlossen."',
|
||||
'Gewährleistungsfrist wird auf das gesetzliche Minimum (2 Jahre bei Neuwaren) gesetzt; vollständiger Ausschluss ist nicht vorhanden.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat2_id,
|
||||
'Wird die Haftung für die Verletzung wesentlicher Vertragspflichten (Kardinalpflichten) unangemessen eingeschränkt?',
|
||||
'SCORE', 50, 'ABWEICHUNG',
|
||||
'„Jegliche Haftung des Anbieters ist auf 50 € begrenzt, unabhängig von der Art der Pflichtverletzung."',
|
||||
'Haftungsbegrenzung gilt nicht für die Verletzung wesentlicher Vertragspflichten; bei deren Verletzung haftet der Anbieter auf den typischerweise vorhersehbaren Schaden.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
-- KATEGORIE 3: Zahlungs- & Lieferbedingungen
|
||||
------------------------------------------------------------
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat3_id,
|
||||
'Sind die vereinbarten Zahlungsfristen klar und eindeutig definiert?',
|
||||
'SCORE', 60, 'HINWEIS',
|
||||
'Keine Angabe zur Zahlungsfrist; Fälligkeit unklar.',
|
||||
'Zahlung ist innerhalb von 14 Tagen nach Rechnungsstellung ohne Abzug fällig.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat3_id,
|
||||
'Werden unangemessen hohe Verzugszinsen oder Vertragsstrafen bei Zahlungsverzug festgelegt (§309 Nr. 6 BGB)?',
|
||||
'SCORE', 50, 'ABWEICHUNG',
|
||||
'„Bei Zahlungsverzug werden Zinsen in Höhe von 15 % p. a. sowie eine Bearbeitungsgebühr von 50 € pro Mahnung berechnet."',
|
||||
'Verzugszinsen entsprechen dem gesetzlichen Zinssatz (§288 BGB); keine unverhältnismäßigen Mahngebühren.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat3_id,
|
||||
'Sind Lieferfristen verbindlich oder werden nur unverbindliche Richtwerte ohne Konsequenzen bei Überschreitung angegeben?',
|
||||
'SCORE', 50, 'HINWEIS',
|
||||
'„Lieferzeiten sind unverbindlich; eine Überschreitung berechtigt nicht zur Stornierung."',
|
||||
'Lieferfristen sind verbindlich; bei Überschreitung um mehr als 14 Tage hat der Käufer ein Rücktrittsrecht.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat3_id,
|
||||
'Wird ein erweiterter Eigentumsvorbehalt vereinbart, der über die gelieferte Ware hinausgeht (z. B. Kontokorrentvorbehalt)?',
|
||||
'SCORE', 50, 'HINWEIS',
|
||||
'„Das Eigentum verbleibt beim Verkäufer bis zur vollständigen Bezahlung aller gegenwärtigen und zukünftigen Forderungen aus der Geschäftsbeziehung."',
|
||||
'Einfacher Eigentumsvorbehalt: Eigentum geht nach vollständiger Bezahlung des jeweiligen Kaufpreises über.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
-- KATEGORIE 4: Laufzeit & Kündigung
|
||||
------------------------------------------------------------
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat4_id,
|
||||
'Beträgt die Erstlaufzeit bei Dauerschuldverhältnissen mehr als zwei Jahre (§309 Nr. 9a BGB – unzulässig)?',
|
||||
'SCORE', 50, 'ABWEICHUNG',
|
||||
'„Der Vertrag hat eine Mindestlaufzeit von 36 Monaten."',
|
||||
'Mindestlaufzeit beträgt maximal 24 Monate.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat4_id,
|
||||
'Verlängert sich der Vertrag bei Nichtkündigung um mehr als ein Jahr automatisch (§309 Nr. 9b BGB – unzulässig)?',
|
||||
'SCORE', 50, 'ABWEICHUNG',
|
||||
'„Wird der Vertrag nicht 3 Monate vor Ablauf gekündigt, verlängert er sich automatisch um 24 Monate."',
|
||||
'Automatische Verlängerung beträgt maximal 12 Monate; Kündigungsfrist höchstens 3 Monate.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat4_id,
|
||||
'Beträgt die Kündigungsfrist für den Vertragspartner mehr als drei Monate (§309 Nr. 9c BGB – unzulässig)?',
|
||||
'SCORE', 50, 'ABWEICHUNG',
|
||||
'„Der Vertrag kann mit einer Frist von 6 Monaten zum Vertragsende gekündigt werden."',
|
||||
'Kündigungsfrist beträgt maximal 3 Monate zum Ende der Vertragslaufzeit.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat4_id,
|
||||
'Ist das Recht zur außerordentlichen Kündigung aus wichtigem Grund explizit oder durch Generalausschluss eingeschränkt?',
|
||||
'SCORE', 50, 'ABWEICHUNG',
|
||||
'„Eine Kündigung ist ausschließlich zum regulären Vertragsende möglich."',
|
||||
'Das Recht zur fristlosen Kündigung aus wichtigem Grund gemäß §314 BGB bleibt ausdrücklich unberührt.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
-- KATEGORIE 5: Datenschutz & Compliance
|
||||
------------------------------------------------------------
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat5_id,
|
||||
'Wird eine Rechtsgrundlage für die Verarbeitung personenbezogener Daten gemäß Art. 6 DSGVO genannt?',
|
||||
'SCORE', 70, 'ABWEICHUNG',
|
||||
'Keine Angabe zur Rechtsgrundlage der Datenverarbeitung in den AGB.',
|
||||
'Datenverarbeitung erfolgt auf Grundlage von Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung); weitere Grundlagen sind aufgeführt.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat5_id,
|
||||
'Wird der Verantwortliche gemäß Art. 4 Nr. 7 DSGVO mit vollständigen Kontaktdaten genannt?',
|
||||
'SCORE', 70, 'ABWEICHUNG',
|
||||
'Kein Verantwortlicher genannt; nur allgemeiner Firmenname ohne Adresse.',
|
||||
'Name, Anschrift, E-Mail und ggf. Datenschutzbeauftragter sind vollständig angegeben.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat5_id,
|
||||
'Werden die Betroffenenrechte (Auskunft, Löschung, Berichtigung, Widerspruch) gemäß Art. 15–21 DSGVO kommuniziert?',
|
||||
'SCORE', 60, 'ABWEICHUNG',
|
||||
'Kein Hinweis auf Betroffenenrechte.',
|
||||
'Alle Betroffenenrechte sind aufgeführt mit Hinweis auf den Kontaktweg zur Ausübung.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat5_id,
|
||||
'Wird eine Weitergabe von Daten an Dritte oder eine Übermittlung in Drittländer außerhalb der EU geregelt?',
|
||||
'SCORE', 60, 'HINWEIS',
|
||||
'Keine Regelung zur Datenweitergabe; Drittlandübermittlung unerwähnt.',
|
||||
'Datenweitergabe an Dritte ist aufgelistet; Drittlandübermittlung erfolgt nur auf Basis von Standardvertragsklauseln gemäß Art. 46 DSGVO.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
-- KATEGORIE 6: Verbotene Klauseln §307–309 BGB
|
||||
------------------------------------------------------------
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat6_id,
|
||||
'Enthält die AGB eine Klausel, die dem Verwender einseitig das Recht einräumt, vereinbarte Leistungen zu ändern (§308 Nr. 4 BGB)?',
|
||||
'SCORE', 50, 'ABWEICHUNG',
|
||||
'„Wir behalten uns vor, unsere Leistungen jederzeit und ohne Ankündigung zu ändern oder einzustellen."',
|
||||
'Leistungsänderungen sind nur aus sachlich gerechtfertigtem Grund zulässig; dem Kunden steht ein Sonderkündigungsrecht zu.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat6_id,
|
||||
'Wird dem Vertragspartner das Recht zur Aufrechnung mit unbestrittenen oder rechtskräftig festgestellten Forderungen entzogen (§309 Nr. 3 BGB)?',
|
||||
'SCORE', 50, 'ABWEICHUNG',
|
||||
'„Eine Aufrechnung gegen Forderungen des Anbieters ist ausgeschlossen."',
|
||||
'Aufrechnung ist zulässig mit unbestrittenen oder rechtskräftig festgestellten Gegenforderungen.',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat6_id,
|
||||
'Wird der Gerichtsstand ausschließlich auf den Sitz des Verwenders festgelegt, obwohl der Vertragspartner Verbraucher ist (§29c ZPO)?',
|
||||
'SCORE', 50, 'ABWEICHUNG',
|
||||
'„Ausschließlicher Gerichtsstand für alle Streitigkeiten ist München."',
|
||||
'Gerichtsstandvereinbarung gilt nur für Kaufleute; bei Verbrauchern gilt der gesetzliche Gerichtsstand (Wohnort des Verbrauchers).',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat6_id,
|
||||
'Wird die Anwendung deutschen Rechts ausdrücklich geregelt, und werden verbraucherschützende Vorschriften des Herkunftslandes des Kunden ausgehebelt?',
|
||||
'SCORE', 50, 'HINWEIS',
|
||||
'„Es gilt ausschließlich deutsches Recht unter vollständigem Ausschluss des UN-Kaufrechts sowie aller ausländischen Schutzvorschriften."',
|
||||
'Rechtswahl gilt für Kaufleute; Verbraucher genießen den Schutz der zwingenden Vorschriften ihres Heimatlandes (Art. 6 Rom-I-VO).',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
INSERT INTO dc_questions (
|
||||
category_id, question_text, evaluation_type, threshold, result_handling,
|
||||
example_0_percent, example_100_percent,
|
||||
row_version, created, created_by, updated, updated_by
|
||||
) VALUES (
|
||||
v_cat6_id,
|
||||
'Enthält die AGB eine salvatorische Klausel für den Fall der Unwirksamkeit einzelner Bestimmungen?',
|
||||
'SCORE', 70, 'HINWEIS',
|
||||
'Keine salvatorische Klausel vorhanden; Unwirksamkeit einzelner Klauseln könnte gesamte AGB gefährden.',
|
||||
'„Sollten einzelne Bestimmungen dieser AGB unwirksam sein, bleibt die Wirksamkeit der übrigen Bestimmungen unberührt."',
|
||||
1, SYSDATE, 'SYSTEM', SYSDATE, 'SYSTEM'
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
|
||||
DBMS_OUTPUT.PUT_LINE('Erfolgreich angelegt:');
|
||||
DBMS_OUTPUT.PUT_LINE(' Dokumententyp-ID : ' || v_doc_type_id);
|
||||
DBMS_OUTPUT.PUT_LINE(' Katalog-ID : ' || v_catalog_id);
|
||||
DBMS_OUTPUT.PUT_LINE(' Kategorie 1 ID : ' || v_cat1_id || ' (Einbeziehung & Transparenz)');
|
||||
DBMS_OUTPUT.PUT_LINE(' Kategorie 2 ID : ' || v_cat2_id || ' (Haftung & Gewährleistung)');
|
||||
DBMS_OUTPUT.PUT_LINE(' Kategorie 3 ID : ' || v_cat3_id || ' (Zahlungs- & Lieferbedingungen)');
|
||||
DBMS_OUTPUT.PUT_LINE(' Kategorie 4 ID : ' || v_cat4_id || ' (Laufzeit & Kündigung)');
|
||||
DBMS_OUTPUT.PUT_LINE(' Kategorie 5 ID : ' || v_cat5_id || ' (Datenschutz & Compliance)');
|
||||
DBMS_OUTPUT.PUT_LINE(' Kategorie 6 ID : ' || v_cat6_id || ' (Verbotene Klauseln §307–309 BGB)');
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
ROLLBACK;
|
||||
DBMS_OUTPUT.PUT_LINE('FEHLER: ' || SQLERRM);
|
||||
RAISE;
|
||||
END;
|
||||
/
|
||||
737
Scripts/ORDS REST Services.sql
Normal file
737
Scripts/ORDS REST Services.sql
Normal file
@@ -0,0 +1,737 @@
|
||||
-- =============================================================================
|
||||
-- ORDS REST Services: Dokumenten-Check
|
||||
-- Modul: frigosped.dc
|
||||
-- Base-Path: /api/dc/
|
||||
-- =============================================================================
|
||||
-- Voraussetzungen:
|
||||
-- - Oracle Database 23c mit installiertem ORDS (>= 23.x)
|
||||
-- - Script wird als Schema-Owner ausgeführt (hat ORDS_METADATA-Rolle)
|
||||
-- - Datenbankschema bereits angelegt via "Datenmodell DocumentCheck.sql"
|
||||
--
|
||||
-- Deployment:
|
||||
-- sqlplus user/pass@db @"Scripts/ORDS REST Services.sql"
|
||||
--
|
||||
-- Das Skript ist idempotent: bestehende Modul-Definition wird gelöscht und
|
||||
-- neu erstellt. Bestehende Daten bleiben unberührt.
|
||||
--
|
||||
-- Endpunkt-Übersicht:
|
||||
-- 1. GET /api/dc/catalogs/ Alle Fragenkataloge
|
||||
-- 2. GET /api/dc/catalogs/:id/questions/ Fragen eines Katalogs (flach)
|
||||
-- 3. GET /api/dc/projects/:id Projektdetail
|
||||
-- 4. POST /api/dc/projects/:id/start → IN_PROGRESS
|
||||
-- 5. PUT /api/dc/projects/:id/progress Fortschritt aktualisieren
|
||||
-- 6. POST /api/dc/projects/:id/complete → COMPLETED
|
||||
-- 7. GET /api/dc/projects/:id/documents/ Dokument-Liste (kein BLOB)
|
||||
-- 8. GET /api/dc/documents/:id/file BLOB-Download
|
||||
-- 9. PUT /api/dc/documents/:id/texts OCR-Text + Übersetzung schreiben
|
||||
-- 10. POST /api/dc/projects/:id/results Prüfergebnis einfügen
|
||||
-- 11. DELETE /api/dc/projects/:id/results Alle Ergebnisse löschen
|
||||
-- =============================================================================
|
||||
|
||||
-- =============================================================================
|
||||
-- BLOCK 1: Modul, Templates und Handler
|
||||
-- =============================================================================
|
||||
BEGIN
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Idempotenz: Modul löschen falls vorhanden
|
||||
-- -------------------------------------------------------------------------
|
||||
BEGIN
|
||||
ORDS.DELETE_MODULE(p_module_name => 'frigosped.dc');
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN NULL;
|
||||
END;
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Modul definieren
|
||||
-- -------------------------------------------------------------------------
|
||||
ORDS.DEFINE_MODULE(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_base_path => '/api/dc/',
|
||||
p_items_per_page => 0, -- Pagination deaktiviert (Quarkus steuert selbst)
|
||||
p_status => 'PUBLISHED',
|
||||
p_comments => 'Dokumenten-Check Backend-API fuer den Quarkus-Verarbeitungsserver'
|
||||
);
|
||||
|
||||
|
||||
-- ===========================================================================
|
||||
-- TEMPLATE 1: catalogs/
|
||||
-- ===========================================================================
|
||||
ORDS.DEFINE_TEMPLATE(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'catalogs/',
|
||||
p_priority => 0,
|
||||
p_etag_type => 'HASH',
|
||||
p_comments => 'Alle Fragenkataloge mit Dokumenttyp-Name'
|
||||
);
|
||||
|
||||
-- 1. GET /api/dc/catalogs/
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'catalogs/',
|
||||
p_method => 'GET',
|
||||
p_source_type => ORDS.SOURCE_TYPE_COLLECTION_FEED,
|
||||
p_items_per_page => 0,
|
||||
p_comments => 'Gibt alle Kataloge mit Dokumenttyp-Name zurueck',
|
||||
p_source => q'[
|
||||
SELECT
|
||||
c.id,
|
||||
c.name,
|
||||
c.description,
|
||||
c.document_type_id,
|
||||
dt.name AS document_type_name,
|
||||
c.row_version,
|
||||
c.created,
|
||||
c.updated
|
||||
FROM dc_question_catalogs c
|
||||
JOIN dc_document_types dt ON dt.id = c.document_type_id
|
||||
ORDER BY c.name
|
||||
]'
|
||||
);
|
||||
|
||||
|
||||
-- ===========================================================================
|
||||
-- TEMPLATE 2: catalogs/:catalog_id/questions/
|
||||
-- ===========================================================================
|
||||
ORDS.DEFINE_TEMPLATE(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'catalogs/:catalog_id/questions/',
|
||||
p_priority => 0,
|
||||
p_etag_type => 'HASH',
|
||||
p_comments => 'Alle Fragen eines Katalogs (flach mit Kategorie-Info)'
|
||||
);
|
||||
|
||||
-- 2. GET /api/dc/catalogs/:catalog_id/questions/
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'catalogs/:catalog_id/questions/',
|
||||
p_method => 'GET',
|
||||
p_source_type => ORDS.SOURCE_TYPE_COLLECTION_FEED,
|
||||
p_items_per_page => 0,
|
||||
p_comments => 'Flacher Join: Kategorie + Frage-Spalten, nach Kategorie/ID sortiert',
|
||||
p_source => q'[
|
||||
SELECT
|
||||
q.id AS question_id,
|
||||
cat.id AS category_id,
|
||||
cat.name AS category_name,
|
||||
cat.description AS category_description,
|
||||
q.question_text,
|
||||
q.evaluation_type,
|
||||
q.threshold,
|
||||
q.result_handling,
|
||||
q.example_0_percent,
|
||||
q.example_100_percent,
|
||||
q.row_version
|
||||
FROM dc_questions q
|
||||
JOIN dc_question_categories cat ON cat.id = q.category_id
|
||||
WHERE cat.catalog_id = :catalog_id
|
||||
ORDER BY cat.name, q.id
|
||||
]'
|
||||
);
|
||||
|
||||
|
||||
-- ===========================================================================
|
||||
-- TEMPLATE 3: projects/:project_id (kein Trailing-Slash = Einzel-Ressource)
|
||||
-- ===========================================================================
|
||||
ORDS.DEFINE_TEMPLATE(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'projects/:project_id',
|
||||
p_priority => 0,
|
||||
p_etag_type => 'HASH',
|
||||
p_comments => 'Einzelnes Projekt'
|
||||
);
|
||||
|
||||
-- 3. GET /api/dc/projects/:project_id
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'projects/:project_id',
|
||||
p_method => 'GET',
|
||||
p_source_type => ORDS.SOURCE_TYPE_COLLECTION_ITEM,
|
||||
p_items_per_page => 1,
|
||||
p_comments => 'Projektdetail als einzelnes JSON-Objekt; 404 wenn nicht gefunden',
|
||||
p_source => q'[
|
||||
SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
p.description,
|
||||
p.catalog_id,
|
||||
p.created_by_user,
|
||||
p.status,
|
||||
p.progress,
|
||||
p.completed_at,
|
||||
p.notification_email,
|
||||
p.row_version,
|
||||
p.created,
|
||||
p.updated
|
||||
FROM dc_projects p
|
||||
WHERE p.id = :project_id
|
||||
]'
|
||||
);
|
||||
|
||||
|
||||
-- ===========================================================================
|
||||
-- TEMPLATE 4: projects/:project_id/start
|
||||
-- ===========================================================================
|
||||
ORDS.DEFINE_TEMPLATE(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'projects/:project_id/start',
|
||||
p_priority => 0,
|
||||
p_comments => 'Projekt-Status auf IN_PROGRESS setzen'
|
||||
);
|
||||
|
||||
-- 4. POST /api/dc/projects/:project_id/start
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'projects/:project_id/start',
|
||||
p_method => 'POST',
|
||||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||||
p_comments => '200 OK; 409 wenn Status nicht PENDING; 404 wenn Projekt fehlt',
|
||||
p_source => q'[
|
||||
DECLARE
|
||||
v_status dc_projects.status%TYPE;
|
||||
BEGIN
|
||||
SELECT status
|
||||
INTO v_status
|
||||
FROM dc_projects
|
||||
WHERE id = :project_id
|
||||
FOR UPDATE NOWAIT;
|
||||
|
||||
IF v_status != 'PENDING' THEN
|
||||
:status_code := 409;
|
||||
HTP.P('{"error":"Projekt ist nicht im Status PENDING",'
|
||||
|| '"current_status":"' || v_status || '"}');
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
UPDATE dc_projects
|
||||
SET status = 'IN_PROGRESS',
|
||||
progress = 0
|
||||
WHERE id = :project_id;
|
||||
|
||||
:status_code := 200;
|
||||
HTP.P('{"project_id":' || :project_id
|
||||
|| ',"status":"IN_PROGRESS"}');
|
||||
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN
|
||||
:status_code := 404;
|
||||
HTP.P('{"error":"Projekt nicht gefunden","project_id":' || :project_id || '}');
|
||||
WHEN OTHERS THEN
|
||||
ROLLBACK;
|
||||
:status_code := 500;
|
||||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||||
END;
|
||||
]'
|
||||
);
|
||||
|
||||
|
||||
-- ===========================================================================
|
||||
-- TEMPLATE 5: projects/:project_id/progress
|
||||
-- ===========================================================================
|
||||
ORDS.DEFINE_TEMPLATE(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'projects/:project_id/progress',
|
||||
p_priority => 0,
|
||||
p_comments => 'Verarbeitungsfortschritt aktualisieren'
|
||||
);
|
||||
|
||||
-- 5. PUT /api/dc/projects/:project_id/progress
|
||||
-- Body: {"progress": 45}
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'projects/:project_id/progress',
|
||||
p_method => 'PUT',
|
||||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||||
p_comments => 'Setzt progress (0-100); 400 bei ungueltigem Wert',
|
||||
p_source => q'[
|
||||
DECLARE
|
||||
v_progress NUMBER;
|
||||
v_body CLOB;
|
||||
BEGIN
|
||||
v_body := :body_text;
|
||||
v_progress := TO_NUMBER(JSON_VALUE(v_body, '$.progress'));
|
||||
|
||||
IF v_progress IS NULL OR v_progress < 0 OR v_progress > 100 THEN
|
||||
:status_code := 400;
|
||||
HTP.P('{"error":"progress muss eine Zahl zwischen 0 und 100 sein"}');
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
UPDATE dc_projects
|
||||
SET progress = v_progress
|
||||
WHERE id = :project_id;
|
||||
|
||||
IF SQL%ROWCOUNT = 0 THEN
|
||||
:status_code := 404;
|
||||
HTP.P('{"error":"Projekt nicht gefunden","project_id":' || :project_id || '}');
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
:status_code := 200;
|
||||
HTP.P('{"project_id":' || :project_id
|
||||
|| ',"progress":' || v_progress || '}');
|
||||
|
||||
EXCEPTION
|
||||
WHEN VALUE_ERROR THEN
|
||||
:status_code := 400;
|
||||
HTP.P('{"error":"progress muss eine gueltige Zahl sein"}');
|
||||
WHEN OTHERS THEN
|
||||
ROLLBACK;
|
||||
:status_code := 500;
|
||||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||||
END;
|
||||
]'
|
||||
);
|
||||
|
||||
|
||||
-- ===========================================================================
|
||||
-- TEMPLATE 6: projects/:project_id/complete
|
||||
-- ===========================================================================
|
||||
ORDS.DEFINE_TEMPLATE(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'projects/:project_id/complete',
|
||||
p_priority => 0,
|
||||
p_comments => 'Projekt auf COMPLETED setzen'
|
||||
);
|
||||
|
||||
-- 6. POST /api/dc/projects/:project_id/complete
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'projects/:project_id/complete',
|
||||
p_method => 'POST',
|
||||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||||
p_comments => 'Setzt status=COMPLETED, completed_at=SYSDATE, progress=100',
|
||||
p_source => q'[
|
||||
DECLARE
|
||||
v_status dc_projects.status%TYPE;
|
||||
v_now DATE := SYSDATE;
|
||||
BEGIN
|
||||
SELECT status
|
||||
INTO v_status
|
||||
FROM dc_projects
|
||||
WHERE id = :project_id
|
||||
FOR UPDATE NOWAIT;
|
||||
|
||||
IF v_status = 'COMPLETED' THEN
|
||||
:status_code := 409;
|
||||
HTP.P('{"error":"Projekt ist bereits COMPLETED","project_id":' || :project_id || '}');
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
UPDATE dc_projects
|
||||
SET status = 'COMPLETED',
|
||||
completed_at = v_now,
|
||||
progress = 100
|
||||
WHERE id = :project_id;
|
||||
|
||||
:status_code := 200;
|
||||
HTP.P('{"project_id":' || :project_id
|
||||
|| ',"status":"COMPLETED"'
|
||||
|| ',"completed_at":"' || TO_CHAR(v_now, 'YYYY-MM-DD"T"HH24:MI:SS') || '"'
|
||||
|| '}');
|
||||
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN
|
||||
:status_code := 404;
|
||||
HTP.P('{"error":"Projekt nicht gefunden","project_id":' || :project_id || '}');
|
||||
WHEN OTHERS THEN
|
||||
ROLLBACK;
|
||||
:status_code := 500;
|
||||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||||
END;
|
||||
]'
|
||||
);
|
||||
|
||||
|
||||
-- ===========================================================================
|
||||
-- TEMPLATE 7: projects/:project_id/documents/
|
||||
-- ===========================================================================
|
||||
ORDS.DEFINE_TEMPLATE(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'projects/:project_id/documents/',
|
||||
p_priority => 0,
|
||||
p_etag_type => 'HASH',
|
||||
p_comments => 'Dokument-Liste eines Projekts (ohne BLOB-Payload)'
|
||||
);
|
||||
|
||||
-- 7. GET /api/dc/projects/:project_id/documents/
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'projects/:project_id/documents/',
|
||||
p_method => 'GET',
|
||||
p_source_type => ORDS.SOURCE_TYPE_COLLECTION_FEED,
|
||||
p_items_per_page => 0,
|
||||
p_comments => 'Metadaten aller Dokumente; has_original_text/has_translated_text als 0/1',
|
||||
p_source => q'[
|
||||
SELECT
|
||||
d.id,
|
||||
d.filename,
|
||||
d.mime_type,
|
||||
d.document_type_id,
|
||||
d.catalog_id,
|
||||
d.uploaded_at,
|
||||
CASE WHEN d.original_text IS NOT NULL THEN 1 ELSE 0 END AS has_original_text,
|
||||
CASE WHEN d.translated_text IS NOT NULL THEN 1 ELSE 0 END AS has_translated_text,
|
||||
d.row_version,
|
||||
d.created,
|
||||
d.updated
|
||||
FROM dc_project_documents d
|
||||
WHERE d.project_id = :project_id
|
||||
ORDER BY d.uploaded_at
|
||||
]'
|
||||
);
|
||||
|
||||
|
||||
-- ===========================================================================
|
||||
-- TEMPLATE 8: documents/:doc_id/file
|
||||
-- ===========================================================================
|
||||
ORDS.DEFINE_TEMPLATE(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'documents/:doc_id/file',
|
||||
p_priority => 0,
|
||||
p_comments => 'BLOB-Download: original_file mit korrektem Content-Type'
|
||||
);
|
||||
|
||||
-- 8. GET /api/dc/documents/:doc_id/file
|
||||
-- SOURCE_TYPE_MEDIA: Pflicht-Aliase: content, content_type, filename
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'documents/:doc_id/file',
|
||||
p_method => 'GET',
|
||||
p_source_type => ORDS.SOURCE_TYPE_MEDIA,
|
||||
p_comments => 'Streamt original_file BLOB; ORDS setzt Content-Type aus mime_type-Spalte',
|
||||
p_source => q'[
|
||||
SELECT
|
||||
d.original_file AS content,
|
||||
d.mime_type AS content_type,
|
||||
d.filename AS filename,
|
||||
d.updated AS last_modified
|
||||
FROM dc_project_documents d
|
||||
WHERE d.id = :doc_id
|
||||
]'
|
||||
);
|
||||
|
||||
|
||||
-- ===========================================================================
|
||||
-- TEMPLATE 9: documents/:doc_id/texts
|
||||
-- ===========================================================================
|
||||
ORDS.DEFINE_TEMPLATE(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'documents/:doc_id/texts',
|
||||
p_priority => 0,
|
||||
p_comments => 'OCR-Ergebnis und Uebersetzung zurueckschreiben'
|
||||
);
|
||||
|
||||
-- 9. PUT /api/dc/documents/:doc_id/texts
|
||||
-- Body: {"original_text": "## ...", "translated_text": "## ..."}
|
||||
-- Beide Felder sind optional: NULL-Wert ueberschreibt nicht (nur wenn im Body vorhanden)
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'documents/:doc_id/texts',
|
||||
p_method => 'PUT',
|
||||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||||
p_comments => 'Schreibt original_text und/oder translated_text als CLOB',
|
||||
p_source => q'[
|
||||
DECLARE
|
||||
v_body CLOB;
|
||||
v_original_text CLOB;
|
||||
v_translated CLOB;
|
||||
v_rows INTEGER;
|
||||
BEGIN
|
||||
v_body := :body_text;
|
||||
-- RETURNING CLOB erlaubt Werte groesser als 32767 Bytes (wichtig fuer OCR-Output)
|
||||
v_original_text := JSON_VALUE(v_body, '$.original_text' RETURNING CLOB);
|
||||
v_translated := JSON_VALUE(v_body, '$.translated_text' RETURNING CLOB);
|
||||
|
||||
UPDATE dc_project_documents
|
||||
SET original_text = CASE
|
||||
WHEN JSON_EXISTS(v_body, '$.original_text')
|
||||
THEN v_original_text
|
||||
ELSE original_text
|
||||
END,
|
||||
translated_text = CASE
|
||||
WHEN JSON_EXISTS(v_body, '$.translated_text')
|
||||
THEN v_translated
|
||||
ELSE translated_text
|
||||
END
|
||||
WHERE id = :doc_id;
|
||||
|
||||
v_rows := SQL%ROWCOUNT;
|
||||
IF v_rows = 0 THEN
|
||||
:status_code := 404;
|
||||
HTP.P('{"error":"Dokument nicht gefunden","doc_id":' || :doc_id || '}');
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
:status_code := 200;
|
||||
HTP.P('{"doc_id":' || :doc_id || ',"updated":true}');
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
ROLLBACK;
|
||||
:status_code := 500;
|
||||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||||
END;
|
||||
]'
|
||||
);
|
||||
|
||||
|
||||
-- ===========================================================================
|
||||
-- TEMPLATE 10+11: projects/:project_id/results
|
||||
-- (POST = Einfuegen, DELETE = Alle loeschen)
|
||||
-- ===========================================================================
|
||||
ORDS.DEFINE_TEMPLATE(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'projects/:project_id/results',
|
||||
p_priority => 0,
|
||||
p_comments => 'Prüfergebnisse: POST einfuegen, DELETE alle loeschen'
|
||||
);
|
||||
|
||||
-- 10. POST /api/dc/projects/:project_id/results
|
||||
-- Body: {
|
||||
-- "question_id": 5,
|
||||
-- "doc_id": 101,
|
||||
-- "answer": "Ja, §305 BGB ist erfuellt.",
|
||||
-- "score": 85,
|
||||
-- "result_type": "OK",
|
||||
-- "deviation": 0,
|
||||
-- "warning": 0,
|
||||
-- "the_comment": "Klare Einbeziehungsklausel vorhanden"
|
||||
-- }
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'projects/:project_id/results',
|
||||
p_method => 'POST',
|
||||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||||
p_comments => '201 Created mit Location-Header; 400 bei fehlendem question_id/doc_id/result_type',
|
||||
p_source => q'[
|
||||
DECLARE
|
||||
v_body CLOB;
|
||||
v_question_id NUMBER;
|
||||
v_doc_id NUMBER;
|
||||
v_answer CLOB;
|
||||
v_score NUMBER;
|
||||
v_result_type VARCHAR2(20);
|
||||
v_deviation NUMBER;
|
||||
v_warning NUMBER;
|
||||
v_comment CLOB;
|
||||
v_new_id NUMBER;
|
||||
BEGIN
|
||||
v_body := :body_text;
|
||||
v_question_id := TO_NUMBER(JSON_VALUE(v_body, '$.question_id'));
|
||||
v_doc_id := TO_NUMBER(JSON_VALUE(v_body, '$.doc_id'));
|
||||
v_answer := JSON_VALUE(v_body, '$.answer' RETURNING CLOB);
|
||||
v_score := TO_NUMBER(JSON_VALUE(v_body, '$.score'));
|
||||
v_result_type := JSON_VALUE(v_body, '$.result_type');
|
||||
v_deviation := NVL(TO_NUMBER(JSON_VALUE(v_body, '$.deviation')), 0);
|
||||
v_warning := NVL(TO_NUMBER(JSON_VALUE(v_body, '$.warning')), 0);
|
||||
v_comment := JSON_VALUE(v_body, '$.the_comment' RETURNING CLOB);
|
||||
|
||||
-- Pflichtfelder prüfen
|
||||
IF v_question_id IS NULL OR v_doc_id IS NULL THEN
|
||||
:status_code := 400;
|
||||
HTP.P('{"error":"question_id und doc_id sind Pflichtfelder"}');
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF v_result_type NOT IN ('OK', 'UNKLAR', 'NOK') THEN
|
||||
:status_code := 400;
|
||||
HTP.P('{"error":"result_type muss OK, UNKLAR oder NOK sein",'
|
||||
|| '"received":"' || NVL(v_result_type, 'NULL') || '"}');
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
INSERT INTO dc_results (
|
||||
project_id,
|
||||
question_id,
|
||||
doc_id,
|
||||
answer,
|
||||
score,
|
||||
result_type,
|
||||
deviation,
|
||||
warning,
|
||||
the_comment,
|
||||
evaluated_at
|
||||
) VALUES (
|
||||
:project_id,
|
||||
v_question_id,
|
||||
v_doc_id,
|
||||
v_answer,
|
||||
v_score,
|
||||
v_result_type,
|
||||
v_deviation,
|
||||
v_warning,
|
||||
v_comment,
|
||||
SYSDATE
|
||||
)
|
||||
RETURNING id INTO v_new_id;
|
||||
|
||||
-- Location-Header gemaess REST-Konvention
|
||||
ORDS.SET_RESPONSE_HEADER(
|
||||
'Location',
|
||||
'/api/dc/projects/' || :project_id || '/results/' || v_new_id
|
||||
);
|
||||
|
||||
:status_code := 201;
|
||||
HTP.P('{"id":' || v_new_id
|
||||
|| ',"project_id":' || :project_id
|
||||
|| ',"question_id":' || v_question_id
|
||||
|| ',"doc_id":' || v_doc_id
|
||||
|| ',"result_type":"' || v_result_type || '"'
|
||||
|| ',"score":' || NVL(TO_CHAR(v_score), 'null')
|
||||
|| '}');
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
ROLLBACK;
|
||||
:status_code := 500;
|
||||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||||
END;
|
||||
]'
|
||||
);
|
||||
|
||||
-- 11. DELETE /api/dc/projects/:project_id/results
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'projects/:project_id/results',
|
||||
p_method => 'DELETE',
|
||||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||||
p_comments => 'Loescht alle Ergebnisse eines Projekts (fuer Re-Processing)',
|
||||
p_source => q'[
|
||||
DECLARE
|
||||
v_deleted INTEGER;
|
||||
BEGIN
|
||||
DELETE FROM dc_results
|
||||
WHERE project_id = :project_id;
|
||||
|
||||
v_deleted := SQL%ROWCOUNT;
|
||||
|
||||
:status_code := 200;
|
||||
HTP.P('{"project_id":' || :project_id
|
||||
|| ',"deleted_count":' || v_deleted || '}');
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
ROLLBACK;
|
||||
:status_code := 500;
|
||||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||||
END;
|
||||
]'
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
END;
|
||||
/
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- BLOCK 2: Auth / Privilege (fuer Produktion)
|
||||
-- =============================================================================
|
||||
-- In der Entwicklung kann dieser Block auskommentiert bleiben – das Modul
|
||||
-- ist dann ohne Auth-Pruefung erreichbar (STATUS = PUBLISHED genuegt).
|
||||
--
|
||||
-- Fuer Produktion einkommentieren und danach den OAuth2-Client anlegen:
|
||||
--
|
||||
-- SELECT client_id, client_secret
|
||||
-- FROM user_ords_clients
|
||||
-- WHERE name = 'quarkus_dc_backend';
|
||||
--
|
||||
-- Den client_id/client_secret in die Quarkus application.properties eintragen.
|
||||
-- =============================================================================
|
||||
/*
|
||||
BEGIN
|
||||
-- Rolle anlegen (falls noch nicht vorhanden)
|
||||
BEGIN
|
||||
ORDS.CREATE_ROLE(p_role_name => 'dc_api_user');
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN NULL;
|
||||
END;
|
||||
|
||||
-- Privilege loeschen falls vorhanden
|
||||
BEGIN
|
||||
ORDS.DROP_PRIVILEGE(p_name => 'dc.api.privilege');
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN NULL;
|
||||
END;
|
||||
|
||||
-- Privilege definieren: mappt Rolle auf alle /api/dc/-Endpunkte
|
||||
ORDS.DEFINE_PRIVILEGE(
|
||||
p_privilege_name => 'dc.api.privilege',
|
||||
p_roles => ORDS_TYPES.T_ORDS_VARCHARS('dc_api_user'),
|
||||
p_patterns => ORDS_TYPES.T_ORDS_VARCHARS('/api/dc/*'),
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_label => 'Dokumenten-Check API',
|
||||
p_description => 'Zugriff auf alle /api/dc/-Endpunkte',
|
||||
p_comments => 'Wird dem OAuth2-Client quarkus_dc_backend zugewiesen'
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
END;
|
||||
/
|
||||
|
||||
-- OAuth2-Client fuer den Quarkus-Server (einmalig ausfuehren)
|
||||
BEGIN
|
||||
OAUTH.CREATE_CLIENT(
|
||||
p_name => 'quarkus_dc_backend',
|
||||
p_grant_type => 'CLIENT_CREDENTIALS',
|
||||
p_owner => 'Frigosped',
|
||||
p_description => 'Quarkus-Verarbeitungsserver Service-Account',
|
||||
p_redirect_uri => NULL,
|
||||
p_support_email => 'admin@frigosped.de',
|
||||
p_privilege_names => 'dc.api.privilege'
|
||||
);
|
||||
COMMIT;
|
||||
END;
|
||||
/
|
||||
*/
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- Test-Aufrufe (curl)
|
||||
-- =============================================================================
|
||||
-- Ersetze <BASE> und <SCHEMA> mit deinen ORDS-Werten, z.B.:
|
||||
-- BASE="https://apex.example.com/ords/FRIGOSPED_APP"
|
||||
--
|
||||
-- Ohne Auth (Entwicklung):
|
||||
--
|
||||
-- # 1. Alle Kataloge
|
||||
-- curl -sS "$BASE/api/dc/catalogs/" | jq .
|
||||
--
|
||||
-- # 2. Fragen fuer Katalog 1
|
||||
-- curl -sS "$BASE/api/dc/catalogs/1/questions/" | jq .
|
||||
--
|
||||
-- # 3. Projektdetail
|
||||
-- curl -sS "$BASE/api/dc/projects/42" | jq .
|
||||
--
|
||||
-- # 4. Projekt starten
|
||||
-- curl -sS -X POST "$BASE/api/dc/projects/42/start" | jq .
|
||||
--
|
||||
-- # 5. Fortschritt setzen
|
||||
-- curl -sS -X PUT -H "Content-Type: application/json" \
|
||||
-- -d '{"progress":35}' "$BASE/api/dc/projects/42/progress" | jq .
|
||||
--
|
||||
-- # 6. Projekt abschliessen
|
||||
-- curl -sS -X POST "$BASE/api/dc/projects/42/complete" | jq .
|
||||
--
|
||||
-- # 7. Dokumente auflisten
|
||||
-- curl -sS "$BASE/api/dc/projects/42/documents/" | jq .
|
||||
--
|
||||
-- # 8. Datei herunterladen
|
||||
-- curl -sS -o original.pdf "$BASE/api/dc/documents/101/file"
|
||||
--
|
||||
-- # 9. OCR-Text + Uebersetzung zurueckschreiben
|
||||
-- curl -sS -X PUT -H "Content-Type: application/json" \
|
||||
-- -d '{"original_text":"## Vertrag\n\nAbsatz 1...",
|
||||
-- "translated_text":"## Vertrag (DE)\n\nAbsatz 1..."}' \
|
||||
-- "$BASE/api/dc/documents/101/texts" | jq .
|
||||
--
|
||||
-- # 10. Ergebnis einfuegen
|
||||
-- curl -sS -X POST -H "Content-Type: application/json" \
|
||||
-- -d '{"question_id":5,"doc_id":101,"answer":"Ja, §305 erfuellt.",
|
||||
-- "score":85,"result_type":"OK","deviation":0,"warning":0,
|
||||
-- "the_comment":"Klare Einbeziehungsklausel vorhanden"}' \
|
||||
-- "$BASE/api/dc/projects/42/results" | jq .
|
||||
--
|
||||
-- # 11. Alle Ergebnisse loeschen (Re-Processing)
|
||||
-- curl -sS -X DELETE "$BASE/api/dc/projects/42/results" | jq .
|
||||
-- =============================================================================
|
||||
5
claude_code.sh
Normal file
5
claude_code.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
export ANTHROPIC_AUTH_TOKEN="ollama"
|
||||
export ANTHROPIC_API_KEY=""
|
||||
export ANTHROPIC_BASE_URL="http://gx10.aquantico.lan:11434"
|
||||
claude --verbose --model qwen3.6:35b-a3b-q4_K_M
|
||||
4
dc-backend/.dockerignore
Normal file
4
dc-backend/.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
target/
|
||||
.mvn/
|
||||
*.md
|
||||
dev.sh
|
||||
44
dc-backend/Dockerfile
Normal file
44
dc-backend/Dockerfile
Normal file
@@ -0,0 +1,44 @@
|
||||
# =============================================================================
|
||||
# Stage 1: Build
|
||||
# =============================================================================
|
||||
FROM maven:3.9-eclipse-temurin-21 AS build
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Dependencies zuerst cachen (Layer-Cache bei Code-Änderungen stabil)
|
||||
COPY pom.xml .
|
||||
RUN mvn dependency:go-offline -q
|
||||
|
||||
COPY src ./src
|
||||
RUN mvn package -DskipTests -q
|
||||
|
||||
# =============================================================================
|
||||
# Stage 2: Runtime
|
||||
# =============================================================================
|
||||
FROM eclipse-temurin:21-jre
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Quarkus Fast-JAR Layout kopieren
|
||||
COPY --from=build /build/target/quarkus-app/lib/ ./lib/
|
||||
COPY --from=build /build/target/quarkus-app/*.jar ./
|
||||
COPY --from=build /build/target/quarkus-app/app/ ./app/
|
||||
COPY --from=build /build/target/quarkus-app/quarkus/ ./quarkus/
|
||||
|
||||
EXPOSE 8090
|
||||
|
||||
# Alle Konfigurationswerte können als Env-Variablen überschrieben werden:
|
||||
#
|
||||
# DC_API_KEY API-Key für eingehende Anfragen
|
||||
# DC_AI_BASE_URL Ollama-Endpunkt
|
||||
# DC_AI_API_KEY Ollama X-API-KEY
|
||||
# DC_AI_MAIN_MODEL Hauptmodell (Übersetzung + Auswertung)
|
||||
# DC_AI_OCR_MODEL OCR-Modell
|
||||
# QUARKUS_REST_CLIENT_ORDS_API_URL ORDS-Basis-URL
|
||||
|
||||
ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||||
CMD curl -sf http://localhost:8090/check/health || exit 1
|
||||
|
||||
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar quarkus-run.jar"]
|
||||
44
dc-backend/build-and-push.sh
Normal file
44
dc-backend/build-and-push.sh
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# =============================================================================
|
||||
# Build dc-backend Docker-Image und push auf Registry
|
||||
# =============================================================================
|
||||
|
||||
REGISTRY="registry.express-o.org"
|
||||
IMAGE_NAME="frigosped/dc-backend"
|
||||
TAG="${1:-latest}"
|
||||
FULL_IMAGE="$REGISTRY/$IMAGE_NAME:$TAG"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
echo "=== dc-backend Build & Push ==="
|
||||
echo "Image : $FULL_IMAGE"
|
||||
echo ""
|
||||
|
||||
# --- Build ---
|
||||
echo "→ Build Image..."
|
||||
podman build \
|
||||
--format docker \
|
||||
--tag "$FULL_IMAGE" \
|
||||
--label "build.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
--label "build.version=$TAG" \
|
||||
"$SCRIPT_DIR"
|
||||
|
||||
# Bei explizitem Tag zusätzlich 'latest' setzen
|
||||
if [ "$TAG" != "latest" ]; then
|
||||
podman tag "$FULL_IMAGE" "$REGISTRY/$IMAGE_NAME:latest"
|
||||
echo "→ Auch getaggt als: $REGISTRY/$IMAGE_NAME:latest"
|
||||
fi
|
||||
|
||||
# --- Push ---
|
||||
echo "→ Push $FULL_IMAGE ..."
|
||||
podman push "$FULL_IMAGE"
|
||||
|
||||
if [ "$TAG" != "latest" ]; then
|
||||
echo "→ Push $REGISTRY/$IMAGE_NAME:latest ..."
|
||||
podman push "$REGISTRY/$IMAGE_NAME:latest"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✓ Fertig: $FULL_IMAGE"
|
||||
5
dc-backend/dev.sh
Normal file
5
dc-backend/dev.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
cd "$(dirname "$0")"
|
||||
exec quarkus dev
|
||||
88
dc-backend/k8s/deployment.yaml
Normal file
88
dc-backend/k8s/deployment.yaml
Normal file
@@ -0,0 +1,88 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: dc-backend
|
||||
namespace: ai-env
|
||||
labels:
|
||||
app: dc-backend
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: dc-backend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: dc-backend
|
||||
spec:
|
||||
containers:
|
||||
- name: dc-backend
|
||||
image: registry.express-o.org/frigosped/dc-backend:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8090
|
||||
env:
|
||||
# ORDS-Verbindung (Service im selben Namespace)
|
||||
- name: QUARKUS_REST_CLIENT_ORDS_API_URL
|
||||
value: "http://ords:8080/ords/FRIGOSPED_APP"
|
||||
# API-Key für eingehende Anfragen (DC_API_KEY)
|
||||
- name: DC_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: dc-backend-secrets
|
||||
key: api-key
|
||||
optional: true
|
||||
# Ollama KI-Endpunkt
|
||||
- name: DC_AI_BASE_URL
|
||||
value: "https://ollama.aquantico.de"
|
||||
- name: DC_AI_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: dc-backend-secrets
|
||||
key: ollama-api-key
|
||||
optional: true
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /check/health
|
||||
port: 8090
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 30
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /check/health
|
||||
port: 8090
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: dc-backend
|
||||
namespace: ai-env
|
||||
labels:
|
||||
app: dc-backend
|
||||
spec:
|
||||
selector:
|
||||
app: dc-backend
|
||||
ports:
|
||||
- port: 8090
|
||||
targetPort: 8090
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: dc-backend-secrets
|
||||
namespace: ai-env
|
||||
type: Opaque
|
||||
stringData:
|
||||
api-key: ""
|
||||
ollama-api-key: "324GF44-50AA-4B57-9386-K435DLJ764DFR"
|
||||
18
dc-backend/logs.sh
Normal file
18
dc-backend/logs.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
export KUBECONFIG="$SCRIPT_DIR/../env/frigo-dev.yaml"
|
||||
|
||||
POD=$(kubectl get pod -n ai-env -l app=dc-backend \
|
||||
--field-selector=status.phase=Running \
|
||||
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
|
||||
|
||||
if [ -z "$POD" ]; then
|
||||
echo "Kein laufender dc-backend Pod gefunden. Alle Pods:"
|
||||
kubectl get pods -n ai-env -l app=dc-backend
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Logs: $POD ==="
|
||||
kubectl logs -f "$POD" -n ai-env
|
||||
146
dc-backend/pom.xml
Normal file
146
dc-backend/pom.xml
Normal file
@@ -0,0 +1,146 @@
|
||||
<?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>de.frigosped</groupId>
|
||||
<artifactId>dc-backend</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<quarkus.platform.version>3.15.1</quarkus.platform.version>
|
||||
<langchain4j.version>0.36.2</langchain4j.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.quarkus.platform</groupId>
|
||||
<artifactId>quarkus-bom</artifactId>
|
||||
<version>${quarkus.platform.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- ================================================================ -->
|
||||
<!-- Quarkus REST Server -->
|
||||
<!-- ================================================================ -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest-jackson</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MicroProfile REST Client für ORDS-Aufrufe -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest-client-jackson</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Config -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-config-yaml</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- OpenAPI / Swagger UI (optional, für Entwicklung) -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-smallrye-openapi</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- LangChain4J (roh, ohne Quarkus-Extension – volle Kontrolle -->
|
||||
<!-- über Modell-Konfiguration und Custom-Headers) -->
|
||||
<!-- ================================================================ -->
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j</artifactId>
|
||||
<version>${langchain4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-ollama</artifactId>
|
||||
<version>${langchain4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- Dokumentenverarbeitung -->
|
||||
<!-- ================================================================ -->
|
||||
|
||||
<!-- PDF → Seitenbild (für KI-OCR) -->
|
||||
<dependency>
|
||||
<groupId>org.apache.pdfbox</groupId>
|
||||
<artifactId>pdfbox</artifactId>
|
||||
<version>3.0.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- DOCX → Markdown -->
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>5.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- ODT → Text -->
|
||||
<dependency>
|
||||
<groupId>org.odftoolkit</groupId>
|
||||
<artifactId>odfdom-java</artifactId>
|
||||
<version>0.12.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- Tests -->
|
||||
<!-- ================================================================ -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-junit5</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.rest-assured</groupId>
|
||||
<artifactId>rest-assured</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.quarkus.platform</groupId>
|
||||
<artifactId>quarkus-maven-plugin</artifactId>
|
||||
<version>${quarkus.platform.version}</version>
|
||||
<extensions>true</extensions>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>build</goal>
|
||||
<goal>generate-code</goal>
|
||||
<goal>generate-code-tests</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<compilerArgs>
|
||||
<arg>-parameters</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
110
dc-backend/src/main/java/de/frigosped/dc/ai/AiModelProducer.java
Normal file
110
dc-backend/src/main/java/de/frigosped/dc/ai/AiModelProducer.java
Normal file
@@ -0,0 +1,110 @@
|
||||
package de.frigosped.dc.ai;
|
||||
|
||||
import dev.langchain4j.model.chat.ChatLanguageModel;
|
||||
import dev.langchain4j.model.ollama.OllamaChatModel;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.inject.Produces;
|
||||
import jakarta.inject.Named;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* CDI-Producer für die zwei KI-Modelle.
|
||||
*
|
||||
* Beide Modelle sprechen denselben Ollama-kompatiblen Endpunkt an,
|
||||
* unterscheiden sich aber in Modellname und Timeout.
|
||||
* Der X-API-KEY-Header wird über customHeaders gesetzt.
|
||||
*
|
||||
* Einsatz:
|
||||
* @Inject @Named("main") ChatLanguageModel mainModel → Auswertung + Übersetzung
|
||||
* @Inject @Named("ocr") ChatLanguageModel ocrModel → PDF-OCR (Vision)
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class AiModelProducer {
|
||||
|
||||
@ConfigProperty(name = "dc.ai.base-url")
|
||||
String baseUrl;
|
||||
|
||||
@ConfigProperty(name = "dc.ai.api-key")
|
||||
String apiKey;
|
||||
|
||||
@ConfigProperty(name = "dc.ai.main.model")
|
||||
String mainModelName;
|
||||
|
||||
@ConfigProperty(name = "dc.ai.main.timeout", defaultValue = "300s")
|
||||
String mainTimeout;
|
||||
|
||||
@ConfigProperty(name = "dc.ai.main.max-retries", defaultValue = "2")
|
||||
int mainMaxRetries;
|
||||
|
||||
@ConfigProperty(name = "dc.ai.main.temperature", defaultValue = "0.1")
|
||||
double mainTemperature;
|
||||
|
||||
@ConfigProperty(name = "dc.ai.ocr.model")
|
||||
String ocrModelName;
|
||||
|
||||
@ConfigProperty(name = "dc.ai.ocr.timeout", defaultValue = "120s")
|
||||
String ocrTimeout;
|
||||
|
||||
@ConfigProperty(name = "dc.ai.ocr.max-retries", defaultValue = "2")
|
||||
int ocrMaxRetries;
|
||||
|
||||
@ConfigProperty(name = "dc.ai.log-requests", defaultValue = "false")
|
||||
boolean logRequests;
|
||||
|
||||
@ConfigProperty(name = "dc.ai.log-responses", defaultValue = "false")
|
||||
boolean logResponses;
|
||||
|
||||
/**
|
||||
* Hauptmodell: Auswertung der Fragen + Übersetzung
|
||||
* Modell: gpt-oss:20b
|
||||
*/
|
||||
@Produces
|
||||
@ApplicationScoped
|
||||
@Named("main")
|
||||
public ChatLanguageModel mainModel() {
|
||||
return OllamaChatModel.builder()
|
||||
.baseUrl(baseUrl)
|
||||
.modelName(mainModelName)
|
||||
.temperature(mainTemperature)
|
||||
.timeout(parseDuration(mainTimeout))
|
||||
.maxRetries(mainMaxRetries)
|
||||
.customHeaders(Map.of("X-API-KEY", apiKey))
|
||||
.logRequests(logRequests)
|
||||
.logResponses(logResponses)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* OCR-Modell: Bildtext-Extraktion aus PDF-Seiten (Vision-Modus)
|
||||
* Modell: quen3.5:9b
|
||||
*/
|
||||
@Produces
|
||||
@ApplicationScoped
|
||||
@Named("ocr")
|
||||
public ChatLanguageModel ocrModel() {
|
||||
return OllamaChatModel.builder()
|
||||
.baseUrl(baseUrl)
|
||||
.modelName(ocrModelName)
|
||||
.timeout(parseDuration(ocrTimeout))
|
||||
.maxRetries(ocrMaxRetries)
|
||||
.customHeaders(Map.of("X-API-KEY", apiKey))
|
||||
.logRequests(logRequests)
|
||||
.logResponses(logResponses)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parst Timeout-Strings wie "300s", "5m", "120s" in Duration.
|
||||
*/
|
||||
private Duration parseDuration(String value) {
|
||||
if (value.endsWith("s")) {
|
||||
return Duration.ofSeconds(Long.parseLong(value.replace("s", "")));
|
||||
} else if (value.endsWith("m")) {
|
||||
return Duration.ofMinutes(Long.parseLong(value.replace("m", "")));
|
||||
}
|
||||
return Duration.ofSeconds(Long.parseLong(value));
|
||||
}
|
||||
}
|
||||
125
dc-backend/src/main/java/de/frigosped/dc/client/OrdsClient.java
Normal file
125
dc-backend/src/main/java/de/frigosped/dc/client/OrdsClient.java
Normal file
@@ -0,0 +1,125 @@
|
||||
package de.frigosped.dc.client;
|
||||
|
||||
import de.frigosped.dc.model.*;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam;
|
||||
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
|
||||
|
||||
/**
|
||||
* MicroProfile REST Client für die ORDS /api/dc/-Endpunkte.
|
||||
*
|
||||
* Basis-URL wird in application.properties gesetzt:
|
||||
* quarkus.rest-client.ords-api.url=http://host/ords/SCHEMA
|
||||
*
|
||||
* Optionaler Bearer-Token für Produktion:
|
||||
* dc.ords.bearer-token=eyJ...
|
||||
* → @ClientHeaderParam(name = "Authorization", value = "${dc.ords.bearer-token}")
|
||||
*/
|
||||
@RegisterRestClient(configKey = "ords-api")
|
||||
@Path("/api/dc")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public interface OrdsClient {
|
||||
|
||||
// =========================================================================
|
||||
// Fragenkatalog (Lesend)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* GET /api/dc/catalogs/:catalogId/questions/
|
||||
* Liefert alle Fragen (mit Kategorie-Info) für einen Katalog.
|
||||
*/
|
||||
@GET
|
||||
@Path("/catalogs/{catalogId}/questions/")
|
||||
OrdsQuestionList getQuestions(@PathParam("catalogId") long catalogId);
|
||||
|
||||
// =========================================================================
|
||||
// Projekte (Status-Steuerung)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* GET /api/dc/projects/:projectId
|
||||
* Projektdetails laden (Status, catalog_id, notification_email, ...).
|
||||
*/
|
||||
@GET
|
||||
@Path("/projects/{projectId}")
|
||||
OrdsProject getProject(@PathParam("projectId") long projectId);
|
||||
|
||||
/**
|
||||
* POST /api/dc/projects/:projectId/start
|
||||
* Setzt Status auf IN_PROGRESS. 409 wenn nicht PENDING.
|
||||
*/
|
||||
@POST
|
||||
@Path("/projects/{projectId}/start")
|
||||
Response startProject(@PathParam("projectId") long projectId);
|
||||
|
||||
/**
|
||||
* PUT /api/dc/projects/:projectId/progress
|
||||
* Aktualisiert den Fortschritt (0–100).
|
||||
*/
|
||||
@PUT
|
||||
@Path("/projects/{projectId}/progress")
|
||||
Response updateProgress(@PathParam("projectId") long projectId,
|
||||
ProgressRequest body);
|
||||
|
||||
/**
|
||||
* POST /api/dc/projects/:projectId/complete
|
||||
* Setzt Status auf COMPLETED + completed_at = SYSDATE.
|
||||
*/
|
||||
@POST
|
||||
@Path("/projects/{projectId}/complete")
|
||||
Response completeProject(@PathParam("projectId") long projectId);
|
||||
|
||||
// =========================================================================
|
||||
// Dokumente
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* GET /api/dc/projects/:projectId/documents/
|
||||
* Liste aller Dokumente (Metadaten, kein BLOB).
|
||||
*/
|
||||
@GET
|
||||
@Path("/projects/{projectId}/documents/")
|
||||
OrdsDocumentList getDocuments(@PathParam("projectId") long projectId);
|
||||
|
||||
/**
|
||||
* GET /api/dc/documents/:docId/file
|
||||
* Download des Original-BLOBs.
|
||||
* Content-Type wird von ORDS aus mime_type-Spalte gesetzt.
|
||||
*/
|
||||
@GET
|
||||
@Path("/documents/{docId}/file")
|
||||
@Produces(MediaType.WILDCARD)
|
||||
Response downloadFile(@PathParam("docId") long docId);
|
||||
|
||||
/**
|
||||
* PUT /api/dc/documents/:docId/texts
|
||||
* Schreibt original_text und/oder translated_text zurück.
|
||||
*/
|
||||
@PUT
|
||||
@Path("/documents/{docId}/texts")
|
||||
Response updateTexts(@PathParam("docId") long docId, TextsRequest body);
|
||||
|
||||
// =========================================================================
|
||||
// Ergebnisse
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* POST /api/dc/projects/:projectId/results
|
||||
* Einzelnes Prüfergebnis speichern.
|
||||
*/
|
||||
@POST
|
||||
@Path("/projects/{projectId}/results")
|
||||
Response saveResult(@PathParam("projectId") long projectId,
|
||||
ResultRequest body);
|
||||
|
||||
/**
|
||||
* DELETE /api/dc/projects/:projectId/results
|
||||
* Alle Ergebnisse löschen (vor Re-Processing).
|
||||
*/
|
||||
@DELETE
|
||||
@Path("/projects/{projectId}/results")
|
||||
Response deleteResults(@PathParam("projectId") long projectId);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package de.frigosped.dc.client;
|
||||
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.ws.rs.client.ClientRequestContext;
|
||||
import jakarta.ws.rs.client.ClientRequestFilter;
|
||||
import jakarta.ws.rs.client.ClientResponseFilter;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Loggt jede HTTP-Anfrage und -Antwort des ORDS REST Clients:
|
||||
* - Vollständige URL (Methode + URI)
|
||||
* - Request-Body
|
||||
* - Response-Status + Body
|
||||
*/
|
||||
@Provider
|
||||
@Priority(Javax.ws.rs.Priorities.AUTHENTICATION + 1)
|
||||
public class OrdsLoggingFilter implements ClientRequestFilter, ClientResponseFilter {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(OrdsLoggingFilter.class);
|
||||
|
||||
@Override
|
||||
public void filter(ClientRequestContext ctx) throws IOException {
|
||||
String method = ctx.getMethod();
|
||||
String uri = ctx.getUri().toString();
|
||||
String entity = readRequest(ctx);
|
||||
|
||||
LOG.infof("=== ORDS %s %s ===", method, uri);
|
||||
if (entity != null && !entity.isBlank()) {
|
||||
LOG.infof("REQUEST: %s", entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void filter(ClientRequestContext requestCtx, ClientResponseContext responseCtx) throws IOException {
|
||||
String body = readResponse(responseCtx);
|
||||
LOG.infof("RESPONSE: HTTP %d%s",
|
||||
responseCtx.getStatus(),
|
||||
body != null ? " " + body : "");
|
||||
LOG.infof("=== ORDS %s %s END ===", requestCtx.getMethod(), requestCtx.getUri());
|
||||
}
|
||||
|
||||
private String readRequest(ClientRequestContext ctx) {
|
||||
Object entity = ctx.getEntity();
|
||||
if (entity == null) return null;
|
||||
|
||||
String body;
|
||||
if (entity instanceof byte[] bytes) {
|
||||
body = new String(bytes, StandardCharsets.UTF_8);
|
||||
} else if (entity instanceof String str) {
|
||||
body = str;
|
||||
} else if (entity instanceof List<?> list) {
|
||||
body = list.toString();
|
||||
} else if (entity instanceof Object[] arr) {
|
||||
body = Arrays.toString(arr);
|
||||
} else {
|
||||
body = entity.toString();
|
||||
}
|
||||
if (body.isBlank()) return null;
|
||||
|
||||
// Entity neu setzen — Stream nicht verbrauchen
|
||||
ctx.setEntity(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)),
|
||||
ctx.getHeaders().getFirst("Content-Type", MediaType.APPLICATION_OCTET_STREAM),
|
||||
Long.valueOf(body.length()));
|
||||
return body;
|
||||
}
|
||||
|
||||
private String readResponse(ClientResponseContext ctx) throws IOException {
|
||||
if (ctx.getEntityStream() == null) return null;
|
||||
|
||||
byte[] raw = ctx.getEntityStream().readAllBytes();
|
||||
if (raw.length == 0) return null;
|
||||
|
||||
// Stream neu setzen für nachfolgende Consumer (z.B. Response.readEntity)
|
||||
ctx.setEntityStream(new ByteArrayInputStream(raw));
|
||||
|
||||
if (raw.length > 4000) {
|
||||
return String.format("[%.4k bytes, trunc. zu 4000]", raw.length);
|
||||
}
|
||||
|
||||
String mediaType = ctx.getMediaType() != null ? ctx.getMediaType().toString() : "";
|
||||
if (mediaType.contains("application/json") || mediaType.contains("text/") || mediaType.isEmpty()) {
|
||||
return new String(raw, StandardCharsets.UTF_8);
|
||||
}
|
||||
return String.format("[%d bytes binary]", raw.length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package de.frigosped.dc.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
|
||||
/**
|
||||
* Deserialisiertes JSON-Ergebnis aus der KI-Auswertung.
|
||||
* Die KI gibt dieses Objekt als JSON-String zurück.
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class EvaluationResult {
|
||||
|
||||
/** Antwort auf Deutsch (Zitate in Originalsprache + Übersetzung) */
|
||||
private String answer;
|
||||
|
||||
/** Erfüllungsgrad 0–100 */
|
||||
private Integer score;
|
||||
|
||||
/** OK | UNKLAR | NOK */
|
||||
private String resultType;
|
||||
|
||||
/** Kommentar auf Deutsch */
|
||||
private String theComment;
|
||||
|
||||
public EvaluationResult() {}
|
||||
|
||||
public String getAnswer() { return answer; }
|
||||
public void setAnswer(String v) { this.answer = v; }
|
||||
|
||||
public Integer getScore() { return score; }
|
||||
public void setScore(Integer v) { this.score = v; }
|
||||
|
||||
public String getResultType() { return resultType; }
|
||||
public void setResultType(String v) { this.resultType = v; }
|
||||
|
||||
public String getTheComment() { return theComment; }
|
||||
public void setTheComment(String v) { this.theComment = v; }
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package de.frigosped.dc.model;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* DTO für ORDS-Endpunkt GET /api/dc/projects/:id/documents/
|
||||
* Kein BLOB; has_original_text / has_translated_text kommen als 0/1.
|
||||
*/
|
||||
public class OrdsDocument {
|
||||
|
||||
private Long id;
|
||||
private String filename;
|
||||
private String mimeType;
|
||||
private Long documentTypeId;
|
||||
private Long catalogId;
|
||||
private LocalDateTime uploadedAt;
|
||||
private Integer hasOriginalText; // 0 oder 1
|
||||
private Integer hasTranslatedText; // 0 oder 1
|
||||
private Integer rowVersion;
|
||||
private LocalDateTime created;
|
||||
private LocalDateTime updated;
|
||||
|
||||
public OrdsDocument() {}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getFilename() { return filename; }
|
||||
public void setFilename(String filename) { this.filename = filename; }
|
||||
|
||||
public String getMimeType() { return mimeType; }
|
||||
public void setMimeType(String mimeType) { this.mimeType = mimeType; }
|
||||
|
||||
public Long getDocumentTypeId() { return documentTypeId; }
|
||||
public void setDocumentTypeId(Long v) { this.documentTypeId = v; }
|
||||
|
||||
public Long getCatalogId() { return catalogId; }
|
||||
public void setCatalogId(Long catalogId) { this.catalogId = catalogId; }
|
||||
|
||||
public LocalDateTime getUploadedAt() { return uploadedAt; }
|
||||
public void setUploadedAt(LocalDateTime v) { this.uploadedAt = v; }
|
||||
|
||||
public Integer getHasOriginalText() { return hasOriginalText; }
|
||||
public void setHasOriginalText(Integer v) { this.hasOriginalText = v; }
|
||||
|
||||
public Integer getHasTranslatedText() { return hasTranslatedText; }
|
||||
public void setHasTranslatedText(Integer v) { this.hasTranslatedText = v; }
|
||||
|
||||
public Integer getRowVersion() { return rowVersion; }
|
||||
public void setRowVersion(Integer v) { this.rowVersion = v; }
|
||||
|
||||
public LocalDateTime getCreated() { return created; }
|
||||
public void setCreated(LocalDateTime v) { this.created = v; }
|
||||
|
||||
public LocalDateTime getUpdated() { return updated; }
|
||||
public void setUpdated(LocalDateTime v) { this.updated = v; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package de.frigosped.dc.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ORDS COLLECTION_FEED-Antwort für Dokumente.
|
||||
* {"items":[...], "hasMore":false, "count":N}
|
||||
*/
|
||||
public class OrdsDocumentList {
|
||||
|
||||
private List<OrdsDocument> items;
|
||||
private Boolean hasMore;
|
||||
private Integer count;
|
||||
|
||||
public OrdsDocumentList() {}
|
||||
|
||||
public List<OrdsDocument> getItems() { return items; }
|
||||
public void setItems(List<OrdsDocument> items) { this.items = items; }
|
||||
|
||||
public Boolean getHasMore() { return hasMore; }
|
||||
public void setHasMore(Boolean hasMore) { this.hasMore = hasMore; }
|
||||
|
||||
public Integer getCount() { return count; }
|
||||
public void setCount(Integer count) { this.count = count; }
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package de.frigosped.dc.model;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* DTO für den ORDS-Endpunkt GET /api/dc/projects/:project_id
|
||||
* (COLLECTION_ITEM – einzelnes JSON-Objekt, snake_case von ORDS)
|
||||
*/
|
||||
public class OrdsProject {
|
||||
|
||||
private Long id;
|
||||
private String name;
|
||||
private String description;
|
||||
private Long catalogId;
|
||||
private Long createdByUser;
|
||||
private String status; // PENDING | IN_PROGRESS | COMPLETED
|
||||
private Integer progress; // 0–100
|
||||
private LocalDateTime completedAt;
|
||||
private String notificationEmail;
|
||||
private Integer rowVersion;
|
||||
private LocalDateTime created;
|
||||
private LocalDateTime updated;
|
||||
|
||||
public OrdsProject() {}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String d) { this.description = d; }
|
||||
|
||||
public Long getCatalogId() { return catalogId; }
|
||||
public void setCatalogId(Long catalogId) { this.catalogId = catalogId; }
|
||||
|
||||
public Long getCreatedByUser() { return createdByUser; }
|
||||
public void setCreatedByUser(Long v) { this.createdByUser = v; }
|
||||
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
|
||||
public Integer getProgress() { return progress; }
|
||||
public void setProgress(Integer progress) { this.progress = progress; }
|
||||
|
||||
public LocalDateTime getCompletedAt() { return completedAt; }
|
||||
public void setCompletedAt(LocalDateTime v) { this.completedAt = v; }
|
||||
|
||||
public String getNotificationEmail() { return notificationEmail; }
|
||||
public void setNotificationEmail(String v) { this.notificationEmail = v; }
|
||||
|
||||
public Integer getRowVersion() { return rowVersion; }
|
||||
public void setRowVersion(Integer v) { this.rowVersion = v; }
|
||||
|
||||
public LocalDateTime getCreated() { return created; }
|
||||
public void setCreated(LocalDateTime v) { this.created = v; }
|
||||
|
||||
public LocalDateTime getUpdated() { return updated; }
|
||||
public void setUpdated(LocalDateTime v) { this.updated = v; }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package de.frigosped.dc.model;
|
||||
|
||||
/**
|
||||
* DTO für ORDS-Endpunkt GET /api/dc/catalogs/:id/questions/
|
||||
* Flache Darstellung: Kategorie + Frage in einer Zeile.
|
||||
*/
|
||||
public class OrdsQuestion {
|
||||
|
||||
private Long questionId;
|
||||
private Long categoryId;
|
||||
private String categoryName;
|
||||
private String categoryDescription;
|
||||
private String questionText;
|
||||
private String evaluationType; // z.B. SCORE, BINARY
|
||||
private Integer threshold; // Schwellwert 0–100
|
||||
private String resultHandling; // ABWEICHUNG | HINWEIS
|
||||
private String example0Percent; // Beispiel: Anforderung nicht erfüllt
|
||||
private String example100Percent; // Beispiel: Anforderung voll erfüllt
|
||||
private Integer rowVersion;
|
||||
|
||||
public OrdsQuestion() {}
|
||||
|
||||
public Long getQuestionId() { return questionId; }
|
||||
public void setQuestionId(Long v) { this.questionId = v; }
|
||||
|
||||
public Long getCategoryId() { return categoryId; }
|
||||
public void setCategoryId(Long v) { this.categoryId = v; }
|
||||
|
||||
public String getCategoryName() { return categoryName; }
|
||||
public void setCategoryName(String v) { this.categoryName = v; }
|
||||
|
||||
public String getCategoryDescription() { return categoryDescription; }
|
||||
public void setCategoryDescription(String v) { this.categoryDescription = v; }
|
||||
|
||||
public String getQuestionText() { return questionText; }
|
||||
public void setQuestionText(String v) { this.questionText = v; }
|
||||
|
||||
public String getEvaluationType() { return evaluationType; }
|
||||
public void setEvaluationType(String v) { this.evaluationType = v; }
|
||||
|
||||
public Integer getThreshold() { return threshold; }
|
||||
public void setThreshold(Integer v) { this.threshold = v; }
|
||||
|
||||
public String getResultHandling() { return resultHandling; }
|
||||
public void setResultHandling(String v) { this.resultHandling = v; }
|
||||
|
||||
public String getExample0Percent() { return example0Percent; }
|
||||
public void setExample0Percent(String v) { this.example0Percent = v; }
|
||||
|
||||
public String getExample100Percent() { return example100Percent; }
|
||||
public void setExample100Percent(String v) { this.example100Percent = v; }
|
||||
|
||||
public Integer getRowVersion() { return rowVersion; }
|
||||
public void setRowVersion(Integer v) { this.rowVersion = v; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package de.frigosped.dc.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ORDS COLLECTION_FEED-Antwort für Fragen.
|
||||
* {"items":[...], "hasMore":false, "count":N}
|
||||
*/
|
||||
public class OrdsQuestionList {
|
||||
|
||||
private List<OrdsQuestion> items;
|
||||
private Boolean hasMore;
|
||||
private Integer count;
|
||||
|
||||
public OrdsQuestionList() {}
|
||||
|
||||
public List<OrdsQuestion> getItems() { return items; }
|
||||
public void setItems(List<OrdsQuestion> items) { this.items = items; }
|
||||
|
||||
public Boolean getHasMore() { return hasMore; }
|
||||
public void setHasMore(Boolean hasMore) { this.hasMore = hasMore; }
|
||||
|
||||
public Integer getCount() { return count; }
|
||||
public void setCount(Integer count) { this.count = count; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.frigosped.dc.model;
|
||||
|
||||
/**
|
||||
* Request-Body für PUT /api/dc/projects/:id/progress
|
||||
* {"progress": 45}
|
||||
*/
|
||||
public class ProgressRequest {
|
||||
|
||||
private Integer progress;
|
||||
|
||||
public ProgressRequest() {}
|
||||
|
||||
public ProgressRequest(int progress) {
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
public Integer getProgress() { return progress; }
|
||||
public void setProgress(Integer v) { this.progress = v; }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package de.frigosped.dc.model;
|
||||
|
||||
/**
|
||||
* Request-Body für POST /api/dc/projects/:id/results
|
||||
*/
|
||||
public class ResultRequest {
|
||||
|
||||
private Long questionId;
|
||||
private Long docId;
|
||||
private String answer;
|
||||
private Integer score;
|
||||
private String resultType; // OK | UNKLAR | NOK
|
||||
private Integer deviation; // 0 oder 1
|
||||
private Integer warning; // 0 oder 1
|
||||
private String theComment;
|
||||
|
||||
public ResultRequest() {}
|
||||
|
||||
public ResultRequest(Long questionId, Long docId, String answer,
|
||||
Integer score, String resultType,
|
||||
Integer deviation, Integer warning, String theComment) {
|
||||
this.questionId = questionId;
|
||||
this.docId = docId;
|
||||
this.answer = answer;
|
||||
this.score = score;
|
||||
this.resultType = resultType;
|
||||
this.deviation = deviation;
|
||||
this.warning = warning;
|
||||
this.theComment = theComment;
|
||||
}
|
||||
|
||||
public Long getQuestionId() { return questionId; }
|
||||
public void setQuestionId(Long v) { this.questionId = v; }
|
||||
|
||||
public Long getDocId() { return docId; }
|
||||
public void setDocId(Long v) { this.docId = v; }
|
||||
|
||||
public String getAnswer() { return answer; }
|
||||
public void setAnswer(String v) { this.answer = v; }
|
||||
|
||||
public Integer getScore() { return score; }
|
||||
public void setScore(Integer v) { this.score = v; }
|
||||
|
||||
public String getResultType() { return resultType; }
|
||||
public void setResultType(String v) { this.resultType = v; }
|
||||
|
||||
public Integer getDeviation() { return deviation; }
|
||||
public void setDeviation(Integer v) { this.deviation = v; }
|
||||
|
||||
public Integer getWarning() { return warning; }
|
||||
public void setWarning(Integer v) { this.warning = v; }
|
||||
|
||||
public String getTheComment() { return theComment; }
|
||||
public void setTheComment(String v) { this.theComment = v; }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.frigosped.dc.model;
|
||||
|
||||
/**
|
||||
* Request-Body für PUT /api/dc/documents/:id/texts
|
||||
* {"original_text": "...", "translated_text": "..."}
|
||||
*/
|
||||
public class TextsRequest {
|
||||
|
||||
private String originalText;
|
||||
private String translatedText;
|
||||
|
||||
public TextsRequest() {}
|
||||
|
||||
public TextsRequest(String originalText, String translatedText) {
|
||||
this.originalText = originalText;
|
||||
this.translatedText = translatedText;
|
||||
}
|
||||
|
||||
public String getOriginalText() { return originalText; }
|
||||
public void setOriginalText(String v) { this.originalText = v; }
|
||||
|
||||
public String getTranslatedText() { return translatedText; }
|
||||
public void setTranslatedText(String v) { this.translatedText = v; }
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package de.frigosped.dc.resource;
|
||||
|
||||
import de.frigosped.dc.service.CheckOrchestrationService;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* REST-Endpunkte des Dokumenten-Check-Backend.
|
||||
*
|
||||
* POST /check/{projectId}
|
||||
* → Startet die asynchrone Verarbeitung eines Projekts.
|
||||
* → Gibt sofort 202 Accepted zurück; Fortschritt über ORDS-Endpunkte ablesbar.
|
||||
*
|
||||
* GET /check/health
|
||||
* → Einfacher Health-Check.
|
||||
*/
|
||||
@Path("/check")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Dokumenten-Check", description = "KI-gestützte Dokumentenprüfung")
|
||||
public class CheckResource {
|
||||
|
||||
@Inject
|
||||
CheckOrchestrationService orchestrationService;
|
||||
|
||||
// =========================================================================
|
||||
// POST /check/{projectId}
|
||||
// =========================================================================
|
||||
|
||||
@POST
|
||||
@Path("/{projectId}")
|
||||
@Operation(
|
||||
summary = "Startet die Dokumentenprüfung für ein Projekt",
|
||||
description = """
|
||||
Triggert den vollständigen Prüfablauf asynchron:
|
||||
1. OCR / Konvertierung der Dokumente (KI)
|
||||
2. Übersetzung ins Deutsche (KI)
|
||||
3. Auswertung des Fragenkatalogs (KI)
|
||||
4. Ergebnisse werden in ORDS gespeichert
|
||||
|
||||
Das Projekt muss im Status PENDING sein.
|
||||
Fortschritt: GET {ords}/api/dc/projects/{id}
|
||||
"""
|
||||
)
|
||||
public Response startCheck(@PathParam("projectId") long projectId) {
|
||||
// Asynchron starten – Fire & Forget.
|
||||
// Der aufrufende Thread (z.B. APEX-Button) bekommt sofort 202.
|
||||
// Der Verarbeitungs-Thread läuft im ForkJoin-CommonPool.
|
||||
CompletableFuture
|
||||
.runAsync(() -> orchestrationService.process(projectId))
|
||||
.exceptionally(ex -> {
|
||||
// Der Orchestrator fängt intern; dies ist eine letzte Absicherung.
|
||||
// Logging ist dort bereits vorhanden.
|
||||
return null;
|
||||
});
|
||||
|
||||
return Response
|
||||
.accepted(Map.of(
|
||||
"message", "Verarbeitung gestartet",
|
||||
"project_id", projectId,
|
||||
"hint", "Fortschritt: GET /api/dc/projects/" + projectId
|
||||
))
|
||||
.build();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// GET /check/health
|
||||
// =========================================================================
|
||||
|
||||
@GET
|
||||
@Path("/health")
|
||||
@Operation(summary = "Health-Check", description = "Gibt UP zurück wenn der Service läuft.")
|
||||
public Response health() {
|
||||
return Response.ok(Map.of("status", "UP", "service", "dc-backend")).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package de.frigosped.dc.security;
|
||||
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.ws.rs.Priorities;
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import jakarta.ws.rs.container.ContainerRequestFilter;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Prüft den X-API-KEY-Header bei allen Anfragen.
|
||||
* Konfiguration: dc.api.key (Env: DC_API_KEY)
|
||||
* Wenn kein Key konfiguriert ist, läuft der Service ohne Auth (Dev-Modus).
|
||||
*/
|
||||
@Provider
|
||||
@Priority(Priorities.AUTHENTICATION)
|
||||
public class ApiKeyFilter implements ContainerRequestFilter {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(ApiKeyFilter.class);
|
||||
private static final String HEADER = "X-API-KEY";
|
||||
|
||||
@ConfigProperty(name = "dc.api.key")
|
||||
Optional<String> apiKey;
|
||||
|
||||
@Override
|
||||
public void filter(ContainerRequestContext ctx) {
|
||||
// Health-Endpunkt immer durchlassen (Docker-Healthcheck)
|
||||
if (ctx.getUriInfo().getPath().endsWith("/health")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Kein Key konfiguriert → Auth deaktiviert (Dev-Modus)
|
||||
if (apiKey.isEmpty() || apiKey.get().isBlank()) {
|
||||
LOG.warnf("DC_API_KEY nicht gesetzt – Anfrage ohne Auth durchgelassen: %s",
|
||||
ctx.getUriInfo().getPath());
|
||||
return;
|
||||
}
|
||||
|
||||
String provided = ctx.getHeaderString(HEADER);
|
||||
if (provided == null || !provided.equals(apiKey.get())) {
|
||||
LOG.warnf("Ungültiger oder fehlender API-Key für: %s", ctx.getUriInfo().getPath());
|
||||
ctx.abortWith(Response
|
||||
.status(Response.Status.UNAUTHORIZED)
|
||||
.type(MediaType.APPLICATION_JSON)
|
||||
.entity(Map.of("error", "Ungültiger oder fehlender API-Key",
|
||||
"header", HEADER))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package de.frigosped.dc.service;
|
||||
|
||||
import de.frigosped.dc.client.OrdsClient;
|
||||
import de.frigosped.dc.model.*;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.rest.client.inject.RestClient;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Orchestriert den kompletten Prüf-Ablauf für ein Projekt:
|
||||
*
|
||||
* 1. Projekt laden → IN_PROGRESS setzen
|
||||
* 2. Dokumente herunterladen → OCR / Konvertierung → Texte speichern
|
||||
* 3. Texte ins Deutsche übersetzen → gespeichert
|
||||
* 4. Fragenkatalog für jeden Dokument-Text auswerten
|
||||
* 5. Ergebnisse Frage × Dokument in DB speichern
|
||||
* 6. Projekt auf COMPLETED setzen
|
||||
*
|
||||
* Wird von CheckResource.startCheck() asynchron (CompletableFuture) aufgerufen.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class CheckOrchestrationService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(CheckOrchestrationService.class);
|
||||
|
||||
@RestClient
|
||||
OrdsClient ordsClient;
|
||||
|
||||
@Inject
|
||||
DocumentProcessingService documentProcessingService;
|
||||
|
||||
@Inject
|
||||
EvaluationService evaluationService;
|
||||
|
||||
// =========================================================================
|
||||
// Haupt-Einstiegspunkt
|
||||
// =========================================================================
|
||||
|
||||
public void process(long projectId) {
|
||||
LOG.infof("=== Starte Verarbeitung Projekt %d ===", projectId);
|
||||
|
||||
try {
|
||||
// 1. Projekt laden
|
||||
OrdsProject project = ordsClient.getProject(projectId);
|
||||
LOG.infof("Projekt: '%s', Status: %s, Katalog: %d",
|
||||
project.getName(), project.getStatus(), project.getCatalogId());
|
||||
|
||||
// Absicherung: nur PENDING starten
|
||||
if (!"PENDING".equals(project.getStatus())) {
|
||||
LOG.warnf("Projekt %d ist nicht PENDING (ist: %s) – Abbruch",
|
||||
projectId, project.getStatus());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Status → IN_PROGRESS
|
||||
Response startResp = ordsClient.startProject(projectId);
|
||||
if (startResp.getStatus() == 409) {
|
||||
LOG.warnf("Projekt %d konnte nicht gestartet werden (409) – Abbruch", projectId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Dokumente + Fragen laden
|
||||
List<OrdsDocument> documents = ordsClient.getDocuments(projectId).getItems();
|
||||
List<OrdsQuestion> questions = ordsClient
|
||||
.getQuestions(project.getCatalogId()).getItems();
|
||||
|
||||
LOG.infof("Projekt %d: %d Dokument(e), %d Frage(n)",
|
||||
projectId, documents.size(), questions.size());
|
||||
|
||||
if (documents.isEmpty()) {
|
||||
LOG.warnf("Projekt %d hat keine Dokumente – markiere als abgeschlossen", projectId);
|
||||
ordsClient.completeProject(projectId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Fortschrittsberechnung
|
||||
// Pro Dokument: 1 Schritt OCR/Übersetzung + questions.size() Auswertungsschritte
|
||||
int totalSteps = documents.size() * (1 + questions.size());
|
||||
int[] stepHolder = {0}; // Array für Verwendung in Lambda
|
||||
|
||||
// 5. Alte Ergebnisse löschen (ermöglicht Re-Processing)
|
||||
Response delResp = ordsClient.deleteResults(projectId);
|
||||
LOG.debugf("Alte Ergebnisse gelöscht: %s", delResp.getStatus());
|
||||
|
||||
// 6. Texte sammeln (in-memory: vermeidet Re-Fetch aus ORDS)
|
||||
Map<Long, String> originalTexts = new HashMap<>();
|
||||
|
||||
// ─── PHASE A: OCR + Übersetzung ───────────────────────────────
|
||||
for (OrdsDocument doc : documents) {
|
||||
LOG.infof("Verarbeite Dokument %d: %s", doc.getId(), doc.getFilename());
|
||||
|
||||
String originalText = "";
|
||||
String translatedText = "";
|
||||
|
||||
try {
|
||||
// Datei herunterladen
|
||||
Response fileResp = ordsClient.downloadFile(doc.getId());
|
||||
if (fileResp.getStatus() != 200) {
|
||||
LOG.warnf("Download Dokument %d fehlgeschlagen (HTTP %d)",
|
||||
doc.getId(), fileResp.getStatus());
|
||||
} else {
|
||||
byte[] fileBytes = fileResp.readEntity(byte[].class);
|
||||
|
||||
// OCR / Konvertierung → Markdown
|
||||
originalText = documentProcessingService.extractText(
|
||||
fileBytes, doc.getMimeType(), doc.getFilename());
|
||||
LOG.debugf("Dokument %d: %d Zeichen extrahiert",
|
||||
(long) doc.getId(), (long) originalText.length());
|
||||
|
||||
// Übersetzung ins Deutsche
|
||||
translatedText = evaluationService.translate(originalText);
|
||||
LOG.debugf("Dokument %d: Übersetzung fertig (%d Zeichen)",
|
||||
(long) doc.getId(), (long) translatedText.length());
|
||||
|
||||
// Texte in ORDS zurückschreiben
|
||||
ordsClient.updateTexts(doc.getId(),
|
||||
new TextsRequest(originalText, translatedText));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Fehler bei Dokumentenverarbeitung %d", doc.getId());
|
||||
}
|
||||
|
||||
originalTexts.put(doc.getId(), originalText);
|
||||
|
||||
// Fortschritt aktualisieren
|
||||
stepHolder[0]++;
|
||||
pushProgress(projectId, stepHolder[0], totalSteps);
|
||||
}
|
||||
|
||||
// ─── PHASE B: Fragen auswerten ────────────────────────────────
|
||||
for (OrdsDocument doc : documents) {
|
||||
String originalText = originalTexts.getOrDefault(doc.getId(), "");
|
||||
|
||||
if (originalText.isBlank()) {
|
||||
LOG.warnf("Kein Text für Dokument %d – überspringe Auswertung", doc.getId());
|
||||
stepHolder[0] += questions.size();
|
||||
pushProgress(projectId, stepHolder[0], totalSteps);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (OrdsQuestion question : questions) {
|
||||
try {
|
||||
LOG.debugf("Auswertung: Dok %d × Frage %d",
|
||||
doc.getId(), question.getQuestionId());
|
||||
|
||||
EvaluationResult result = evaluationService.evaluate(question, originalText);
|
||||
|
||||
// Abweichung / Hinweis aus Schwellwert + result_handling berechnen
|
||||
int deviation = 0;
|
||||
int warning = 0;
|
||||
if (result.getScore() != null && question.getThreshold() != null
|
||||
&& result.getScore() < question.getThreshold()) {
|
||||
if ("ABWEICHUNG".equalsIgnoreCase(question.getResultHandling())) {
|
||||
deviation = 1;
|
||||
} else if ("HINWEIS".equalsIgnoreCase(question.getResultHandling())) {
|
||||
warning = 1;
|
||||
}
|
||||
}
|
||||
|
||||
ResultRequest rr = new ResultRequest(
|
||||
question.getQuestionId(),
|
||||
doc.getId(),
|
||||
result.getAnswer(),
|
||||
result.getScore(),
|
||||
result.getResultType(),
|
||||
deviation,
|
||||
warning,
|
||||
result.getTheComment()
|
||||
);
|
||||
|
||||
Response saveResp = ordsClient.saveResult(projectId, rr);
|
||||
if (saveResp.getStatus() != 201) {
|
||||
LOG.warnf("Ergebnis speichern: HTTP %d (Frage %d, Dok %d)",
|
||||
saveResp.getStatus(), question.getQuestionId(), doc.getId());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Auswertungsfehler: Frage %d / Dokument %d",
|
||||
question.getQuestionId(), doc.getId());
|
||||
}
|
||||
|
||||
stepHolder[0]++;
|
||||
pushProgress(projectId, stepHolder[0], totalSteps);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Abschließen
|
||||
ordsClient.completeProject(projectId);
|
||||
LOG.infof("=== Projekt %d erfolgreich abgeschlossen ===", projectId);
|
||||
|
||||
} catch (Exception e) {
|
||||
// Kritischer Fehler: Projekt bleibt in IN_PROGRESS.
|
||||
// APEX-Oberfläche / Admin muss manuell zurücksetzen.
|
||||
LOG.errorf(e, "=== Kritischer Fehler bei Projekt %d – bleibt IN_PROGRESS ===",
|
||||
projectId);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Hilfsmethoden
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Sendet den aktuellen Fortschritt an ORDS (0–100%).
|
||||
* Fehler werden nur geloggt, kein Abbruch.
|
||||
*/
|
||||
private void pushProgress(long projectId, int step, int total) {
|
||||
int pct = total > 0 ? Math.min(step * 100 / total, 99) : 0;
|
||||
try {
|
||||
ordsClient.updateProgress(projectId, new ProgressRequest(pct));
|
||||
} catch (Exception e) {
|
||||
LOG.debugf("Fortschritt konnte nicht gesetzt werden (%d%%): %s", pct, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package de.frigosped.dc.service;
|
||||
|
||||
import dev.langchain4j.data.message.ImageContent;
|
||||
import dev.langchain4j.data.message.TextContent;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import dev.langchain4j.model.chat.ChatLanguageModel;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.apache.poi.xwpf.usermodel.*;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.odftoolkit.odfdom.doc.OdfTextDocument;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Konvertiert hochgeladene Dokumente in Markdown-Text.
|
||||
*
|
||||
* Unterstützte Formate:
|
||||
* - PDF → KI-OCR (jede Seite als Bild an quen3.5:9b)
|
||||
* - DOCX → Apache POI → Markdown
|
||||
* - ODT → ODF Toolkit → Markdown
|
||||
* - TXT → direkt als Markdown
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class DocumentProcessingService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(DocumentProcessingService.class);
|
||||
|
||||
/** 200 DPI: gutes Gleichgewicht zwischen Qualität und Dateigröße für OCR */
|
||||
private static final float OCR_DPI = 200f;
|
||||
|
||||
@Inject
|
||||
@Named("ocr")
|
||||
ChatLanguageModel ocrModel;
|
||||
|
||||
// =========================================================================
|
||||
// Öffentliche API
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Extrahiert den Textinhalt eines Dokuments als Markdown.
|
||||
*
|
||||
* @param fileBytes rohe Datei-Bytes (aus BLOB)
|
||||
* @param mimeType MIME-Typ des Dokuments
|
||||
* @param filename Dateiname (Fallback für MIME-Typ-Erkennung)
|
||||
* @return Markdown-String
|
||||
*/
|
||||
public String extractText(byte[] fileBytes, String mimeType, String filename) {
|
||||
String effectiveMime = resolveMimeType(mimeType, filename);
|
||||
LOG.debugf("Extrahiere Text: %s (%s)", filename, effectiveMime);
|
||||
|
||||
try {
|
||||
return switch (effectiveMime) {
|
||||
case "application/pdf"
|
||||
-> extractFromPdf(fileBytes);
|
||||
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/msword"
|
||||
-> extractFromDocx(fileBytes);
|
||||
case "application/vnd.oasis.opendocument.text"
|
||||
-> extractFromOdt(fileBytes);
|
||||
case "text/plain", "text/markdown"
|
||||
-> new String(fileBytes, java.nio.charset.StandardCharsets.UTF_8);
|
||||
default -> {
|
||||
LOG.warnf("Unbekannter MIME-Typ '%s' – versuche als Text", effectiveMime);
|
||||
yield new String(fileBytes, java.nio.charset.StandardCharsets.UTF_8);
|
||||
}
|
||||
};
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Fehler bei Textextraktion (%s)", filename);
|
||||
throw new RuntimeException("Textextraktion fehlgeschlagen: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// PDF: Seiten rendern + KI-OCR
|
||||
// =========================================================================
|
||||
|
||||
private String extractFromPdf(byte[] fileBytes) throws Exception {
|
||||
LOG.debug("Starte PDF-OCR...");
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
try (PDDocument pdf = Loader.loadPDF(fileBytes)) {
|
||||
PDFRenderer renderer = new PDFRenderer(pdf);
|
||||
int pageCount = pdf.getNumberOfPages();
|
||||
LOG.debugf("PDF hat %d Seite(n)", pageCount);
|
||||
|
||||
for (int i = 0; i < pageCount; i++) {
|
||||
LOG.debugf("OCR Seite %d/%d", i + 1, pageCount);
|
||||
BufferedImage image = renderer.renderImageWithDPI(i, OCR_DPI, ImageType.RGB);
|
||||
|
||||
// Bild in PNG-Base64 umwandeln
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, "PNG", baos);
|
||||
String base64Image = Base64.getEncoder().encodeToString(baos.toByteArray());
|
||||
|
||||
String pageText = ocrPage(base64Image, i + 1, pageCount);
|
||||
result.append(pageText).append("\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet eine PDF-Seite als Base64-Bild an das Vision-Modell.
|
||||
*/
|
||||
private String ocrPage(String base64Image, int pageNum, int totalPages) {
|
||||
UserMessage message = UserMessage.from(
|
||||
ImageContent.from(base64Image, "image/png"),
|
||||
TextContent.from(
|
||||
"Du bist ein OCR-Spezialist. " +
|
||||
"Extrahiere den gesamten Text aus diesem Dokument-Bild (Seite " + pageNum
|
||||
+ " von " + totalPages + ").\n\n" +
|
||||
"Regeln:\n" +
|
||||
"- Formatiere den Text als Markdown\n" +
|
||||
"- Überschriften als # / ## / ###\n" +
|
||||
"- Tabellen als Markdown-Tabellen\n" +
|
||||
"- Listen als - oder 1. 2. 3.\n" +
|
||||
"- Behalte den Originaltext exakt (keine Korrekturen, keine Zusammenfassungen)\n" +
|
||||
"- Antworte NUR mit dem extrahierten Markdown, ohne Präambel"
|
||||
)
|
||||
);
|
||||
|
||||
return ocrModel.generate(List.of(message)).content().text();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DOCX → Markdown (Apache POI)
|
||||
// =========================================================================
|
||||
|
||||
private String extractFromDocx(byte[] fileBytes) throws Exception {
|
||||
LOG.debug("Konvertiere DOCX → Markdown...");
|
||||
StringBuilder md = new StringBuilder();
|
||||
|
||||
try (XWPFDocument doc = new XWPFDocument(new ByteArrayInputStream(fileBytes))) {
|
||||
for (IBodyElement element : doc.getBodyElements()) {
|
||||
if (element instanceof XWPFParagraph para) {
|
||||
md.append(paragraphToMarkdown(para)).append("\n");
|
||||
} else if (element instanceof XWPFTable table) {
|
||||
md.append(tableToMarkdown(table)).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return md.toString().trim();
|
||||
}
|
||||
|
||||
private String paragraphToMarkdown(XWPFParagraph para) {
|
||||
String text = para.getText();
|
||||
if (text == null || text.isBlank()) return "";
|
||||
|
||||
String style = para.getStyle();
|
||||
if (style == null) return text;
|
||||
|
||||
// Überschriften
|
||||
if (style.equalsIgnoreCase("Heading1") || style.equalsIgnoreCase("berschrift1"))
|
||||
return "# " + text;
|
||||
if (style.equalsIgnoreCase("Heading2") || style.equalsIgnoreCase("berschrift2"))
|
||||
return "## " + text;
|
||||
if (style.equalsIgnoreCase("Heading3") || style.equalsIgnoreCase("berschrift3"))
|
||||
return "### " + text;
|
||||
if (style.equalsIgnoreCase("Heading4") || style.equalsIgnoreCase("berschrift4"))
|
||||
return "#### " + text;
|
||||
|
||||
// Nummerierte Liste
|
||||
if (para.getNumIlvl() != null) {
|
||||
return "- " + text;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private String tableToMarkdown(XWPFTable table) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
List<XWPFTableRow> rows = table.getRows();
|
||||
if (rows.isEmpty()) return "";
|
||||
|
||||
// Header-Zeile
|
||||
XWPFTableRow header = rows.get(0);
|
||||
sb.append("| ");
|
||||
header.getTableCells().forEach(c -> sb.append(c.getText()).append(" | "));
|
||||
sb.append("\n|");
|
||||
header.getTableCells().forEach(c -> sb.append("---|"));
|
||||
sb.append("\n");
|
||||
|
||||
// Daten-Zeilen
|
||||
for (int i = 1; i < rows.size(); i++) {
|
||||
sb.append("| ");
|
||||
rows.get(i).getTableCells().forEach(c -> sb.append(c.getText()).append(" | "));
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// ODT → Markdown (ODF Toolkit)
|
||||
// =========================================================================
|
||||
|
||||
private String extractFromOdt(byte[] fileBytes) throws Exception {
|
||||
LOG.debug("Konvertiere ODT → Markdown...");
|
||||
StringBuilder md = new StringBuilder();
|
||||
|
||||
try (OdfTextDocument odfDoc = OdfTextDocument.loadDocument(
|
||||
new ByteArrayInputStream(fileBytes))) {
|
||||
|
||||
NodeList paragraphs = odfDoc.getContentDom()
|
||||
.getElementsByTagName("text:p");
|
||||
|
||||
for (int i = 0; i < paragraphs.getLength(); i++) {
|
||||
Node node = paragraphs.item(i);
|
||||
String text = node.getTextContent();
|
||||
if (text != null && !text.isBlank()) {
|
||||
// Stil-Attribut auslesen
|
||||
Node styleAttr = node.getAttributes()
|
||||
.getNamedItem("text:style-name");
|
||||
String style = styleAttr != null ? styleAttr.getNodeValue() : "";
|
||||
|
||||
if (style.startsWith("Heading_20_1") || style.startsWith("Heading1"))
|
||||
md.append("# ").append(text).append("\n");
|
||||
else if (style.startsWith("Heading_20_2") || style.startsWith("Heading2"))
|
||||
md.append("## ").append(text).append("\n");
|
||||
else if (style.startsWith("Heading_20_3") || style.startsWith("Heading3"))
|
||||
md.append("### ").append(text).append("\n");
|
||||
else
|
||||
md.append(text).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return md.toString().trim();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Hilfsmethoden
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Leitet MIME-Typ aus Dateiendung ab, wenn der gespeicherte MIME-Typ
|
||||
* fehlt oder generic ist.
|
||||
*/
|
||||
private String resolveMimeType(String mimeType, String filename) {
|
||||
if (mimeType != null && !mimeType.isBlank()
|
||||
&& !mimeType.equals("application/octet-stream")) {
|
||||
return mimeType;
|
||||
}
|
||||
if (filename == null) return "application/octet-stream";
|
||||
String lower = filename.toLowerCase();
|
||||
if (lower.endsWith(".pdf")) return "application/pdf";
|
||||
if (lower.endsWith(".docx")) return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
||||
if (lower.endsWith(".doc")) return "application/msword";
|
||||
if (lower.endsWith(".odt")) return "application/vnd.oasis.opendocument.text";
|
||||
if (lower.endsWith(".txt")) return "text/plain";
|
||||
if (lower.endsWith(".md")) return "text/markdown";
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package de.frigosped.dc.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.frigosped.dc.model.EvaluationResult;
|
||||
import de.frigosped.dc.model.OrdsQuestion;
|
||||
import dev.langchain4j.data.message.AiMessage;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.data.message.SystemMessage;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import dev.langchain4j.model.chat.ChatLanguageModel;
|
||||
import dev.langchain4j.model.output.Response;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* KI-gestützte Auswertung:
|
||||
* 1. Übersetzung eines Dokuments ins Deutsche
|
||||
* 2. Prüfung des Dokuments gegen eine einzelne Frage
|
||||
*
|
||||
* Verwendet das Hauptmodell (gpt-oss:20b).
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class EvaluationService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(EvaluationService.class);
|
||||
|
||||
/** Regex zum Extrahieren von JSON aus der KI-Antwort (toleriert Markdown-Codeblöcke) */
|
||||
private static final Pattern JSON_PATTERN =
|
||||
Pattern.compile("```(?:json)?\\s*([\\s\\S]*?)```|([{][\\s\\S]*[}])",
|
||||
Pattern.DOTALL);
|
||||
|
||||
@Inject
|
||||
@Named("main")
|
||||
ChatLanguageModel mainModel;
|
||||
|
||||
@Inject
|
||||
ObjectMapper objectMapper;
|
||||
|
||||
// =========================================================================
|
||||
// Übersetzung
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Übersetzt einen Markdown-Text aus der Quellsprache ins Deutsche.
|
||||
* Wenn der Text bereits auf Deutsch ist, wird er unverändert zurückgegeben.
|
||||
*
|
||||
* @param originalText Markdown-Text in Originalsprache
|
||||
* @return Markdown-Text auf Deutsch
|
||||
*/
|
||||
public String translate(String originalText) {
|
||||
if (originalText == null || originalText.isBlank()) return "";
|
||||
LOG.debugf("Starte Übersetzung (%d Zeichen)...", originalText.length());
|
||||
|
||||
List<ChatMessage> messages = List.of(
|
||||
SystemMessage.from(
|
||||
"Du bist ein professioneller Übersetzer für rechtliche und logistische Dokumente.\n" +
|
||||
"Übersetze den folgenden Text ins Deutsche.\n" +
|
||||
"Regeln:\n" +
|
||||
"- Behalte die Markdown-Formatierung exakt bei\n" +
|
||||
"- Fachbegriffe korrekt übersetzen\n" +
|
||||
"- Wenn der Text bereits auf Deutsch ist, gib ihn unverändert zurück\n" +
|
||||
"- Antworte NUR mit dem übersetzten Text, ohne Kommentar oder Präambel"
|
||||
),
|
||||
UserMessage.from(originalText)
|
||||
);
|
||||
|
||||
Response<AiMessage> response = mainModel.generate(messages);
|
||||
return response.content().text().trim();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Fragen-Auswertung
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Wertet eine einzelne Frage gegen den Dokument-Text aus.
|
||||
*
|
||||
* Der Schwellwert (threshold) und die Abweichungs-Logik werden
|
||||
* im Orchestrator berechnet – die KI liefert nur score + answer + comment.
|
||||
*
|
||||
* @param question Frage aus dem Katalog (inkl. Beispiele, Schwellwert)
|
||||
* @param documentText Originaltext des Dokuments (Markdown)
|
||||
* @return Strukturiertes Auswertungsergebnis
|
||||
*/
|
||||
public EvaluationResult evaluate(OrdsQuestion question, String documentText) {
|
||||
LOG.debugf("Werte Frage %d aus: %s",
|
||||
question.getQuestionId(),
|
||||
abbreviate(question.getQuestionText(), 80));
|
||||
|
||||
String systemPrompt = buildEvaluationSystem();
|
||||
String userPrompt = buildEvaluationUser(question, documentText);
|
||||
|
||||
List<ChatMessage> messages = List.of(
|
||||
SystemMessage.from(systemPrompt),
|
||||
UserMessage.from(userPrompt)
|
||||
);
|
||||
|
||||
Response<AiMessage> response = mainModel.generate(messages);
|
||||
String rawText = response.content().text();
|
||||
|
||||
return parseEvaluationResult(rawText, question);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Prompt-Builder
|
||||
// =========================================================================
|
||||
|
||||
private String buildEvaluationSystem() {
|
||||
return """
|
||||
Du bist ein Experte für Transportrecht, AGB-Recht und Vertragsanalyse.
|
||||
Du prüfst Transport- und Speditionsdokumente gegen einen Fragenkatalog.
|
||||
|
||||
Antworte AUSSCHLIESSLICH mit einem gültigen JSON-Objekt – kein Text davor oder danach.
|
||||
Das JSON-Objekt muss folgende Felder enthalten:
|
||||
|
||||
{
|
||||
"answer": "<Deine Antwort auf Deutsch. Zitate aus dem Dokument in Originalsprache angeben und danach in Klammern übersetzen>",
|
||||
"score": <Zahl 0 bis 100, wie gut das Dokument die Anforderung erfüllt>,
|
||||
"result_type": "<OK wenn score >= Schwellwert, UNKLAR wenn grenzwertig, NOK wenn klar nicht erfüllt>",
|
||||
"the_comment": "<Kurzer fachlicher Kommentar auf Deutsch, max. 3 Sätze>"
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
private String buildEvaluationUser(OrdsQuestion question, String documentText) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("## Zu prüfendes Dokument\n\n");
|
||||
sb.append(documentText).append("\n\n");
|
||||
|
||||
sb.append("---\n\n");
|
||||
sb.append("## Prüffrage (Kategorie: ").append(question.getCategoryName()).append(")\n\n");
|
||||
sb.append(question.getQuestionText()).append("\n\n");
|
||||
|
||||
if (question.getThreshold() != null) {
|
||||
sb.append("**Schwellwert:** ").append(question.getThreshold())
|
||||
.append("% – unterhalb gilt die Anforderung als nicht erfüllt\n\n");
|
||||
}
|
||||
|
||||
if (question.getExample0Percent() != null && !question.getExample0Percent().isBlank()) {
|
||||
sb.append("**Beispiel: 0% Erfüllung (nicht ok):**\n")
|
||||
.append(question.getExample0Percent()).append("\n\n");
|
||||
}
|
||||
|
||||
if (question.getExample100Percent() != null && !question.getExample100Percent().isBlank()) {
|
||||
sb.append("**Beispiel: 100% Erfüllung (vollständig ok):**\n")
|
||||
.append(question.getExample100Percent()).append("\n\n");
|
||||
}
|
||||
|
||||
sb.append("---\n\nAntworte jetzt mit dem JSON-Objekt:");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// JSON-Parsing mit Fallback
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Parst das KI-Ergebnis zu einem EvaluationResult.
|
||||
* Toleriert Markdown-Codeblöcke und leichte Formatierungsfehler.
|
||||
*/
|
||||
private EvaluationResult parseEvaluationResult(String rawText, OrdsQuestion question) {
|
||||
try {
|
||||
String json = extractJson(rawText);
|
||||
// Jackson liest snake_case dank quarkus.jackson.property-naming-strategy=SNAKE_CASE
|
||||
EvaluationResult result = objectMapper.readValue(json, EvaluationResult.class);
|
||||
|
||||
// Validierung + Defaults
|
||||
if (result.getScore() == null) result.setScore(0);
|
||||
if (result.getResultType() == null || !isValidResultType(result.getResultType())) {
|
||||
result.setResultType(deriveResultType(result.getScore(), question.getThreshold()));
|
||||
}
|
||||
if (result.getAnswer() == null) result.setAnswer("Keine Antwort erhalten.");
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
LOG.warnf("JSON-Parsing fehlgeschlagen für Frage %d, verwende Fallback. Fehler: %s",
|
||||
question.getQuestionId(), e.getMessage());
|
||||
LOG.debugf("Rohe KI-Antwort: %s", rawText);
|
||||
|
||||
// Fallback: UNKLAR mit dem Rohtext als Antwort
|
||||
EvaluationResult fallback = new EvaluationResult();
|
||||
fallback.setAnswer(rawText != null ? rawText.trim() : "Parsing-Fehler");
|
||||
fallback.setScore(0);
|
||||
fallback.setResultType("UNKLAR");
|
||||
fallback.setTheComment("Automatische Auswertung konnte nicht strukturiert werden.");
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert den JSON-Teil aus der KI-Antwort
|
||||
* (toleriert ```json ... ``` Blöcke oder reines JSON).
|
||||
*/
|
||||
private String extractJson(String text) {
|
||||
if (text == null || text.isBlank()) {
|
||||
throw new IllegalArgumentException("Leere KI-Antwort");
|
||||
}
|
||||
|
||||
// Versuch 1: JSON in ```-Block
|
||||
Matcher m = JSON_PATTERN.matcher(text);
|
||||
if (m.find()) {
|
||||
String candidate = m.group(1) != null ? m.group(1) : m.group(2);
|
||||
if (candidate != null && !candidate.isBlank()) {
|
||||
return candidate.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Versuch 2: Alles ab erstem { bis letztem }
|
||||
int start = text.indexOf('{');
|
||||
int end = text.lastIndexOf('}');
|
||||
if (start >= 0 && end > start) {
|
||||
return text.substring(start, end + 1);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Kein JSON-Objekt in KI-Antwort gefunden");
|
||||
}
|
||||
|
||||
private boolean isValidResultType(String type) {
|
||||
return "OK".equals(type) || "UNKLAR".equals(type) || "NOK".equals(type);
|
||||
}
|
||||
|
||||
private String deriveResultType(int score, Integer threshold) {
|
||||
if (threshold == null) threshold = 70;
|
||||
if (score >= threshold) return "OK";
|
||||
if (score >= threshold / 2) return "UNKLAR";
|
||||
return "NOK";
|
||||
}
|
||||
|
||||
private String abbreviate(String s, int maxLen) {
|
||||
if (s == null) return "";
|
||||
return s.length() <= maxLen ? s : s.substring(0, maxLen) + "…";
|
||||
}
|
||||
}
|
||||
64
dc-backend/src/main/resources/application.properties
Normal file
64
dc-backend/src/main/resources/application.properties
Normal file
@@ -0,0 +1,64 @@
|
||||
# =============================================================================
|
||||
# Dokumenten-Check Backend – Konfiguration
|
||||
# =============================================================================
|
||||
|
||||
quarkus.application.name=dc-backend
|
||||
quarkus.http.port=8090
|
||||
|
||||
# =============================================================================
|
||||
# API-Key Absicherung (Env: DC_API_KEY)
|
||||
# Leer = Auth deaktiviert (nur für lokale Entwicklung)
|
||||
# =============================================================================
|
||||
dc.api.key=${DC_API_KEY:}
|
||||
|
||||
# =============================================================================
|
||||
# KI-Modelle (Ollama-kompatibler Endpunkt)
|
||||
# =============================================================================
|
||||
dc.ai.base-url=https://ollama.aquantico.de
|
||||
dc.ai.api-key=324GF44-50AA-4B57-9386-K435DLJ764DFR
|
||||
|
||||
# Hauptmodell: Auswertung + Übersetzung
|
||||
dc.ai.main.model=qwen3.6:35b-a3b-q4_K_M
|
||||
dc.ai.main.timeout=300s
|
||||
dc.ai.main.max-retries=2
|
||||
dc.ai.main.temperature=0.1
|
||||
|
||||
# OCR-Modell: Bildtext-Extraktion aus PDF-Seiten
|
||||
dc.ai.ocr.model=qwen3.6:35b-a3b-q4_K_M
|
||||
dc.ai.ocr.timeout=120s
|
||||
dc.ai.ocr.max-retries=2
|
||||
|
||||
# =============================================================================
|
||||
# ORDS REST Client
|
||||
# =============================================================================
|
||||
# Basis-URL des ORDS-Servers (Schema-Pfad anpassen)
|
||||
quarkus.rest-client.ords-api.url=http://ords:8080/ords/ai_dev
|
||||
quarkus.rest-client.ords-api.scope=jakarta.inject.Singleton
|
||||
quarkus.rest-client.ords-api.connect-timeout=10000
|
||||
quarkus.rest-client.ords-api.read-timeout=60000
|
||||
|
||||
# ORDS-Authentifizierung (OAuth2 Bearer Token – leer = kein Auth in Dev)
|
||||
dc.ords.bearer-token=
|
||||
|
||||
# =============================================================================
|
||||
# Logging
|
||||
# =============================================================================
|
||||
quarkus.log.level=INFO
|
||||
quarkus.log.category."de.frigosped".level=DEBUG
|
||||
|
||||
# KI-Kommunikation mitloggen (nur in Dev)
|
||||
%dev.dc.ai.log-requests=true
|
||||
%dev.dc.ai.log-responses=true
|
||||
%prod.dc.ai.log-requests=false
|
||||
%prod.dc.ai.log-responses=false
|
||||
|
||||
# =============================================================================
|
||||
# Jackson: snake_case für ORDS-Responses
|
||||
# =============================================================================
|
||||
quarkus.jackson.property-naming-strategy=SNAKE_CASE
|
||||
|
||||
# =============================================================================
|
||||
# OpenAPI
|
||||
# =============================================================================
|
||||
quarkus.smallrye-openapi.path=/openapi
|
||||
quarkus.swagger-ui.always-include=true
|
||||
1
dc-backend/target/build-metrics.json
Normal file
1
dc-backend/target/build-metrics.json
Normal file
File diff suppressed because one or more lines are too long
64
dc-backend/target/classes/application.properties
Normal file
64
dc-backend/target/classes/application.properties
Normal file
@@ -0,0 +1,64 @@
|
||||
# =============================================================================
|
||||
# Dokumenten-Check Backend – Konfiguration
|
||||
# =============================================================================
|
||||
|
||||
quarkus.application.name=dc-backend
|
||||
quarkus.http.port=8090
|
||||
|
||||
# =============================================================================
|
||||
# API-Key Absicherung (Env: DC_API_KEY)
|
||||
# Leer = Auth deaktiviert (nur für lokale Entwicklung)
|
||||
# =============================================================================
|
||||
dc.api.key=${DC_API_KEY:}
|
||||
|
||||
# =============================================================================
|
||||
# KI-Modelle (Ollama-kompatibler Endpunkt)
|
||||
# =============================================================================
|
||||
dc.ai.base-url=https://ollama.aquantico.de
|
||||
dc.ai.api-key=324GF44-50AA-4B57-9386-K435DLJ764DFR
|
||||
|
||||
# Hauptmodell: Auswertung + Übersetzung
|
||||
dc.ai.main.model=gpt-oss:20b
|
||||
dc.ai.main.timeout=300s
|
||||
dc.ai.main.max-retries=2
|
||||
dc.ai.main.temperature=0.1
|
||||
|
||||
# OCR-Modell: Bildtext-Extraktion aus PDF-Seiten
|
||||
dc.ai.ocr.model=quen3.5:9b
|
||||
dc.ai.ocr.timeout=120s
|
||||
dc.ai.ocr.max-retries=2
|
||||
|
||||
# =============================================================================
|
||||
# ORDS REST Client
|
||||
# =============================================================================
|
||||
# Basis-URL des ORDS-Servers (Schema-Pfad anpassen)
|
||||
quarkus.rest-client.ords-api.url=http://localhost:8080/ords/FRIGOSPED_APP
|
||||
quarkus.rest-client.ords-api.scope=jakarta.inject.Singleton
|
||||
quarkus.rest-client.ords-api.connect-timeout=10000
|
||||
quarkus.rest-client.ords-api.read-timeout=60000
|
||||
|
||||
# ORDS-Authentifizierung (OAuth2 Bearer Token – leer = kein Auth in Dev)
|
||||
dc.ords.bearer-token=
|
||||
|
||||
# =============================================================================
|
||||
# Logging
|
||||
# =============================================================================
|
||||
quarkus.log.level=INFO
|
||||
quarkus.log.category."de.frigosped".level=DEBUG
|
||||
|
||||
# KI-Kommunikation mitloggen (nur in Dev)
|
||||
%dev.dc.ai.log-requests=true
|
||||
%dev.dc.ai.log-responses=true
|
||||
%prod.dc.ai.log-requests=false
|
||||
%prod.dc.ai.log-responses=false
|
||||
|
||||
# =============================================================================
|
||||
# Jackson: snake_case für ORDS-Responses
|
||||
# =============================================================================
|
||||
quarkus.jackson.property-naming-strategy=SNAKE_CASE
|
||||
|
||||
# =============================================================================
|
||||
# OpenAPI
|
||||
# =============================================================================
|
||||
quarkus.smallrye-openapi.path=/openapi
|
||||
quarkus.swagger-ui.always-include=true
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
de/frigosped/dc/service/CheckOrchestrationService.class
|
||||
de/frigosped/dc/service/EvaluationService.class
|
||||
de/frigosped/dc/service/DocumentProcessingService.class
|
||||
de/frigosped/dc/security/ApiKeyFilter.class
|
||||
@@ -0,0 +1,16 @@
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/ai/AiModelProducer.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/client/OrdsClient.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/EvaluationResult.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/OrdsDocument.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/OrdsDocumentList.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/OrdsProject.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/OrdsQuestion.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/OrdsQuestionList.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/ProgressRequest.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/ResultRequest.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/TextsRequest.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/resource/CheckResource.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/security/ApiKeyFilter.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/service/CheckOrchestrationService.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/service/DocumentProcessingService.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/service/EvaluationService.java
|
||||
@@ -0,0 +1,12 @@
|
||||
de/frigosped/dc/model/OrdsQuestion.class
|
||||
de/frigosped/dc/model/OrdsQuestionList.class
|
||||
de/frigosped/dc/model/ProgressRequest.class
|
||||
de/frigosped/dc/client/OrdsClient.class
|
||||
de/frigosped/dc/ai/AiModelProducer.class
|
||||
de/frigosped/dc/model/OrdsDocument.class
|
||||
de/frigosped/dc/model/OrdsProject.class
|
||||
de/frigosped/dc/model/OrdsDocumentList.class
|
||||
de/frigosped/dc/model/TextsRequest.class
|
||||
de/frigosped/dc/model/ResultRequest.class
|
||||
de/frigosped/dc/model/EvaluationResult.class
|
||||
de/frigosped/dc/resource/CheckResource.class
|
||||
@@ -0,0 +1,15 @@
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/ai/AiModelProducer.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/client/OrdsClient.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/EvaluationResult.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/OrdsDocument.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/OrdsDocumentList.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/OrdsProject.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/OrdsQuestion.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/OrdsQuestionList.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/ProgressRequest.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/ResultRequest.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/TextsRequest.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/resource/CheckResource.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/service/CheckOrchestrationService.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/service/DocumentProcessingService.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/service/EvaluationService.java
|
||||
33
dc-backend/update.sh
Normal file
33
dc-backend/update.sh
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
export KUBECONFIG="$SCRIPT_DIR/../env/frigo-dev.yaml"
|
||||
|
||||
TAG="${1:-latest}"
|
||||
|
||||
echo "=== dc-backend Update ==="
|
||||
echo ""
|
||||
|
||||
# --- Build & Push ---
|
||||
echo "→ [1/3] Build & Push Image (Tag: $TAG)..."
|
||||
bash "$SCRIPT_DIR/build-and-push.sh" "$TAG"
|
||||
|
||||
# --- Apply Kubernetes Manifests ---
|
||||
echo ""
|
||||
echo "→ [2/3] Kubernetes Manifests anwenden..."
|
||||
kubectl apply -f "$SCRIPT_DIR/k8s/deployment.yaml"
|
||||
|
||||
# --- Rolling Restart (zieht neues 'latest'-Image) ---
|
||||
echo ""
|
||||
echo "→ [3/3] Rolling Restart..."
|
||||
kubectl rollout restart deployment/dc-backend -n ai-env
|
||||
|
||||
echo ""
|
||||
echo "→ Warte auf Rollout..."
|
||||
kubectl rollout status deployment/dc-backend -n ai-env --timeout=120s
|
||||
|
||||
echo ""
|
||||
echo "✓ Deployment abgeschlossen."
|
||||
echo ""
|
||||
kubectl get pods -n ai-env -l app=dc-backend
|
||||
10
env/aidev_env.bat
vendored
Normal file
10
env/aidev_env.bat
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
@echo off
|
||||
set dbuser=wksp_ai
|
||||
set dbpasswd=s!)löyx209aasgtrasdasiudkash5235FDSXljLJ
|
||||
set dbsid=freepdb1
|
||||
set dbhost=10.75.10.171
|
||||
set dbport=32710
|
||||
|
||||
set dbconnect=%dbuser%/%dbpasswd%@%dbhost%:%dbport%/%dbsid%
|
||||
set NLS_LANG=GERMAN_GERMANY.AL32UTF8
|
||||
chcp 65001
|
||||
21
env/frigo-dev.yaml
vendored
Normal file
21
env/frigo-dev.yaml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
apiVersion: v1
|
||||
kind: Config
|
||||
clusters:
|
||||
- name: "local"
|
||||
cluster:
|
||||
server: "https://rancherdev.express-o.org/k8s/clusters/local"
|
||||
insecure-skip-tls-verify: true
|
||||
|
||||
users:
|
||||
- name: "local"
|
||||
user:
|
||||
token: "kubeconfig-u-zugnvqkby7rgbsc:7f4n9tplsd9r2wpdl5bx2n72wwkmfvpvsfg56z8b88s2hq986rqsr4"
|
||||
|
||||
|
||||
contexts:
|
||||
- name: "local"
|
||||
context:
|
||||
user: "local"
|
||||
cluster: "local"
|
||||
|
||||
current-context: "local"
|
||||
0
env/history.log
vendored
Normal file
0
env/history.log
vendored
Normal file
Reference in New Issue
Block a user