Перейти к основному содержимому

Business Services Layer — Бизнес-сервисы

Версия: 1.0
Дата: 19.04.2026
Статус: Черновик

Обзор

Business Services реализуют бизнес-логику через микросервисы на Go + gRPC согласно разделу "### 4. Business services (Бизнес-сервисы)" в Архитектура платформы. Сервисы взаимодействуют с PostgreSQL и Redis структурами из Storage Layer и обрабатывают запросы между API Gateway и хранилищем данных согласно "### Поток пользовательских запросов" в Архитектура платформы.

Архитектура сервисов: См. диаграмму vitrip_business_services.jpg — схема 4 основных сервисов с gRPC интерфейсами и зависимостями.

Архитектура бизнес-сервисов vitrip.store

Интеграция с данными: См. диаграмму vitrip_services_data_flow.jpg — как сервисы используют PostgreSQL схемы и Redis кеши из Storage Layer.

Интеграция сервисов с данными vitrip.store

Поток бронирования: См. диаграмму vitrip_booking_flow.jpg — детальный процесс бронирования с интеграцией Supplier API согласно разделу "### Поток бронирования" в Архитектура платформы.

Детальный поток бронирования vitrip.store

Архитектура сервисов

Принципы проектирования

Single Responsibility: Каждый сервис отвечает за одну бизнес-область

  • Search Service — поиск и фильтрация отелей
  • Pricing Service — ценообразование и валютные операции
  • Booking Service — управление бронированиями
  • Tour Builder Service — конструктор туристических программ

Technology Stack: Go + gRPC согласно технологическому стеку из Архитектура платформы:

// Стандартная структура микросервиса
type Service struct {
db *postgresql.Pool // Подключение к PostgreSQL
cache *redis.Client // Подключение к Redis
nats *nats.Conn // Message queue для событий
logger *zap.Logger // Структурированное логирование
}

Search Service

Назначение

Поиск отелей по параметрам, фильтрация, ранжирование результатов. Использует PostGIS для геопоиска и кеширует результаты согласно TTL стратегиям из Storage Layer.

Основные операции

gRPC интерфейс

service SearchService {
rpc SearchHotels(SearchRequest) returns (SearchResponse);
rpc GetHotelDetails(HotelDetailsRequest) returns (HotelDetailsResponse);
rpc GetAvailableRooms(RoomAvailabilityRequest) returns (RoomAvailabilityResponse);
}

message SearchRequest {
GeoLocation location = 1; // Координаты поиска
DateRange dates = 2; // Даты заезда-выезда
int32 guests = 3; // Количество гостей
PriceRange price_range = 4; // Диапазон цен
repeated string amenities = 5; // Требуемые удобства
string sort_by = 6; // price_asc, rating_desc, distance
int32 page = 7; // Пагинация
int32 page_size = 8;
}

PostgreSQL интеграция

// Использует схему hotels.properties из reference/storage.md
func (s *SearchService) searchByLocation(ctx context.Context, req *SearchRequest) ([]*Hotel, error) {
query := `
SELECT p.id, p.name, p.coordinates, p.star_rating
FROM hotels.properties p
WHERE ST_DWithin(p.coordinates, ST_MakePoint($1, $2), $3)
AND p.id IN (
SELECT DISTINCT r.hotel_id
FROM hotels.rooms r
WHERE r.max_occupancy >= $4
)
ORDER BY ST_Distance(p.coordinates, ST_MakePoint($1, $2))
LIMIT $5 OFFSET $6`

return s.db.Query(ctx, query, req.Location.Lng, req.Location.Lat,
req.RadiusKm*1000, req.Guests, req.PageSize, req.Page*req.PageSize)
}

Redis кеширование

// Использует naming convention из reference/storage.md: search:results:hash123
func (s *SearchService) cacheResults(ctx context.Context, searchHash string, results *SearchResponse) error {
key := fmt.Sprintf("search:results:%s", searchHash)
data, _ := json.Marshal(results)

// TTL 15-30 минут согласно reference/storage.md
return s.cache.Set(ctx, key, data, 30*time.Minute).Err()
}

Pricing Service

Назначение

Расчёт цен, применение наценок, конвертация валют, комиссии агентов. Работает с Redis структурами hotel:prices:uuid:date из Storage Layer.

gRPC интерфейс

service PricingService {
rpc GetHotelPrices(PriceRequest) returns (PriceResponse);
rpc CalculateMarkup(MarkupRequest) returns (MarkupResponse);
rpc ConvertCurrency(CurrencyRequest) returns (CurrencyResponse);
rpc ApplyAgentCommission(CommissionRequest) returns (CommissionResponse);
}

message PriceRequest {
string hotel_id = 1;
DateRange dates = 2;
string room_type = 3;
string currency = 4; // Целевая валюта
string agent_id = 5; // Для расчёта комиссии
}

Redis интеграция

// Использует Redis Hash структуру из reference/storage.md
func (s *PricingService) getBasePrice(ctx context.Context, hotelID string, date time.Time) (*Price, error) {
key := fmt.Sprintf("hotel:prices:%s:%s", hotelID, date.Format("2006-01-02"))

// Получаем все цены по типам номеров
prices, err := s.cache.HGetAll(ctx, key).Result()
if err != nil {
// Cache miss - загружаем из PostgreSQL и кешируем
return s.loadPriceFromDB(ctx, hotelID, date)
}

return s.parseRedisPrice(prices), nil
}

Наценки и комиссии

type MarkupCalculator struct {
AgentCommission float64 // Комиссия агента (%)
PlatformMarkup float64 // Наценка платформы (%)
SeasonalMarkup float64 // Сезонная наценка (%)
CurrencyRates map[string]float64
}

func (m *MarkupCalculator) calculateFinalPrice(basePrice float64, agentID string) *FinalPrice {
// Базовая цена + платформенная наценка
withMarkup := basePrice * (1 + m.PlatformMarkup)

// Сезонная корректировка
withSeasonal := withMarkup * (1 + m.SeasonalMarkup)

// Комиссия агента
agentCommission := withSeasonal * m.AgentCommission

return &FinalPrice{
BasePrice: basePrice,
MarkupPrice: withSeasonal,
Commission: agentCommission,
FinalPrice: withSeasonal + agentCommission,
}
}

Booking Service

Назначение

Создание, подтверждение, отмена бронирований. Реализует "### Поток бронирования" из Архитектура платформы: проверка доступности → резерв у поставщика → запись в PostgreSQL bookings schema → инвалидация availability cache.

gRPC интерфейс

service BookingService {
rpc CreateBooking(CreateBookingRequest) returns (BookingResponse);
rpc ConfirmBooking(ConfirmBookingRequest) returns (BookingResponse);
rpc CancelBooking(CancelBookingRequest) returns (BookingResponse);
rpc GetBookingDetails(BookingDetailsRequest) returns (BookingDetailsResponse);
rpc GetBookingHistory(BookingHistoryRequest) returns (BookingHistoryResponse);
}

message CreateBookingRequest {
string hotel_id = 1;
string room_type = 2;
DateRange dates = 3;
GuestDetails guest = 4;
string agent_id = 5;
PaymentDetails payment = 6;
}

Booking Flow Implementation

// Реализует критически важную транзакцию из overview/index.md
func (s *BookingService) CreateBooking(ctx context.Context, req *CreateBookingRequest) (*BookingResponse, error) {
// 1. Проверка доступности в Redis cache
if !s.checkRoomAvailability(ctx, req.HotelId, req.RoomType, req.Dates) {
return nil, errors.New("room not available")
}

// 2. Резерв у поставщика (Supplier API)
reservation, err := s.supplierClient.ReserveRoom(ctx, &supplier.ReservationRequest{
HotelId: req.HotelId,
RoomType: req.RoomType,
Dates: req.Dates,
})
if err != nil {
return nil, fmt.Errorf("supplier reservation failed: %w", err)
}

// 3. Создание записи в PostgreSQL (bookings schema из reference/storage.md)
booking, err := s.createBookingRecord(ctx, req, reservation.ConfirmationCode)
if err != nil {
// Откат резерва у поставщика
s.supplierClient.CancelReservation(ctx, reservation.ConfirmationCode)
return nil, fmt.Errorf("database booking failed: %w", err)
}

// 4. Инвалидация availability cache согласно reference/storage.md
s.invalidateAvailabilityCache(ctx, req.HotelId, req.Dates)

// 5. Отправка события в NATS для других сервисов
s.publishBookingEvent(ctx, &BookingCreatedEvent{
BookingId: booking.Id,
HotelId: req.HotelId,
Dates: req.Dates,
})

return &BookingResponse{
BookingId: booking.Id,
ConfirmationCode: reservation.ConfirmationCode,
Status: "confirmed",
}, nil
}

PostgreSQL интеграция (bookings schema)

// Использует bookings.reservations из reference/storage.md
func (s *BookingService) createBookingRecord(ctx context.Context, req *CreateBookingRequest, confirmationCode string) (*Booking, error) {
query := `
INSERT INTO bookings.reservations (
id, hotel_id, room_type, checkin_date, checkout_date,
guest_name, guest_email, agent_id, confirmation_code,
status, created_at
) VALUES (
gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, 'confirmed', now()
) RETURNING id, created_at`

var booking Booking
err := s.db.QueryRow(ctx, query,
req.HotelId, req.RoomType, req.Dates.CheckIn, req.Dates.CheckOut,
req.Guest.Name, req.Guest.Email, req.AgentId, confirmationCode,
).Scan(&booking.Id, &booking.CreatedAt)

return &booking, err
}

Tour Builder Service

Назначение

Конструктор программ туров, генерация PDF согласно Архитектура платформы. Агрегирует данные от Search, Pricing и других сервисов.

gRPC интерфейс

service TourBuilderService {
rpc CreateTourProgram(CreateTourRequest) returns (TourResponse);
rpc AddHotelToTour(AddHotelRequest) returns (TourResponse);
rpc GenerateTourPDF(GeneratePDFRequest) returns (PDFResponse);
rpc GetTourDetails(TourDetailsRequest) returns (TourDetailsResponse);
}

message CreateTourRequest {
string agent_id = 1;
string tour_name = 2;
DateRange dates = 3;
repeated Destination destinations = 4;
int32 total_guests = 5;
}

Интеграция с другими сервисами

type TourBuilderService struct {
searchClient searchpb.SearchServiceClient
pricingClient pricingpb.PricingServiceClient
bookingClient bookingpb.BookingServiceClient
pdfGenerator *PDFGenerator
}

func (s *TourBuilderService) CreateTourProgram(ctx context.Context, req *CreateTourRequest) (*TourResponse, error) {
tour := &Tour{
Id: uuid.New().String(),
Name: req.TourName,
AgentId: req.AgentId,
Dates: req.Dates,
Status: "draft",
}

// Поиск отелей для каждой точки маршрута
for _, dest := range req.Destinations {
hotels, err := s.searchClient.SearchHotels(ctx, &searchpb.SearchRequest{
Location: dest.Location,
Dates: dest.Dates,
Guests: req.TotalGuests,
})
if err != nil {
return nil, fmt.Errorf("search failed for %s: %w", dest.Name, err)
}

// Получение цен для найденных отелей
for _, hotel := range hotels.Hotels {
prices, _ := s.pricingClient.GetHotelPrices(ctx, &pricingpb.PriceRequest{
HotelId: hotel.Id,
Dates: dest.Dates,
Currency: "USD",
AgentId: req.AgentId,
})
hotel.Price = prices.TotalPrice
}

tour.Destinations = append(tour.Destinations, &TourDestination{
Location: dest,
Hotels: hotels.Hotels,
})
}

return &TourResponse{Tour: tour}, nil
}

Межсервисное взаимодействие

gRPC Communication

// Service Discovery через Kubernetes DNS
type ServiceRegistry struct {
SearchService string // "search-service.default.svc.cluster.local:80"
PricingService string // "pricing-service.default.svc.cluster.local:80"
BookingService string // "booking-service.default.svc.cluster.local:80"
}

// Circuit Breaker для отказоустойчивости
func (s *Service) callWithCircuitBreaker(ctx context.Context, serviceName string, call func() error) error {
breaker := s.circuitBreakers[serviceName]
return breaker.Execute(func() error {
return call()
})
}

Event-Driven Architecture

// Асинхронные события через NATS
type EventBus struct {
nats *nats.Conn
}

// События инвалидации кеша
type CacheInvalidationEvent struct {
Type string `json:"type"` // "hotel_updated", "price_changed"
EntityID string `json:"entity_id"` // hotel_id, booking_id
Timestamp time.Time `json:"timestamp"`
}

func (e *EventBus) PublishCacheInvalidation(ctx context.Context, event *CacheInvalidationEvent) error {
data, _ := json.Marshal(event)
return e.nats.Publish("cache.invalidation", data)
}

Производительность и масштабирование

Горизонтальное масштабирование

Согласно разделу "## Масштабирование и производительность" в Архитектура платформы: "Business services: добавление подов в K8s"

# Kubernetes Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: search-service
spec:
replicas: 3 # Начальное количество подов
template:
spec:
containers:
- name: search-service
image: vitrip/search-service:v1.0
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "512Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: search-service
spec:
selector:
app: search-service
ports:
- port: 80
targetPort: 8080

Connection Pooling

// PostgreSQL connection pool
pgConfig, _ := pgxpool.ParseConfig("postgresql://user:pass@db/vitrip")
pgConfig.MaxConns = 20 // Максимум соединений
pgConfig.MinConns = 5 // Минимум в пуле
pgConfig.MaxConnLifetime = time.Hour // Время жизни соединения

// Redis connection pool
redisClient := redis.NewClient(&redis.Options{
Addr: "redis:6379",
PoolSize: 20, // Размер пула
MinIdleConns: 5, // Минимум idle соединений
})

Metrics и мониторинг

// Prometheus метрики
var (
requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "service_request_duration_seconds",
Help: "Duration of gRPC requests",
},
[]string{"service", "method", "status"},
)

cacheHitRate = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "cache_requests_total",
Help: "Cache hit/miss statistics",
},
[]string{"service", "cache_type", "result"},
)
)

Связанная документация