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

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