Init project
This commit is contained in:
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"`
|
||||
}
|
||||
Reference in New Issue
Block a user