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_services_data_flow.jpg — как сервисы используют PostgreSQL схемы и Redis кеши из Storage Layer.

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

Архитектура сервисов
Принципы проектирования
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"},
)
)
Связанная документация
- Общая архитектура: раздел "### 4. Business services (Бизнес-сервисы)" в Архитектура платформы
- Storage интеграция: PostgreSQL схемы и Redis структуры из Storage Layer
- Потоки данных: разделы "### Поток пользовательских запросов" и "### Поток бронирования" в Архитектура платформы
- Технологический стек: обоснование выбора Go из таблицы в Архитектура платформы