Init project

This commit is contained in:
2025-08-27 12:47:23 +04:00
commit 9ee249de29
24 changed files with 2449 additions and 0 deletions

95
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
)

View 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
}

View 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
View 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
View 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
View 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()
)

View 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="Время истечения")

View 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
View 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
View 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
View 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
View 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
View 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
View 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"
}
}

View 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
View 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
View 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
View 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;
}