Init project
This commit is contained in:
95
.gitignore
vendored
Normal file
95
.gitignore
vendored
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# --- OS / Editor ---
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.orig
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# --- Logs ---
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# --- Env / Secrets ---
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
*.env
|
||||||
|
*.secret
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# --- Node / Angular ---
|
||||||
|
node_modules/
|
||||||
|
frontend/node_modules/
|
||||||
|
frontend/dist/
|
||||||
|
frontend/.angular/
|
||||||
|
frontend/.cache/
|
||||||
|
coverage/
|
||||||
|
.browserslistrc*
|
||||||
|
|
||||||
|
# --- Python (doc-service) ---
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
.envrc
|
||||||
|
.docz/
|
||||||
|
# Outputs
|
||||||
|
doc-service/app/output/
|
||||||
|
|
||||||
|
# --- Go (core-service) ---
|
||||||
|
core-service/bin/
|
||||||
|
core-service/tmp/
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
# Go coverage
|
||||||
|
coverage.out
|
||||||
|
|
||||||
|
# --- Protobuf generated (optional ignore) ---
|
||||||
|
core-service/proto/**/*.pb.go
|
||||||
|
# Uncomment if generating stubs for Python/TS
|
||||||
|
# doc-service/app/proto/**
|
||||||
|
# frontend/src/app/proto/**
|
||||||
|
|
||||||
|
# --- Docker / Compose ---
|
||||||
|
**/.docker/
|
||||||
|
.dockerignore
|
||||||
|
# Local data mounts (if any)
|
||||||
|
docker/postgres/data/
|
||||||
|
|
||||||
|
# --- Build artifacts ---
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.tmp/
|
||||||
|
.temp/
|
||||||
|
|
||||||
|
# --- Misc ---
|
||||||
|
*.iml
|
||||||
|
*.DS_Store?
|
||||||
|
.sass-cache/
|
||||||
|
CACHE/
|
||||||
|
.cache/
|
||||||
|
.npm/
|
||||||
|
.parcel-cache/
|
||||||
|
|
||||||
|
# --- Reports ---
|
||||||
|
reports/
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# --- OS Trash ---
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
*.Trash-*/
|
||||||
226
PROJECT_STRUCTURE.md
Normal file
226
PROJECT_STRUCTURE.md
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
# 📁 Структура проекта ERP MVP
|
||||||
|
|
||||||
|
```
|
||||||
|
erp-mvp/
|
||||||
|
├── 📁 core-service/ # Go Backend (Core API)
|
||||||
|
│ ├── 📁 cmd/
|
||||||
|
│ │ └── main.go # Точка входа приложения
|
||||||
|
│ ├── 📁 internal/
|
||||||
|
│ │ ├── 📁 api/ # HTTP API handlers
|
||||||
|
│ │ ├── 📁 config/ # Конфигурация
|
||||||
|
│ │ ├── 📁 database/ # Работа с БД
|
||||||
|
│ │ ├── 📁 grpc/ # gRPC клиенты/серверы
|
||||||
|
│ │ ├── 📁 logger/ # Логирование
|
||||||
|
│ │ ├── 📁 models/ # Модели данных
|
||||||
|
│ │ ├── 📁 redis/ # Redis клиент
|
||||||
|
│ │ └── 📁 services/ # Бизнес-логика
|
||||||
|
│ ├── 📁 pkg/ # Публичные пакеты
|
||||||
|
│ ├── 📁 proto/ # Сгенерированные Protocol Buffers
|
||||||
|
│ ├── go.mod # Go зависимости
|
||||||
|
│ ├── go.sum # Go checksums
|
||||||
|
│ └── Dockerfile # Docker образ
|
||||||
|
│
|
||||||
|
├── 📁 doc-service/ # Python Document Service
|
||||||
|
│ ├── 📁 app/
|
||||||
|
│ │ ├── 📁 api/ # FastAPI роуты
|
||||||
|
│ │ │ ├── routes/
|
||||||
|
│ │ │ └── middleware/
|
||||||
|
│ │ ├── 📁 core/ # Основная логика
|
||||||
|
│ │ │ ├── redis_client.py
|
||||||
|
│ │ │ └── logging.py
|
||||||
|
│ │ ├── 📁 models/ # Pydantic модели
|
||||||
|
│ │ │ └── document.py
|
||||||
|
│ │ ├── 📁 services/ # Сервисы генерации документов
|
||||||
|
│ │ │ ├── pdf_service.py
|
||||||
|
│ │ │ ├── excel_service.py
|
||||||
|
│ │ │ └── qr_service.py
|
||||||
|
│ │ ├── 📁 templates/ # Jinja2 шаблоны
|
||||||
|
│ │ ├── config.py # Конфигурация
|
||||||
|
│ │ └── main.py # Точка входа
|
||||||
|
│ ├── 📁 output/ # Генерируемые документы
|
||||||
|
│ ├── requirements.txt # Python зависимости
|
||||||
|
│ └── Dockerfile # Docker образ
|
||||||
|
│
|
||||||
|
├── 📁 frontend/ # Angular PWA Frontend
|
||||||
|
│ ├── 📁 src/
|
||||||
|
│ │ ├── 📁 app/
|
||||||
|
│ │ │ ├── 📁 components/ # Angular компоненты
|
||||||
|
│ │ │ │ ├── header/
|
||||||
|
│ │ │ │ ├── qr-scanner/
|
||||||
|
│ │ │ │ ├── location-details/
|
||||||
|
│ │ │ │ ├── item-list/
|
||||||
|
│ │ │ │ └── search/
|
||||||
|
│ │ │ ├── 📁 services/ # Angular сервисы
|
||||||
|
│ │ │ │ ├── api.service.ts
|
||||||
|
│ │ │ │ ├── auth.service.ts
|
||||||
|
│ │ │ │ └── auth.interceptor.ts
|
||||||
|
│ │ │ ├── 📁 store/ # NgRx store
|
||||||
|
│ │ │ │ ├── app.reducers.ts
|
||||||
|
│ │ │ │ ├── app.effects.ts
|
||||||
|
│ │ │ │ └── app.actions.ts
|
||||||
|
│ │ │ ├── 📁 models/ # TypeScript модели
|
||||||
|
│ │ │ ├── app.component.ts
|
||||||
|
│ │ │ ├── app.module.ts
|
||||||
|
│ │ │ └── app-routing.module.ts
|
||||||
|
│ │ ├── 📁 assets/ # Статические ресурсы
|
||||||
|
│ │ ├── 📁 environments/ # Конфигурация окружений
|
||||||
|
│ │ ├── index.html
|
||||||
|
│ │ ├── main.ts
|
||||||
|
│ │ ├── styles.scss
|
||||||
|
│ │ └── manifest.webmanifest # PWA манифест
|
||||||
|
│ ├── package.json # Node.js зависимости
|
||||||
|
│ ├── angular.json # Angular конфигурация
|
||||||
|
│ ├── nginx.conf # Nginx конфигурация
|
||||||
|
│ └── Dockerfile # Docker образ
|
||||||
|
│
|
||||||
|
├── 📁 proto/ # Protocol Buffers
|
||||||
|
│ ├── core.proto # Core Service API
|
||||||
|
│ ├── document.proto # Document Service API
|
||||||
|
│ └── events.proto # Event-driven сообщения
|
||||||
|
│
|
||||||
|
├── 📁 docker/ # Docker конфигурации
|
||||||
|
│ ├── 📁 postgres/
|
||||||
|
│ │ └── init.sql # Инициализация БД
|
||||||
|
│ ├── 📁 prometheus/
|
||||||
|
│ │ └── prometheus.yml # Prometheus конфигурация
|
||||||
|
│ └── 📁 grafana/
|
||||||
|
│ └── provisioning/ # Grafana дашборды
|
||||||
|
│
|
||||||
|
├── docker-compose.yml # Основной compose файл
|
||||||
|
├── README.md # Основная документация
|
||||||
|
├── STARTUP.md # Инструкции по запуску
|
||||||
|
└── PROJECT_STRUCTURE.md # Этот файл
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Технологический стек
|
||||||
|
|
||||||
|
### Core Service (Go)
|
||||||
|
- **Framework:** Gin (HTTP сервер)
|
||||||
|
- **Database:** PostgreSQL (основная БД)
|
||||||
|
- **Cache:** Redis (кэширование)
|
||||||
|
- **Auth:** JWT (аутентификация)
|
||||||
|
- **gRPC:** Protocol Buffers (межсервисная коммуникация)
|
||||||
|
- **Logging:** logrus/zerolog (структурированное логирование)
|
||||||
|
- **Monitoring:** Prometheus (метрики)
|
||||||
|
|
||||||
|
### Document Service (Python)
|
||||||
|
- **Framework:** FastAPI (веб-фреймворк)
|
||||||
|
- **PDF:** reportlab, weasyprint (генерация PDF)
|
||||||
|
- **Excel:** openpyxl (работа с Excel)
|
||||||
|
- **Word:** python-docx (работа с Word)
|
||||||
|
- **QR:** qrcode (генерация QR-кодов)
|
||||||
|
- **Templates:** Jinja2 (шаблонизация)
|
||||||
|
- **Cache:** Redis (кэширование документов)
|
||||||
|
- **gRPC:** grpcio (межсервисная коммуникация)
|
||||||
|
|
||||||
|
### Frontend (Angular)
|
||||||
|
- **Framework:** Angular 17+ (SPA фреймворк)
|
||||||
|
- **PWA:** @angular/service-worker (Progressive Web App)
|
||||||
|
- **UI:** Angular Material (компоненты UI)
|
||||||
|
- **Styling:** Tailwind CSS (утилитарные стили)
|
||||||
|
- **QR Scanner:** @zxing/ngx-scanner (сканирование QR)
|
||||||
|
- **State:** NgRx (управление состоянием)
|
||||||
|
- **HTTP:** HttpClient (API запросы)
|
||||||
|
|
||||||
|
### Infrastructure
|
||||||
|
- **Containerization:** Docker + Docker Compose
|
||||||
|
- **Database:** PostgreSQL 15 (основная БД)
|
||||||
|
- **Cache:** Redis 7 (кэширование)
|
||||||
|
- **Gateway:** Traefik (опционально)
|
||||||
|
- **Monitoring:** Prometheus + Grafana (опционально)
|
||||||
|
- **Web Server:** Nginx (для frontend)
|
||||||
|
|
||||||
|
## 🚀 Архитектура
|
||||||
|
|
||||||
|
### Микросервисная архитектура
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ Angular PWA │ │ Go Core │ │ PostgreSQL │
|
||||||
|
│ (Frontend) │◄──►│ Service │◄──►│ (Database) │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ QR Scanner │ │ Python Doc │
|
||||||
|
│ (Camera) │ │ Service │
|
||||||
|
└─────────────────┘ └─────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Redis Cache │
|
||||||
|
│ (Documents) │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Коммуникация между сервисами
|
||||||
|
- **Frontend ↔ Core Service:** REST API (HTTP/JSON)
|
||||||
|
- **Frontend ↔ Document Service:** REST API (HTTP/JSON)
|
||||||
|
- **Core Service ↔ Document Service:** gRPC (Protocol Buffers)
|
||||||
|
- **Все сервисы ↔ Redis:** Redis Protocol (кэширование)
|
||||||
|
|
||||||
|
## 📊 База данных
|
||||||
|
|
||||||
|
### Основные таблицы
|
||||||
|
- **organizations** - Организации/компании
|
||||||
|
- **users** - Пользователи системы
|
||||||
|
- **storage_locations** - Места хранения
|
||||||
|
- **items** - Товары/материалы
|
||||||
|
- **item_placements** - Размещение товаров
|
||||||
|
- **operations** - Операции с товарами
|
||||||
|
|
||||||
|
### Индексы
|
||||||
|
- Организация-scope индексы для всех таблиц
|
||||||
|
- Индексы по типам и категориям
|
||||||
|
- Временные индексы для аналитики
|
||||||
|
|
||||||
|
## 🔐 Безопасность
|
||||||
|
|
||||||
|
### Аутентификация
|
||||||
|
- JWT токены с refresh механизмом
|
||||||
|
- Organization-scope на всех данных
|
||||||
|
- Middleware для проверки прав доступа
|
||||||
|
|
||||||
|
### Валидация
|
||||||
|
- Входная валидация всех параметров
|
||||||
|
- SQL injection protection
|
||||||
|
- XSS protection в frontend
|
||||||
|
|
||||||
|
### HTTPS
|
||||||
|
- Обязательное использование HTTPS в продакшене
|
||||||
|
- Secure cookies для JWT
|
||||||
|
- CORS настройки для PWA
|
||||||
|
|
||||||
|
## 📱 PWA Особенности
|
||||||
|
|
||||||
|
### Service Worker
|
||||||
|
- Кэширование статических ресурсов
|
||||||
|
- Offline fallback для базовых функций
|
||||||
|
- Background sync для операций
|
||||||
|
|
||||||
|
### QR Scanner
|
||||||
|
- WebRTC для доступа к камере
|
||||||
|
- Real-time распознавание QR-кодов
|
||||||
|
- Fallback на ручной ввод
|
||||||
|
|
||||||
|
### Установка
|
||||||
|
- Manifest для установки как нативное приложение
|
||||||
|
- Splash screen и иконки
|
||||||
|
- Автоматические обновления
|
||||||
|
|
||||||
|
## 🚀 Развертывание
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
- Все сервисы в отдельных контейнерах
|
||||||
|
- Автоматическая инициализация БД
|
||||||
|
- Готовые образы для быстрого старта
|
||||||
|
|
||||||
|
### Мониторинг
|
||||||
|
- Prometheus для сбора метрик
|
||||||
|
- Grafana для визуализации
|
||||||
|
- Structured logging для всех сервисов
|
||||||
|
|
||||||
|
### Масштабирование
|
||||||
|
- Горизонтальное масштабирование сервисов
|
||||||
|
- Load balancing через Traefik
|
||||||
|
- Кэширование через Redis
|
||||||
49
README.md
Normal file
49
README.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# ERP для мастеров - MVP
|
||||||
|
|
||||||
|
## 🏗️ Архитектура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
erp-mvp/
|
||||||
|
├── core-service/ # Go backend (Core API)
|
||||||
|
├── doc-service/ # Python document service
|
||||||
|
├── frontend/ # Angular PWA frontend
|
||||||
|
├── proto/ # Shared Protocol Buffers
|
||||||
|
├── docker/ # Docker configurations
|
||||||
|
└── docker-compose.yml # Main deployment file
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Быстрый старт
|
||||||
|
|
||||||
|
### Требования
|
||||||
|
- Docker & Docker Compose
|
||||||
|
- Go 1.21+
|
||||||
|
- Node.js 18+
|
||||||
|
- Python 3.11+
|
||||||
|
|
||||||
|
### Запуск
|
||||||
|
```bash
|
||||||
|
# Клонирование и настройка
|
||||||
|
git clone <repository>
|
||||||
|
cd erp-mvp
|
||||||
|
|
||||||
|
# Запуск всех сервисов
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Или разработка локально
|
||||||
|
cd core-service && go run cmd/main.go
|
||||||
|
cd doc-service && python -m uvicorn app.main:app --reload
|
||||||
|
cd frontend && npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Документация
|
||||||
|
|
||||||
|
- [Архитектура MVP](second-mind-aep/💡%20Идеи/💡%20Проекты/ERP%20для%20малых%20производств/Архитектура-MVP.md)
|
||||||
|
- [Техническое задание](second-mind-aep/💡%20Идеи/💡%20Проекты/ERP%20для%20малых%20производств/Техническое-задание-MVP.md)
|
||||||
|
- [MVP План](second-mind-aep/💡%20Идеи/💡%20Проекты/ERP%20для%20малых%20производств/MVP-План.md)
|
||||||
|
|
||||||
|
## 🔧 Технологический стек
|
||||||
|
|
||||||
|
- **Core Service:** Go (Gin) + PostgreSQL + JWT + gRPC
|
||||||
|
- **Document Service:** Python (FastAPI) + Redis + Document libraries
|
||||||
|
- **Frontend:** Angular PWA + Material UI + Tailwind CSS
|
||||||
|
- **Infrastructure:** Docker + Docker Compose + Redis + HTTPS
|
||||||
246
STARTUP.md
Normal file
246
STARTUP.md
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
# 🚀 Инструкции по запуску ERP MVP
|
||||||
|
|
||||||
|
## 📋 Требования
|
||||||
|
|
||||||
|
### Системные требования
|
||||||
|
- **Docker** 20.10+
|
||||||
|
- **Docker Compose** 2.0+
|
||||||
|
- **Git** 2.30+
|
||||||
|
|
||||||
|
### Минимальные ресурсы
|
||||||
|
- **RAM:** 4 GB
|
||||||
|
- **CPU:** 2 ядра
|
||||||
|
- **Диск:** 10 GB свободного места
|
||||||
|
|
||||||
|
## 🔧 Быстрый запуск
|
||||||
|
|
||||||
|
### 1. Клонирование репозитория
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd erp-mvp
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Настройка переменных окружения
|
||||||
|
```bash
|
||||||
|
# Создание .env файла для Core Service
|
||||||
|
cp core-service/.env.example core-service/.env
|
||||||
|
|
||||||
|
# Создание .env файла для Document Service
|
||||||
|
cp doc-service/.env.example doc-service/.env
|
||||||
|
|
||||||
|
# Редактирование переменных (опционально)
|
||||||
|
nano core-service/.env
|
||||||
|
nano doc-service/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Запуск всех сервисов
|
||||||
|
```bash
|
||||||
|
# Сборка и запуск всех контейнеров
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Просмотр логов
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Проверка статуса сервисов
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Проверка работоспособности
|
||||||
|
```bash
|
||||||
|
# Core Service API
|
||||||
|
curl http://localhost:8080/health
|
||||||
|
|
||||||
|
# Document Service API
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
curl http://localhost:3000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 Доступ к сервисам
|
||||||
|
|
||||||
|
После успешного запуска:
|
||||||
|
|
||||||
|
- **Frontend (Angular PWA):** http://localhost:3000
|
||||||
|
- **Core Service API:** http://localhost:8080
|
||||||
|
- **Document Service API:** http://localhost:8000
|
||||||
|
- **API Documentation:** http://localhost:8000/docs
|
||||||
|
- **PostgreSQL:** localhost:5432
|
||||||
|
- **Redis:** localhost:6379
|
||||||
|
|
||||||
|
## 🔍 Мониторинг (опционально)
|
||||||
|
|
||||||
|
### Запуск с мониторингом
|
||||||
|
```bash
|
||||||
|
# Запуск с Prometheus и Grafana
|
||||||
|
docker-compose --profile monitoring up -d
|
||||||
|
|
||||||
|
# Доступ к Grafana: http://localhost:3001
|
||||||
|
# Логин: admin / admin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Запуск с API Gateway
|
||||||
|
```bash
|
||||||
|
# Запуск с Traefik
|
||||||
|
docker-compose --profile gateway up -d
|
||||||
|
|
||||||
|
# Traefik Dashboard: http://localhost:8081
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Разработка
|
||||||
|
|
||||||
|
### Локальная разработка
|
||||||
|
|
||||||
|
#### Core Service (Go)
|
||||||
|
```bash
|
||||||
|
cd core-service
|
||||||
|
go mod download
|
||||||
|
go run cmd/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Document Service (Python)
|
||||||
|
```bash
|
||||||
|
cd doc-service
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Frontend (Angular)
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Горячая перезагрузка
|
||||||
|
```bash
|
||||||
|
# Пересборка и перезапуск конкретного сервиса
|
||||||
|
docker-compose up -d --build core-service
|
||||||
|
|
||||||
|
# Пересборка всех сервисов
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🗄️ База данных
|
||||||
|
|
||||||
|
### Подключение к PostgreSQL
|
||||||
|
```bash
|
||||||
|
# Через Docker
|
||||||
|
docker-compose exec postgres psql -U erp_user -d erp_mvp
|
||||||
|
|
||||||
|
# Или через внешний клиент
|
||||||
|
# Host: localhost
|
||||||
|
# Port: 5432
|
||||||
|
# Database: erp_mvp
|
||||||
|
# Username: erp_user
|
||||||
|
# Password: erp_pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Миграции
|
||||||
|
```bash
|
||||||
|
# Применение миграций (если есть)
|
||||||
|
docker-compose exec core-service ./migrate up
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 Безопасность
|
||||||
|
|
||||||
|
### Изменение паролей по умолчанию
|
||||||
|
```bash
|
||||||
|
# В docker-compose.yml измените:
|
||||||
|
# - POSTGRES_PASSWORD
|
||||||
|
# - JWT_SECRET
|
||||||
|
# - REDIS_PASSWORD (если используется)
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTPS (для продакшена)
|
||||||
|
```bash
|
||||||
|
# Добавьте SSL сертификаты
|
||||||
|
# Настройте Traefik для HTTPS
|
||||||
|
# Обновите переменные окружения
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Устранение неполадок
|
||||||
|
|
||||||
|
### Просмотр логов
|
||||||
|
```bash
|
||||||
|
# Все сервисы
|
||||||
|
docker-compose logs
|
||||||
|
|
||||||
|
# Конкретный сервис
|
||||||
|
docker-compose logs core-service
|
||||||
|
docker-compose logs doc-service
|
||||||
|
docker-compose logs frontend
|
||||||
|
|
||||||
|
# Следить за логами в реальном времени
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Перезапуск сервисов
|
||||||
|
```bash
|
||||||
|
# Перезапуск конкретного сервиса
|
||||||
|
docker-compose restart core-service
|
||||||
|
|
||||||
|
# Перезапуск всех сервисов
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Очистка данных
|
||||||
|
```bash
|
||||||
|
# Остановка и удаление контейнеров
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Удаление с данными
|
||||||
|
docker-compose down -v
|
||||||
|
|
||||||
|
# Полная очистка
|
||||||
|
docker-compose down -v --rmi all
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Проверка работоспособности
|
||||||
|
|
||||||
|
### Тестовые запросы
|
||||||
|
```bash
|
||||||
|
# Проверка Core Service
|
||||||
|
curl -X GET http://localhost:8080/health
|
||||||
|
|
||||||
|
# Проверка Document Service
|
||||||
|
curl -X GET http://localhost:8000/health
|
||||||
|
|
||||||
|
# Проверка Frontend
|
||||||
|
curl -X GET http://localhost:3000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка базы данных
|
||||||
|
```bash
|
||||||
|
# Подключение к PostgreSQL
|
||||||
|
docker-compose exec postgres psql -U erp_user -d erp_mvp -c "SELECT version();"
|
||||||
|
|
||||||
|
# Проверка таблиц
|
||||||
|
docker-compose exec postgres psql -U erp_user -d erp_mvp -c "\dt"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Продакшен развертывание
|
||||||
|
|
||||||
|
### Подготовка к продакшену
|
||||||
|
1. Измените все пароли по умолчанию
|
||||||
|
2. Настройте SSL сертификаты
|
||||||
|
3. Настройте мониторинг
|
||||||
|
4. Настройте бэкапы
|
||||||
|
5. Обновите переменные окружения
|
||||||
|
|
||||||
|
### Команды для продакшена
|
||||||
|
```bash
|
||||||
|
# Сборка оптимизированных образов
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.prod.yml build
|
||||||
|
|
||||||
|
# Запуск в продакшене
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📞 Поддержка
|
||||||
|
|
||||||
|
При возникновении проблем:
|
||||||
|
1. Проверьте логи: `docker-compose logs`
|
||||||
|
2. Убедитесь, что все порты свободны
|
||||||
|
3. Проверьте системные ресурсы
|
||||||
|
4. Обратитесь к документации проекта
|
||||||
51
core-service/Dockerfile
Normal file
51
core-service/Dockerfile
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Многоэтапная сборка для Go приложения
|
||||||
|
FROM golang:1.21-alpine AS builder
|
||||||
|
|
||||||
|
# Установка зависимостей для сборки
|
||||||
|
RUN apk add --no-cache git ca-certificates tzdata
|
||||||
|
|
||||||
|
# Установка рабочей директории
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копирование go mod файлов
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
|
||||||
|
# Скачивание зависимостей
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Копирование исходного кода
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Сборка приложения
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/main.go
|
||||||
|
|
||||||
|
# Финальный образ
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# Установка ca-certificates для HTTPS запросов
|
||||||
|
RUN apk --no-cache add ca-certificates tzdata
|
||||||
|
|
||||||
|
# Создание пользователя для безопасности
|
||||||
|
RUN addgroup -g 1001 -S appgroup && \
|
||||||
|
adduser -u 1001 -S appuser -G appgroup
|
||||||
|
|
||||||
|
# Установка рабочей директории
|
||||||
|
WORKDIR /root/
|
||||||
|
|
||||||
|
# Копирование бинарного файла из builder
|
||||||
|
COPY --from=builder /app/main .
|
||||||
|
|
||||||
|
# Копирование конфигурационных файлов
|
||||||
|
COPY --from=builder /app/config ./config
|
||||||
|
|
||||||
|
# Смена владельца файлов
|
||||||
|
RUN chown -R appuser:appgroup /root/
|
||||||
|
|
||||||
|
# Переключение на непривилегированного пользователя
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Экспорт порта
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Команда запуска
|
||||||
|
CMD ["./main"]
|
||||||
77
core-service/cmd/main.go
Normal file
77
core-service/cmd/main.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"erp-mvp/core-service/internal/api"
|
||||||
|
"erp-mvp/core-service/internal/config"
|
||||||
|
"erp-mvp/core-service/internal/database"
|
||||||
|
"erp-mvp/core-service/internal/grpc"
|
||||||
|
"erp-mvp/core-service/internal/logger"
|
||||||
|
"erp-mvp/core-service/internal/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Инициализация логгера
|
||||||
|
logger := logger.New()
|
||||||
|
|
||||||
|
// Загрузка конфигурации
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal("Failed to load config", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подключение к базе данных
|
||||||
|
db, err := database.Connect(cfg.Database)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal("Failed to connect to database", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Подключение к Redis
|
||||||
|
redisClient, err := redis.Connect(cfg.Redis)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal("Failed to connect to Redis", err)
|
||||||
|
}
|
||||||
|
defer redisClient.Close()
|
||||||
|
|
||||||
|
// Инициализация gRPC клиента для Document Service
|
||||||
|
grpcClient, err := grpc.NewDocumentServiceClient(cfg.DocumentService.URL)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal("Failed to connect to Document Service", err)
|
||||||
|
}
|
||||||
|
defer grpcClient.Close()
|
||||||
|
|
||||||
|
// Создание API сервера
|
||||||
|
server := api.NewServer(cfg, db, redisClient, grpcClient, logger)
|
||||||
|
|
||||||
|
// Запуск HTTP сервера
|
||||||
|
go func() {
|
||||||
|
logger.Info("Starting HTTP server on", cfg.Server.Port)
|
||||||
|
if err := server.Start(); err != nil && err != http.ErrServerClosed {
|
||||||
|
logger.Fatal("Failed to start server", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
|
||||||
|
logger.Info("Shutting down server...")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := server.Shutdown(ctx); err != nil {
|
||||||
|
logger.Fatal("Server forced to shutdown", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Server exited")
|
||||||
|
}
|
||||||
47
core-service/go.mod
Normal file
47
core-service/go.mod
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
module erp-mvp/core-service
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.9.1
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.0.0
|
||||||
|
github.com/lib/pq v1.10.9
|
||||||
|
github.com/go-playground/validator/v10 v10.15.5
|
||||||
|
google.golang.org/grpc v1.58.0
|
||||||
|
github.com/swaggo/gin-swagger v1.6.0
|
||||||
|
github.com/redis/go-redis/v9 v9.2.1
|
||||||
|
github.com/google/uuid v1.3.1
|
||||||
|
github.com/joho/godotenv v1.4.0
|
||||||
|
github.com/sirupsen/logrus v1.9.3
|
||||||
|
github.com/prometheus/client_golang v1.17.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/sonic v1.9.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||||
|
github.com/leodido/go-urn v1.2.4 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
|
golang.org/x/crypto v0.12.0 // indirect
|
||||||
|
golang.org/x/net v0.14.0 // indirect
|
||||||
|
golang.org/x/sys v0.11.0 // indirect
|
||||||
|
golang.org/x/text v0.12.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
|
||||||
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
103
core-service/internal/config/config.go
Normal file
103
core-service/internal/config/config.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Server ServerConfig
|
||||||
|
Database DatabaseConfig
|
||||||
|
Redis RedisConfig
|
||||||
|
JWT JWTConfig
|
||||||
|
DocumentService DocumentServiceConfig
|
||||||
|
Log LogConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerConfig struct {
|
||||||
|
Port string
|
||||||
|
Host string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
DBName string
|
||||||
|
SSLMode string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedisConfig struct {
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
Password string
|
||||||
|
DB int
|
||||||
|
}
|
||||||
|
|
||||||
|
type JWTConfig struct {
|
||||||
|
Secret string
|
||||||
|
Expiration int // в часах
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentServiceConfig struct {
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogConfig struct {
|
||||||
|
Level string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() (*Config, error) {
|
||||||
|
// Загрузка .env файла если существует
|
||||||
|
godotenv.Load()
|
||||||
|
|
||||||
|
return &Config{
|
||||||
|
Server: ServerConfig{
|
||||||
|
Port: getEnv("SERVER_PORT", "8080"),
|
||||||
|
Host: getEnv("SERVER_HOST", "0.0.0.0"),
|
||||||
|
},
|
||||||
|
Database: DatabaseConfig{
|
||||||
|
Host: getEnv("DB_HOST", "localhost"),
|
||||||
|
Port: getEnv("DB_PORT", "5432"),
|
||||||
|
User: getEnv("DB_USER", "erp_user"),
|
||||||
|
Password: getEnv("DB_PASSWORD", "erp_pass"),
|
||||||
|
DBName: getEnv("DB_NAME", "erp_mvp"),
|
||||||
|
SSLMode: getEnv("DB_SSLMODE", "disable"),
|
||||||
|
},
|
||||||
|
Redis: RedisConfig{
|
||||||
|
Host: getEnv("REDIS_HOST", "localhost"),
|
||||||
|
Port: getEnv("REDIS_PORT", "6379"),
|
||||||
|
Password: getEnv("REDIS_PASSWORD", ""),
|
||||||
|
DB: getEnvAsInt("REDIS_DB", 0),
|
||||||
|
},
|
||||||
|
JWT: JWTConfig{
|
||||||
|
Secret: getEnv("JWT_SECRET", "your-secret-key"),
|
||||||
|
Expiration: getEnvAsInt("JWT_EXPIRATION", 24),
|
||||||
|
},
|
||||||
|
DocumentService: DocumentServiceConfig{
|
||||||
|
URL: getEnv("DOC_SERVICE_URL", "http://localhost:8000"),
|
||||||
|
},
|
||||||
|
Log: LogConfig{
|
||||||
|
Level: getEnv("LOG_LEVEL", "info"),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnv(key, defaultValue string) string {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvAsInt(key string, defaultValue int) int {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
if intValue, err := strconv.Atoi(value); err == nil {
|
||||||
|
return intValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
120
core-service/internal/models/models.go
Normal file
120
core-service/internal/models/models.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Organization представляет организацию/компанию
|
||||||
|
type Organization struct {
|
||||||
|
ID uuid.UUID `json:"id" db:"id"`
|
||||||
|
Name string `json:"name" db:"name"`
|
||||||
|
Type string `json:"type" db:"type"`
|
||||||
|
Settings map[string]any `json:"settings" db:"settings"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// User представляет пользователя системы
|
||||||
|
type User struct {
|
||||||
|
ID uuid.UUID `json:"id" db:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
|
||||||
|
Email string `json:"email" db:"email"`
|
||||||
|
Role string `json:"role" db:"role"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorageLocation представляет место хранения
|
||||||
|
type StorageLocation struct {
|
||||||
|
ID uuid.UUID `json:"id" db:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
|
||||||
|
ParentID *uuid.UUID `json:"parent_id" db:"parent_id"`
|
||||||
|
Name string `json:"name" db:"name"`
|
||||||
|
Address string `json:"address" db:"address"`
|
||||||
|
Type string `json:"type" db:"type"`
|
||||||
|
Coordinates map[string]any `json:"coordinates" db:"coordinates"`
|
||||||
|
QRCode string `json:"qr_code" db:"qr_code"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item представляет товар/материал
|
||||||
|
type Item struct {
|
||||||
|
ID uuid.UUID `json:"id" db:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
|
||||||
|
Name string `json:"name" db:"name"`
|
||||||
|
Description string `json:"description" db:"description"`
|
||||||
|
Category string `json:"category" db:"category"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemPlacement представляет размещение товара в месте хранения
|
||||||
|
type ItemPlacement struct {
|
||||||
|
ID uuid.UUID `json:"id" db:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
|
||||||
|
ItemID uuid.UUID `json:"item_id" db:"item_id"`
|
||||||
|
LocationID uuid.UUID `json:"location_id" db:"location_id"`
|
||||||
|
Quantity int `json:"quantity" db:"quantity"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginRequest запрос на аутентификацию
|
||||||
|
type LoginRequest struct {
|
||||||
|
Email string `json:"email" binding:"required,email"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginResponse ответ на аутентификацию
|
||||||
|
type LoginResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
User User `json:"user"`
|
||||||
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateLocationRequest запрос на создание места хранения
|
||||||
|
type CreateLocationRequest struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
Address string `json:"address" binding:"required"`
|
||||||
|
Type string `json:"type" binding:"required"`
|
||||||
|
ParentID *uuid.UUID `json:"parent_id"`
|
||||||
|
Coordinates map[string]any `json:"coordinates"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateItemRequest запрос на создание товара
|
||||||
|
type CreateItemRequest struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Category string `json:"category" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlaceItemRequest запрос на размещение товара
|
||||||
|
type PlaceItemRequest struct {
|
||||||
|
ItemID uuid.UUID `json:"item_id" binding:"required"`
|
||||||
|
LocationID uuid.UUID `json:"location_id" binding:"required"`
|
||||||
|
Quantity int `json:"quantity" binding:"required,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchRequest запрос на поиск
|
||||||
|
type SearchRequest struct {
|
||||||
|
Query string `json:"query" binding:"required"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
LocationID *uuid.UUID `json:"location_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchResponse результат поиска
|
||||||
|
type SearchResponse struct {
|
||||||
|
Items []ItemWithLocation `json:"items"`
|
||||||
|
TotalCount int `json:"total_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemWithLocation товар с информацией о месте размещения
|
||||||
|
type ItemWithLocation struct {
|
||||||
|
Item Item `json:"item"`
|
||||||
|
Location StorageLocation `json:"location"`
|
||||||
|
Quantity int `json:"quantity"`
|
||||||
|
}
|
||||||
53
doc-service/Dockerfile
Normal file
53
doc-service/Dockerfile
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Многоэтапная сборка для Python приложения
|
||||||
|
FROM python:3.11-slim AS builder
|
||||||
|
|
||||||
|
# Установка зависимостей для сборки
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc \
|
||||||
|
g++ \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Установка рабочей директории
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копирование requirements файла
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Установка зависимостей Python
|
||||||
|
RUN pip install --no-cache-dir --user -r requirements.txt
|
||||||
|
|
||||||
|
# Финальный образ
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Установка системных зависимостей
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Создание пользователя для безопасности
|
||||||
|
RUN groupadd -r appuser && useradd -r -g appuser appuser
|
||||||
|
|
||||||
|
# Установка рабочей директории
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копирование Python пакетов из builder
|
||||||
|
COPY --from=builder /root/.local /root/.local
|
||||||
|
|
||||||
|
# Копирование исходного кода
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Создание необходимых директорий
|
||||||
|
RUN mkdir -p /app/templates /app/output && \
|
||||||
|
chown -R appuser:appuser /app
|
||||||
|
|
||||||
|
# Добавление локальных пакетов в PATH
|
||||||
|
ENV PATH=/root/.local/bin:$PATH
|
||||||
|
|
||||||
|
# Переключение на непривилегированного пользователя
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Экспорт порта
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Команда запуска
|
||||||
|
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
56
doc-service/app/config.py
Normal file
56
doc-service/app/config.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import os
|
||||||
|
from typing import List
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Загрузка .env файла
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
"""Настройки приложения"""
|
||||||
|
|
||||||
|
# Основные настройки
|
||||||
|
DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
|
||||||
|
HOST: str = os.getenv("HOST", "0.0.0.0")
|
||||||
|
PORT: int = int(os.getenv("PORT", "8000"))
|
||||||
|
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
ALLOWED_ORIGINS: List[str] = [
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://localhost:8080",
|
||||||
|
"https://localhost:3000",
|
||||||
|
"https://localhost:8080"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_HOST: str = os.getenv("REDIS_HOST", "localhost")
|
||||||
|
REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
|
||||||
|
REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "")
|
||||||
|
REDIS_DB: int = int(os.getenv("REDIS_DB", "0"))
|
||||||
|
|
||||||
|
# Core Service
|
||||||
|
CORE_SERVICE_URL: str = os.getenv("CORE_SERVICE_URL", "http://localhost:8080")
|
||||||
|
|
||||||
|
# Документы
|
||||||
|
DOCUMENTS_CACHE_TTL: int = int(os.getenv("DOCUMENTS_CACHE_TTL", "86400")) # 24 часа
|
||||||
|
MAX_DOCUMENT_SIZE: int = int(os.getenv("MAX_DOCUMENT_SIZE", "10485760")) # 10MB
|
||||||
|
|
||||||
|
# Пути для файлов
|
||||||
|
TEMPLATES_DIR: str = os.getenv("TEMPLATES_DIR", "app/templates")
|
||||||
|
OUTPUT_DIR: str = os.getenv("OUTPUT_DIR", "app/output")
|
||||||
|
|
||||||
|
# QR коды
|
||||||
|
QR_CODE_SIZE: int = int(os.getenv("QR_CODE_SIZE", "10"))
|
||||||
|
QR_CODE_BORDER: int = int(os.getenv("QR_CODE_BORDER", "2"))
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
env_file = ".env"
|
||||||
|
case_sensitive = False
|
||||||
|
|
||||||
|
# Создание экземпляра настроек
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
# Создание директорий если не существуют
|
||||||
|
os.makedirs(settings.TEMPLATES_DIR, exist_ok=True)
|
||||||
|
os.makedirs(settings.OUTPUT_DIR, exist_ok=True)
|
||||||
80
doc-service/app/main.py
Normal file
80
doc-service/app/main.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
import uvicorn
|
||||||
|
import structlog
|
||||||
|
|
||||||
|
from app.config import settings
|
||||||
|
from app.api.routes import documents, templates
|
||||||
|
from app.core.redis_client import redis_client
|
||||||
|
from app.core.logging import setup_logging
|
||||||
|
|
||||||
|
# Настройка логирования
|
||||||
|
setup_logging()
|
||||||
|
logger = structlog.get_logger()
|
||||||
|
|
||||||
|
# Создание FastAPI приложения
|
||||||
|
app = FastAPI(
|
||||||
|
title="ERP Document Service",
|
||||||
|
description="Сервис для генерации документов (PDF, Excel, Word)",
|
||||||
|
version="1.0.0",
|
||||||
|
docs_url="/docs",
|
||||||
|
redoc_url="/redoc"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Настройка CORS
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=settings.ALLOWED_ORIGINS,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Подключение роутов
|
||||||
|
app.include_router(documents.router, prefix="/api/documents", tags=["documents"])
|
||||||
|
app.include_router(templates.router, prefix="/api/templates", tags=["templates"])
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup_event():
|
||||||
|
"""Событие запуска приложения"""
|
||||||
|
logger.info("Starting Document Service")
|
||||||
|
|
||||||
|
# Подключение к Redis
|
||||||
|
await redis_client.connect()
|
||||||
|
logger.info("Connected to Redis")
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def shutdown_event():
|
||||||
|
"""Событие остановки приложения"""
|
||||||
|
logger.info("Shutting down Document Service")
|
||||||
|
|
||||||
|
# Отключение от Redis
|
||||||
|
await redis_client.disconnect()
|
||||||
|
logger.info("Disconnected from Redis")
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
"""Проверка здоровья сервиса"""
|
||||||
|
return {
|
||||||
|
"status": "healthy",
|
||||||
|
"service": "document-service",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
"""Корневой эндпоинт"""
|
||||||
|
return {
|
||||||
|
"message": "ERP Document Service",
|
||||||
|
"docs": "/docs",
|
||||||
|
"health": "/health"
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
uvicorn.run(
|
||||||
|
"app.main:app",
|
||||||
|
host=settings.HOST,
|
||||||
|
port=settings.PORT,
|
||||||
|
reload=settings.DEBUG,
|
||||||
|
log_level=settings.LOG_LEVEL.lower()
|
||||||
|
)
|
||||||
86
doc-service/app/models/document.py
Normal file
86
doc-service/app/models/document.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
from typing import Optional, Dict, Any, List
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class DocumentType(str, Enum):
|
||||||
|
"""Типы документов"""
|
||||||
|
PDF = "pdf"
|
||||||
|
EXCEL = "excel"
|
||||||
|
WORD = "word"
|
||||||
|
QR_CODE = "qr_code"
|
||||||
|
|
||||||
|
class DocumentStatus(str, Enum):
|
||||||
|
"""Статусы документов"""
|
||||||
|
PENDING = "pending"
|
||||||
|
PROCESSING = "processing"
|
||||||
|
COMPLETED = "completed"
|
||||||
|
FAILED = "failed"
|
||||||
|
|
||||||
|
class QRCodeRequest(BaseModel):
|
||||||
|
"""Запрос на генерацию QR-кода"""
|
||||||
|
location_id: str = Field(..., description="ID места хранения")
|
||||||
|
location_address: str = Field(..., description="Адрес места")
|
||||||
|
organization_id: str = Field(..., description="ID организации")
|
||||||
|
size: Optional[int] = Field(10, description="Размер QR-кода")
|
||||||
|
border: Optional[int] = Field(2, description="Размер границы")
|
||||||
|
|
||||||
|
class QRCodeResponse(BaseModel):
|
||||||
|
"""Ответ с QR-кодом"""
|
||||||
|
document_id: str = Field(..., description="ID документа")
|
||||||
|
qr_code_url: str = Field(..., description="URL для скачивания QR-кода")
|
||||||
|
qr_code_data: str = Field(..., description="Данные QR-кода")
|
||||||
|
expires_at: datetime = Field(..., description="Время истечения")
|
||||||
|
|
||||||
|
class ReportRequest(BaseModel):
|
||||||
|
"""Запрос на генерацию отчета"""
|
||||||
|
report_type: str = Field(..., description="Тип отчета")
|
||||||
|
organization_id: str = Field(..., description="ID организации")
|
||||||
|
filters: Optional[Dict[str, Any]] = Field({}, description="Фильтры для отчета")
|
||||||
|
format: DocumentType = Field(DocumentType.PDF, description="Формат отчета")
|
||||||
|
|
||||||
|
class ReportResponse(BaseModel):
|
||||||
|
"""Ответ с отчетом"""
|
||||||
|
document_id: str = Field(..., description="ID документа")
|
||||||
|
download_url: str = Field(..., description="URL для скачивания")
|
||||||
|
file_size: int = Field(..., description="Размер файла в байтах")
|
||||||
|
expires_at: datetime = Field(..., description="Время истечения")
|
||||||
|
|
||||||
|
class DocumentStatusResponse(BaseModel):
|
||||||
|
"""Ответ со статусом документа"""
|
||||||
|
document_id: str = Field(..., description="ID документа")
|
||||||
|
status: DocumentStatus = Field(..., description="Статус документа")
|
||||||
|
progress: Optional[int] = Field(None, description="Прогресс в процентах")
|
||||||
|
error_message: Optional[str] = Field(None, description="Сообщение об ошибке")
|
||||||
|
created_at: datetime = Field(..., description="Время создания")
|
||||||
|
updated_at: datetime = Field(..., description="Время обновления")
|
||||||
|
|
||||||
|
class GeneratePDFRequest(BaseModel):
|
||||||
|
"""Запрос на генерацию PDF"""
|
||||||
|
template_name: str = Field(..., description="Название шаблона")
|
||||||
|
data: Dict[str, Any] = Field(..., description="Данные для шаблона")
|
||||||
|
filename: Optional[str] = Field(None, description="Имя файла")
|
||||||
|
|
||||||
|
class GenerateExcelRequest(BaseModel):
|
||||||
|
"""Запрос на генерацию Excel"""
|
||||||
|
data: List[Dict[str, Any]] = Field(..., description="Данные для таблицы")
|
||||||
|
sheet_name: str = Field("Sheet1", description="Название листа")
|
||||||
|
filename: Optional[str] = Field(None, description="Имя файла")
|
||||||
|
|
||||||
|
class TemplateInfo(BaseModel):
|
||||||
|
"""Информация о шаблоне"""
|
||||||
|
name: str = Field(..., description="Название шаблона")
|
||||||
|
description: str = Field(..., description="Описание шаблона")
|
||||||
|
variables: List[str] = Field(..., description="Переменные шаблона")
|
||||||
|
created_at: datetime = Field(..., description="Время создания")
|
||||||
|
updated_at: datetime = Field(..., description="Время обновления")
|
||||||
|
|
||||||
|
class DocumentInfo(BaseModel):
|
||||||
|
"""Информация о документе"""
|
||||||
|
id: str = Field(..., description="ID документа")
|
||||||
|
type: DocumentType = Field(..., description="Тип документа")
|
||||||
|
status: DocumentStatus = Field(..., description="Статус документа")
|
||||||
|
filename: str = Field(..., description="Имя файла")
|
||||||
|
file_size: int = Field(..., description="Размер файла")
|
||||||
|
created_at: datetime = Field(..., description="Время создания")
|
||||||
|
expires_at: datetime = Field(..., description="Время истечения")
|
||||||
43
doc-service/requirements.txt
Normal file
43
doc-service/requirements.txt
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Web framework
|
||||||
|
fastapi==0.104.1
|
||||||
|
uvicorn==0.24.0
|
||||||
|
|
||||||
|
# PDF generation
|
||||||
|
reportlab==4.0.4
|
||||||
|
weasyprint==60.1
|
||||||
|
|
||||||
|
# Office documents
|
||||||
|
python-docx==1.1.0
|
||||||
|
openpyxl==3.1.2
|
||||||
|
|
||||||
|
# Templates
|
||||||
|
jinja2==3.1.2
|
||||||
|
|
||||||
|
# Redis client
|
||||||
|
redis==5.0.1
|
||||||
|
|
||||||
|
# gRPC
|
||||||
|
grpcio==1.59.0
|
||||||
|
protobuf==4.24.4
|
||||||
|
|
||||||
|
# Data validation
|
||||||
|
pydantic==2.4.2
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
structlog==23.2.0
|
||||||
|
|
||||||
|
# QR code generation
|
||||||
|
qrcode==7.4.2
|
||||||
|
Pillow==10.0.1
|
||||||
|
|
||||||
|
# HTTP client
|
||||||
|
httpx==0.25.1
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
fastapi-cors==0.0.6
|
||||||
|
|
||||||
|
# Monitoring
|
||||||
|
prometheus-client==0.19.0
|
||||||
179
docker/docker-compose.yml
Normal file
179
docker/docker-compose.yml
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Core Service (Go)
|
||||||
|
core-service:
|
||||||
|
build:
|
||||||
|
context: ./core-service
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
- SERVER_HOST=0.0.0.0
|
||||||
|
- SERVER_PORT=8080
|
||||||
|
- DB_HOST=postgres
|
||||||
|
- DB_PORT=5432
|
||||||
|
- DB_USER=erp_user
|
||||||
|
- DB_PASSWORD=erp_pass
|
||||||
|
- DB_NAME=erp_mvp
|
||||||
|
- DB_SSLMODE=disable
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- REDIS_PORT=6379
|
||||||
|
- REDIS_PASSWORD=
|
||||||
|
- REDIS_DB=0
|
||||||
|
- JWT_SECRET=your-super-secret-jwt-key-change-in-production
|
||||||
|
- JWT_EXPIRATION=24
|
||||||
|
- DOC_SERVICE_URL=http://doc-service:8000
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
networks:
|
||||||
|
- erp-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Document Service (Python)
|
||||||
|
doc-service:
|
||||||
|
build:
|
||||||
|
context: ./doc-service
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
environment:
|
||||||
|
- HOST=0.0.0.0
|
||||||
|
- PORT=8000
|
||||||
|
- DEBUG=false
|
||||||
|
- LOG_LEVEL=INFO
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- REDIS_PORT=6379
|
||||||
|
- REDIS_PASSWORD=
|
||||||
|
- REDIS_DB=1
|
||||||
|
- CORE_SERVICE_URL=http://core-service:8080
|
||||||
|
- DOCUMENTS_CACHE_TTL=86400
|
||||||
|
- MAX_DOCUMENT_SIZE=10485760
|
||||||
|
- TEMPLATES_DIR=/app/templates
|
||||||
|
- OUTPUT_DIR=/app/output
|
||||||
|
- QR_CODE_SIZE=10
|
||||||
|
- QR_CODE_BORDER=2
|
||||||
|
volumes:
|
||||||
|
- doc_templates:/app/templates
|
||||||
|
- doc_output:/app/output
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
networks:
|
||||||
|
- erp-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Frontend (Angular)
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "3000:80"
|
||||||
|
environment:
|
||||||
|
- API_URL=http://localhost:8080
|
||||||
|
- DOC_SERVICE_URL=http://localhost:8000
|
||||||
|
depends_on:
|
||||||
|
- core-service
|
||||||
|
networks:
|
||||||
|
- erp-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Database (PostgreSQL)
|
||||||
|
postgres:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=erp_mvp
|
||||||
|
- POSTGRES_USER=erp_user
|
||||||
|
- POSTGRES_PASSWORD=erp_pass
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
networks:
|
||||||
|
- erp-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Cache (Redis)
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
command: redis-server --appendonly yes
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
- erp-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# API Gateway (Traefik) - опционально
|
||||||
|
traefik:
|
||||||
|
image: traefik:v2.10
|
||||||
|
command:
|
||||||
|
- --api.insecure=true
|
||||||
|
- --providers.docker=true
|
||||||
|
- --providers.docker.exposedbydefault=false
|
||||||
|
- --entrypoints.web.address=:80
|
||||||
|
- --entrypoints.websecure.address=:443
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
- "8081:8080"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
networks:
|
||||||
|
- erp-network
|
||||||
|
restart: unless-stopped
|
||||||
|
profiles:
|
||||||
|
- gateway
|
||||||
|
|
||||||
|
# Monitoring (Prometheus)
|
||||||
|
prometheus:
|
||||||
|
image: prom/prometheus:latest
|
||||||
|
ports:
|
||||||
|
- "9090:9090"
|
||||||
|
volumes:
|
||||||
|
- ./docker/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
|
- prometheus_data:/prometheus
|
||||||
|
command:
|
||||||
|
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||||
|
- '--storage.tsdb.path=/prometheus'
|
||||||
|
- '--web.console.libraries=/etc/prometheus/console_libraries'
|
||||||
|
- '--web.console.templates=/etc/prometheus/consoles'
|
||||||
|
- '--storage.tsdb.retention.time=200h'
|
||||||
|
- '--web.enable-lifecycle'
|
||||||
|
networks:
|
||||||
|
- erp-network
|
||||||
|
restart: unless-stopped
|
||||||
|
profiles:
|
||||||
|
- monitoring
|
||||||
|
|
||||||
|
# Monitoring (Grafana)
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana:latest
|
||||||
|
ports:
|
||||||
|
- "3001:3000"
|
||||||
|
environment:
|
||||||
|
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||||
|
volumes:
|
||||||
|
- grafana_data:/var/lib/grafana
|
||||||
|
- ./docker/grafana/provisioning:/etc/grafana/provisioning
|
||||||
|
networks:
|
||||||
|
- erp-network
|
||||||
|
restart: unless-stopped
|
||||||
|
profiles:
|
||||||
|
- monitoring
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
redis_data:
|
||||||
|
doc_templates:
|
||||||
|
doc_output:
|
||||||
|
prometheus_data:
|
||||||
|
grafana_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
erp-network:
|
||||||
|
driver: bridge
|
||||||
124
docker/postgres/init.sql
Normal file
124
docker/postgres/init.sql
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
-- Инициализация базы данных ERP MVP
|
||||||
|
|
||||||
|
-- Создание расширений
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
-- Создание таблиц
|
||||||
|
|
||||||
|
-- Организации
|
||||||
|
CREATE TABLE IF NOT EXISTS organizations (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
type VARCHAR(100),
|
||||||
|
settings JSONB DEFAULT '{}',
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Пользователи
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
role VARCHAR(50) DEFAULT 'user',
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Места хранения
|
||||||
|
CREATE TABLE IF NOT EXISTS storage_locations (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
parent_id UUID REFERENCES storage_locations(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
address VARCHAR(100) NOT NULL,
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
coordinates JSONB DEFAULT '{}',
|
||||||
|
qr_code VARCHAR(255),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Товары
|
||||||
|
CREATE TABLE IF NOT EXISTS items (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
category VARCHAR(100),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Размещение товаров
|
||||||
|
CREATE TABLE IF NOT EXISTS item_placements (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
item_id UUID NOT NULL REFERENCES items(id) ON DELETE CASCADE,
|
||||||
|
location_id UUID NOT NULL REFERENCES storage_locations(id) ON DELETE CASCADE,
|
||||||
|
quantity INTEGER DEFAULT 1,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Операции
|
||||||
|
CREATE TABLE IF NOT EXISTS operations (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
operation_type VARCHAR(50) NOT NULL,
|
||||||
|
item_id UUID REFERENCES items(id) ON DELETE SET NULL,
|
||||||
|
from_location_id UUID REFERENCES storage_locations(id) ON DELETE SET NULL,
|
||||||
|
to_location_id UUID REFERENCES storage_locations(id) ON DELETE SET NULL,
|
||||||
|
quantity INTEGER,
|
||||||
|
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Создание индексов для производительности
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_organization_id ON users(organization_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_storage_locations_organization_id ON storage_locations(organization_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_storage_locations_parent_id ON storage_locations(parent_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_storage_locations_type ON storage_locations(type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_items_organization_id ON items(organization_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_items_category ON items(category);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_item_placements_organization_id ON item_placements(organization_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_item_placements_item_id ON item_placements(item_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_item_placements_location_id ON item_placements(location_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_operations_organization_id ON operations(organization_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_operations_created_at ON operations(created_at);
|
||||||
|
|
||||||
|
-- Создание триггеров для автоматического обновления updated_at
|
||||||
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = NOW();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ language 'plpgsql';
|
||||||
|
|
||||||
|
CREATE TRIGGER update_organizations_updated_at BEFORE UPDATE ON organizations
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_storage_locations_updated_at BEFORE UPDATE ON storage_locations
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_items_updated_at BEFORE UPDATE ON items
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_item_placements_updated_at BEFORE UPDATE ON item_placements
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
-- Создание тестовых данных (опционально)
|
||||||
|
INSERT INTO organizations (id, name, type) VALUES
|
||||||
|
('550e8400-e29b-41d4-a716-446655440000', 'Тестовая мастерская', 'workshop')
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO users (id, organization_id, email, password_hash, role) VALUES
|
||||||
|
('550e8400-e29b-41d4-a716-446655440001', '550e8400-e29b-41d4-a716-446655440000', 'admin@test.com', '$2a$10$example', 'admin')
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
45
frontend/Dockerfile
Normal file
45
frontend/Dockerfile
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Многоэтапная сборка для Angular приложения
|
||||||
|
FROM node:18-alpine AS builder
|
||||||
|
|
||||||
|
# Установка рабочей директории
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копирование package файлов
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Установка зависимостей
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
# Копирование исходного кода
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Сборка приложения
|
||||||
|
RUN npm run build:prod
|
||||||
|
|
||||||
|
# Финальный образ с nginx
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Копирование собранного приложения
|
||||||
|
COPY --from=builder /app/dist/erp-mvp-frontend /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Копирование конфигурации nginx
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# Создание пользователя для безопасности
|
||||||
|
RUN addgroup -g 1001 -S appgroup && \
|
||||||
|
adduser -u 1001 -S appuser -G appgroup
|
||||||
|
|
||||||
|
# Смена владельца файлов
|
||||||
|
RUN chown -R appuser:appgroup /usr/share/nginx/html && \
|
||||||
|
chown -R appuser:appgroup /var/cache/nginx && \
|
||||||
|
chown -R appuser:appgroup /var/log/nginx && \
|
||||||
|
chown -R appuser:appgroup /etc/nginx/conf.d
|
||||||
|
|
||||||
|
# Переключение на непривилегированного пользователя
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Экспорт порта
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Команда запуска
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
141
frontend/angular.json
Normal file
141
frontend/angular.json
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"erp-mvp-frontend": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss",
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:class": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:directive": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:pipe": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:resolver": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:service": {
|
||||||
|
"skipTests": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/erp-mvp-frontend",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets",
|
||||||
|
"src/manifest.webmanifest"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"@angular/material/prebuilt-themes/indigo-pink.css",
|
||||||
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "2mb",
|
||||||
|
"maximumError": "5mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "6kb",
|
||||||
|
"maximumError": "10kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all",
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildOptimizer": false,
|
||||||
|
"optimization": false,
|
||||||
|
"vendorChunk": true,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"namedChunks": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "erp-mvp-frontend:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"browserTarget": "erp-mvp-frontend:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "erp-mvp-frontend:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js",
|
||||||
|
"zone.js/testing"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"@angular/material/prebuilt-themes/indigo-pink.css",
|
||||||
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"builder": "@angular-eslint/builder:lint",
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.html"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"analytics": false
|
||||||
|
}
|
||||||
|
}
|
||||||
91
frontend/nginx.conf
Normal file
91
frontend/nginx.conf
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
# Логирование
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
|
||||||
|
# Основные настройки
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
types_hash_max_size 2048;
|
||||||
|
|
||||||
|
# Gzip сжатие
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
text/xml
|
||||||
|
text/javascript
|
||||||
|
application/json
|
||||||
|
application/javascript
|
||||||
|
application/xml+rss
|
||||||
|
application/atom+xml
|
||||||
|
image/svg+xml;
|
||||||
|
|
||||||
|
# Основной сервер
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Безопасность
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||||
|
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
|
||||||
|
|
||||||
|
# Кэширование статических файлов
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Angular routing - fallback на index.html
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# API проксирование (опционально)
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://core-service:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Document Service проксирование
|
||||||
|
location /doc-api/ {
|
||||||
|
proxy_pass http://doc-service:8000/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
return 200 "healthy\n";
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
67
frontend/package.json
Normal file
67
frontend/package.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"name": "erp-mvp-frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "ERP для мастеров - Angular PWA Frontend",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"build:prod": "ng build --configuration production",
|
||||||
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"test": "ng test",
|
||||||
|
"lint": "ng lint",
|
||||||
|
"e2e": "ng e2e",
|
||||||
|
"pwa:build": "ng build --configuration production && ng add @angular/pwa"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "^17.0.0",
|
||||||
|
"@angular/common": "^17.0.0",
|
||||||
|
"@angular/compiler": "^17.0.0",
|
||||||
|
"@angular/core": "^17.0.0",
|
||||||
|
"@angular/forms": "^17.0.0",
|
||||||
|
"@angular/platform-browser": "^17.0.0",
|
||||||
|
"@angular/platform-browser-dynamic": "^17.0.0",
|
||||||
|
"@angular/router": "^17.0.0",
|
||||||
|
"@angular/service-worker": "^17.0.0",
|
||||||
|
"@angular/material": "^17.0.0",
|
||||||
|
"@angular/cdk": "^17.0.0",
|
||||||
|
"@zxing/ngx-scanner": "^3.0.0",
|
||||||
|
"@ngrx/store": "^17.0.0",
|
||||||
|
"@ngrx/effects": "^17.0.0",
|
||||||
|
"@ngrx/entity": "^17.0.0",
|
||||||
|
"@ngrx/store-devtools": "^17.0.0",
|
||||||
|
"rxjs": "^7.8.0",
|
||||||
|
"tslib": "^2.3.0",
|
||||||
|
"zone.js": "~0.14.2",
|
||||||
|
"tailwindcss": "^3.3.0",
|
||||||
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"postcss": "^8.4.31"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "^17.0.0",
|
||||||
|
"@angular/cli": "^17.0.0",
|
||||||
|
"@angular/compiler-cli": "^17.0.0",
|
||||||
|
"@angular/language-service": "^17.0.0",
|
||||||
|
"@types/jasmine": "~5.1.0",
|
||||||
|
"@types/node": "^18.0.0",
|
||||||
|
"jasmine-core": "~5.1.0",
|
||||||
|
"karma": "~6.4.0",
|
||||||
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
|
"karma-coverage": "~2.2.0",
|
||||||
|
"karma-jasmine": "~5.1.0",
|
||||||
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
|
"typescript": "~5.2.2",
|
||||||
|
"eslint": "^8.0.0",
|
||||||
|
"@angular-eslint/eslint-plugin": "^17.0.0",
|
||||||
|
"@angular-eslint/eslint-plugin-template": "^17.0.0",
|
||||||
|
"@angular-eslint/template-parser": "^17.0.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
|
"@typescript-eslint/parser": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0",
|
||||||
|
"npm": ">=9.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
100
frontend/src/app/app.module.ts
Normal file
100
frontend/src/app/app.module.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
// Angular Material
|
||||||
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
|
import { MatListModule } from '@angular/material/list';
|
||||||
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
|
import { MatBadgeModule } from '@angular/material/badge';
|
||||||
|
|
||||||
|
// NgRx
|
||||||
|
import { StoreModule } from '@ngrx/store';
|
||||||
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
|
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
||||||
|
|
||||||
|
// QR Scanner
|
||||||
|
import { ZXingScannerModule } from '@zxing/ngx-scanner';
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
|
import { HeaderComponent } from './components/header/header.component';
|
||||||
|
import { QRScannerComponent } from './components/qr-scanner/qr-scanner.component';
|
||||||
|
import { LocationDetailsComponent } from './components/location-details/location-details.component';
|
||||||
|
import { ItemListComponent } from './components/item-list/item-list.component';
|
||||||
|
import { SearchComponent } from './components/search/search.component';
|
||||||
|
|
||||||
|
// Services
|
||||||
|
import { AuthInterceptor } from './services/auth.interceptor';
|
||||||
|
|
||||||
|
// Store
|
||||||
|
import { appReducers } from './store/app.reducers';
|
||||||
|
import { AppEffects } from './store/app.effects';
|
||||||
|
|
||||||
|
// Environment
|
||||||
|
import { environment } from '../environments/environment';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent,
|
||||||
|
HeaderComponent,
|
||||||
|
QRScannerComponent,
|
||||||
|
LocationDetailsComponent,
|
||||||
|
ItemListComponent,
|
||||||
|
SearchComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
HttpClientModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
AppRoutingModule,
|
||||||
|
|
||||||
|
// Angular Material
|
||||||
|
MatToolbarModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatSnackBarModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
MatDialogModule,
|
||||||
|
MatListModule,
|
||||||
|
MatChipsModule,
|
||||||
|
MatBadgeModule,
|
||||||
|
|
||||||
|
// QR Scanner
|
||||||
|
ZXingScannerModule,
|
||||||
|
|
||||||
|
// NgRx
|
||||||
|
StoreModule.forRoot(appReducers),
|
||||||
|
EffectsModule.forRoot([AppEffects]),
|
||||||
|
StoreDevtoolsModule.instrument({
|
||||||
|
maxAge: 25,
|
||||||
|
logOnly: environment.production
|
||||||
|
})
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: AuthInterceptor,
|
||||||
|
multi: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
bootstrap: [AppComponent]
|
||||||
|
})
|
||||||
|
export class AppModule { }
|
||||||
12
frontend/src/main.ts
Normal file
12
frontend/src/main.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||||
|
import { enableProdMode } from '@angular/core';
|
||||||
|
|
||||||
|
import { AppModule } from './app/app.module';
|
||||||
|
import { environment } from './environments/environment';
|
||||||
|
|
||||||
|
if (environment.production) {
|
||||||
|
enableProdMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||||
|
.catch(err => console.error(err));
|
||||||
197
proto/core.proto
Normal file
197
proto/core.proto
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package erp.core;
|
||||||
|
|
||||||
|
option go_package = "erp-mvp/core-service/proto/core";
|
||||||
|
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
// Сервис для работы с местами хранения
|
||||||
|
service LocationService {
|
||||||
|
rpc GetLocation(GetLocationRequest) returns (LocationResponse);
|
||||||
|
rpc CreateLocation(CreateLocationRequest) returns (LocationResponse);
|
||||||
|
rpc UpdateLocation(UpdateLocationRequest) returns (LocationResponse);
|
||||||
|
rpc DeleteLocation(DeleteLocationRequest) returns (DeleteLocationResponse);
|
||||||
|
rpc ListLocations(ListLocationsRequest) returns (ListLocationsResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сервис для работы с товарами
|
||||||
|
service ItemService {
|
||||||
|
rpc GetItem(GetItemRequest) returns (ItemResponse);
|
||||||
|
rpc CreateItem(CreateItemRequest) returns (ItemResponse);
|
||||||
|
rpc UpdateItem(UpdateItemRequest) returns (ItemResponse);
|
||||||
|
rpc DeleteItem(DeleteItemRequest) returns (DeleteItemResponse);
|
||||||
|
rpc ListItems(ListItemsRequest) returns (ListItemsResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сервис для операций
|
||||||
|
service OperationService {
|
||||||
|
rpc PlaceItem(PlaceItemRequest) returns (OperationResponse);
|
||||||
|
rpc MoveItem(MoveItemRequest) returns (OperationResponse);
|
||||||
|
rpc SearchItems(SearchItemsRequest) returns (SearchItemsResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запросы для мест хранения
|
||||||
|
message GetLocationRequest {
|
||||||
|
string location_id = 1;
|
||||||
|
string organization_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateLocationRequest {
|
||||||
|
string organization_id = 1;
|
||||||
|
string name = 2;
|
||||||
|
string address = 3;
|
||||||
|
string type = 4;
|
||||||
|
optional string parent_id = 5;
|
||||||
|
map<string, string> coordinates = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateLocationRequest {
|
||||||
|
string location_id = 1;
|
||||||
|
string organization_id = 2;
|
||||||
|
optional string name = 3;
|
||||||
|
optional string address = 4;
|
||||||
|
optional string type = 5;
|
||||||
|
optional string parent_id = 6;
|
||||||
|
map<string, string> coordinates = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteLocationRequest {
|
||||||
|
string location_id = 1;
|
||||||
|
string organization_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListLocationsRequest {
|
||||||
|
string organization_id = 1;
|
||||||
|
optional string parent_id = 2;
|
||||||
|
optional string type = 3;
|
||||||
|
int32 page = 4;
|
||||||
|
int32 page_size = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запросы для товаров
|
||||||
|
message GetItemRequest {
|
||||||
|
string item_id = 1;
|
||||||
|
string organization_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateItemRequest {
|
||||||
|
string organization_id = 1;
|
||||||
|
string name = 2;
|
||||||
|
string description = 3;
|
||||||
|
string category = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateItemRequest {
|
||||||
|
string item_id = 1;
|
||||||
|
string organization_id = 2;
|
||||||
|
optional string name = 3;
|
||||||
|
optional string description = 4;
|
||||||
|
optional string category = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteItemRequest {
|
||||||
|
string item_id = 1;
|
||||||
|
string organization_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListItemsRequest {
|
||||||
|
string organization_id = 1;
|
||||||
|
optional string category = 2;
|
||||||
|
optional string search_query = 3;
|
||||||
|
int32 page = 4;
|
||||||
|
int32 page_size = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запросы для операций
|
||||||
|
message PlaceItemRequest {
|
||||||
|
string organization_id = 1;
|
||||||
|
string item_id = 2;
|
||||||
|
string location_id = 3;
|
||||||
|
int32 quantity = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MoveItemRequest {
|
||||||
|
string organization_id = 1;
|
||||||
|
string item_id = 2;
|
||||||
|
string from_location_id = 3;
|
||||||
|
string to_location_id = 4;
|
||||||
|
int32 quantity = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SearchItemsRequest {
|
||||||
|
string organization_id = 1;
|
||||||
|
string query = 2;
|
||||||
|
optional string category = 3;
|
||||||
|
optional string location_id = 4;
|
||||||
|
int32 page = 5;
|
||||||
|
int32 page_size = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ответы
|
||||||
|
message LocationResponse {
|
||||||
|
string id = 1;
|
||||||
|
string organization_id = 2;
|
||||||
|
optional string parent_id = 3;
|
||||||
|
string name = 4;
|
||||||
|
string address = 5;
|
||||||
|
string type = 6;
|
||||||
|
map<string, string> coordinates = 7;
|
||||||
|
string qr_code = 8;
|
||||||
|
google.protobuf.Timestamp created_at = 9;
|
||||||
|
google.protobuf.Timestamp updated_at = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListLocationsResponse {
|
||||||
|
repeated LocationResponse locations = 1;
|
||||||
|
int32 total_count = 2;
|
||||||
|
int32 page = 3;
|
||||||
|
int32 page_size = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteLocationResponse {
|
||||||
|
bool success = 1;
|
||||||
|
string message = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ItemResponse {
|
||||||
|
string id = 1;
|
||||||
|
string organization_id = 2;
|
||||||
|
string name = 3;
|
||||||
|
string description = 4;
|
||||||
|
string category = 5;
|
||||||
|
google.protobuf.Timestamp created_at = 6;
|
||||||
|
google.protobuf.Timestamp updated_at = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListItemsResponse {
|
||||||
|
repeated ItemResponse items = 1;
|
||||||
|
int32 total_count = 2;
|
||||||
|
int32 page = 3;
|
||||||
|
int32 page_size = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteItemResponse {
|
||||||
|
bool success = 1;
|
||||||
|
string message = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OperationResponse {
|
||||||
|
bool success = 1;
|
||||||
|
string message = 2;
|
||||||
|
string operation_id = 3;
|
||||||
|
google.protobuf.Timestamp created_at = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SearchItemsResponse {
|
||||||
|
repeated ItemWithLocation items = 1;
|
||||||
|
int32 total_count = 2;
|
||||||
|
int32 page = 3;
|
||||||
|
int32 page_size = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ItemWithLocation {
|
||||||
|
ItemResponse item = 1;
|
||||||
|
LocationResponse location = 2;
|
||||||
|
int32 quantity = 3;
|
||||||
|
}
|
||||||
161
proto/document.proto
Normal file
161
proto/document.proto
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package erp.document;
|
||||||
|
|
||||||
|
option go_package = "erp-mvp/core-service/proto/document";
|
||||||
|
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
// Сервис для генерации документов
|
||||||
|
service DocumentService {
|
||||||
|
rpc GenerateQRCode(QRCodeRequest) returns (QRCodeResponse);
|
||||||
|
rpc GenerateReport(ReportRequest) returns (ReportResponse);
|
||||||
|
rpc GetDocumentStatus(StatusRequest) returns (StatusResponse);
|
||||||
|
rpc GetDocumentDownload(DownloadRequest) returns (DownloadResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сервис для работы с шаблонами
|
||||||
|
service TemplateService {
|
||||||
|
rpc GetTemplate(TemplateRequest) returns (TemplateResponse);
|
||||||
|
rpc ListTemplates(ListTemplatesRequest) returns (ListTemplatesResponse);
|
||||||
|
rpc CreateTemplate(CreateTemplateRequest) returns (TemplateResponse);
|
||||||
|
rpc UpdateTemplate(UpdateTemplateRequest) returns (TemplateResponse);
|
||||||
|
rpc DeleteTemplate(DeleteTemplateRequest) returns (DeleteTemplateResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запросы для QR-кодов
|
||||||
|
message QRCodeRequest {
|
||||||
|
string location_id = 1;
|
||||||
|
string location_address = 2;
|
||||||
|
string organization_id = 3;
|
||||||
|
int32 size = 4;
|
||||||
|
int32 border = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message QRCodeResponse {
|
||||||
|
string document_id = 1;
|
||||||
|
string qr_code_url = 2;
|
||||||
|
string qr_code_data = 3;
|
||||||
|
google.protobuf.Timestamp expires_at = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запросы для отчетов
|
||||||
|
message ReportRequest {
|
||||||
|
string report_type = 1;
|
||||||
|
string organization_id = 2;
|
||||||
|
map<string, string> filters = 3;
|
||||||
|
DocumentFormat format = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ReportResponse {
|
||||||
|
string document_id = 1;
|
||||||
|
string download_url = 2;
|
||||||
|
int64 file_size = 3;
|
||||||
|
google.protobuf.Timestamp expires_at = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запросы для статуса документа
|
||||||
|
message StatusRequest {
|
||||||
|
string document_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StatusResponse {
|
||||||
|
string document_id = 1;
|
||||||
|
DocumentStatus status = 2;
|
||||||
|
int32 progress = 3;
|
||||||
|
string error_message = 4;
|
||||||
|
google.protobuf.Timestamp created_at = 5;
|
||||||
|
google.protobuf.Timestamp updated_at = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запросы для скачивания
|
||||||
|
message DownloadRequest {
|
||||||
|
string document_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DownloadResponse {
|
||||||
|
string document_id = 1;
|
||||||
|
string download_url = 2;
|
||||||
|
int64 file_size = 3;
|
||||||
|
string filename = 4;
|
||||||
|
DocumentFormat format = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запросы для шаблонов
|
||||||
|
message TemplateRequest {
|
||||||
|
string template_name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TemplateResponse {
|
||||||
|
string name = 1;
|
||||||
|
string description = 2;
|
||||||
|
repeated string variables = 3;
|
||||||
|
string content = 4;
|
||||||
|
google.protobuf.Timestamp created_at = 5;
|
||||||
|
google.protobuf.Timestamp updated_at = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListTemplatesRequest {
|
||||||
|
string category = 1;
|
||||||
|
int32 page = 2;
|
||||||
|
int32 page_size = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListTemplatesResponse {
|
||||||
|
repeated TemplateInfo templates = 1;
|
||||||
|
int32 total_count = 2;
|
||||||
|
int32 page = 3;
|
||||||
|
int32 page_size = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateTemplateRequest {
|
||||||
|
string name = 1;
|
||||||
|
string description = 2;
|
||||||
|
string content = 3;
|
||||||
|
string category = 4;
|
||||||
|
repeated string variables = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateTemplateRequest {
|
||||||
|
string name = 1;
|
||||||
|
optional string description = 2;
|
||||||
|
optional string content = 3;
|
||||||
|
optional string category = 4;
|
||||||
|
repeated string variables = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteTemplateRequest {
|
||||||
|
string name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteTemplateResponse {
|
||||||
|
bool success = 1;
|
||||||
|
string message = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Информация о шаблоне
|
||||||
|
message TemplateInfo {
|
||||||
|
string name = 1;
|
||||||
|
string description = 2;
|
||||||
|
string category = 3;
|
||||||
|
repeated string variables = 4;
|
||||||
|
google.protobuf.Timestamp created_at = 5;
|
||||||
|
google.protobuf.Timestamp updated_at = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Перечисления
|
||||||
|
enum DocumentFormat {
|
||||||
|
DOCUMENT_FORMAT_UNSPECIFIED = 0;
|
||||||
|
DOCUMENT_FORMAT_PDF = 1;
|
||||||
|
DOCUMENT_FORMAT_EXCEL = 2;
|
||||||
|
DOCUMENT_FORMAT_WORD = 3;
|
||||||
|
DOCUMENT_FORMAT_QR_CODE = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DocumentStatus {
|
||||||
|
DOCUMENT_STATUS_UNSPECIFIED = 0;
|
||||||
|
DOCUMENT_STATUS_PENDING = 1;
|
||||||
|
DOCUMENT_STATUS_PROCESSING = 2;
|
||||||
|
DOCUMENT_STATUS_COMPLETED = 3;
|
||||||
|
DOCUMENT_STATUS_FAILED = 4;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user