All checks were successful
Deploy Smart Search Backend Test / deploy (push) Successful in 1m24s
248 lines
7.6 KiB
Markdown
248 lines
7.6 KiB
Markdown
# gRPC Services
|
||
|
||
## Архитектура
|
||
|
||
gRPC handlers разделены на отдельные структуры для каждого сервиса во избежание коллизий имён методов (например, `GetInfo` присутствует как в `UserService`, так и в `InviteService`).
|
||
|
||
### Структура handlers
|
||
|
||
```
|
||
internal/grpc/
|
||
├── server.go # Инициализация handlers и регистрация
|
||
├── auth_handler.go # AuthService gRPC методы
|
||
├── user_handler.go # UserService gRPC методы
|
||
├── invite_handler.go # InviteService gRPC методы
|
||
├── request_handler.go # RequestService gRPC методы
|
||
├── supplier_handler.go # SupplierService gRPC методы
|
||
└── error_mapper.go # Маппинг ошибок в gRPC статусы
|
||
```
|
||
|
||
## Сервисы
|
||
|
||
### 1. AuthService
|
||
|
||
**Proto:** `api/proto/auth/auth.proto`
|
||
|
||
| Метод | Описание |
|
||
|-------|----------|
|
||
| `Register` | Регистрация нового пользователя по инвайт-коду |
|
||
| `Login` | Аутентификация пользователя (email + password) |
|
||
| `Refresh` | Обновление access token по refresh token |
|
||
| `Validate` | Валидация access token |
|
||
| `Logout` | Выход (invalidate refresh token) |
|
||
|
||
**Пример:**
|
||
|
||
```go
|
||
// Login
|
||
req := &auth.LoginRequest{
|
||
Email: "user@example.com",
|
||
Password: "password",
|
||
Ip: "127.0.0.1",
|
||
UserAgent: "MyApp/1.0",
|
||
}
|
||
resp, err := authClient.Login(ctx, req)
|
||
// resp.AccessToken, resp.RefreshToken
|
||
```
|
||
|
||
### 2. UserService
|
||
|
||
**Proto:** `api/proto/user/user.proto`
|
||
|
||
| Метод | Описание |
|
||
|-------|----------|
|
||
| `GetInfo` | Получить информацию о пользователе |
|
||
| `GetBalance` | Получить баланс |
|
||
| `GetStatistics` | Получить статистику заявок |
|
||
| `GetBalanceStatistics` | Комбинированная статистика баланса и заявок |
|
||
|
||
**Пример:**
|
||
|
||
```go
|
||
req := &user.GetInfoRequest{UserId: 123}
|
||
resp, err := userClient.GetInfo(ctx, req)
|
||
// resp.Email, resp.Name, resp.CompanyName...
|
||
```
|
||
|
||
### 3. InviteService
|
||
|
||
**Proto:** `api/proto/invite/invite.proto`
|
||
|
||
| Метод | Описание |
|
||
|-------|----------|
|
||
| `Generate` | Сгенерировать инвайт-код |
|
||
| `GetInfo` | Получить информацию об инвайт-коде |
|
||
|
||
**Пример:**
|
||
|
||
```go
|
||
req := &invite.GenerateRequest{
|
||
UserId: 123,
|
||
TtlDays: 30,
|
||
MaxUses: 5,
|
||
}
|
||
resp, err := inviteClient.Generate(ctx, req)
|
||
// resp.Code, resp.ExpiresAt
|
||
```
|
||
|
||
### 4. RequestService
|
||
|
||
**Proto:** `api/proto/request/request.proto`
|
||
|
||
| Метод | Описание |
|
||
|-------|----------|
|
||
| `CreateTZ` | Создать заявку и сгенерировать ТЗ (AI) |
|
||
| `ApproveTZ` | Подтвердить ТЗ и найти поставщиков (AI) |
|
||
| `GetMailingList` | Получить список заявок пользователя |
|
||
| `GetMailingListByID` | Получить детали конкретной заявки |
|
||
|
||
**Особенности:**
|
||
|
||
- `CreateTZ`: поддерживает опциональные `file_data` и `file_name` для прикрепления файлов
|
||
- `ApproveTZ`: запускает поиск поставщиков через Perplexity API
|
||
|
||
**Пример:**
|
||
|
||
```go
|
||
// Создание ТЗ
|
||
req := &request.CreateTZRequest{
|
||
UserId: 123,
|
||
RequestTxt: "Нужны поставщики автозапчастей",
|
||
FileData: fileBytes, // опционально
|
||
FileName: "specs.pdf", // опционально
|
||
}
|
||
resp, err := requestClient.CreateTZ(ctx, req)
|
||
// resp.RequestId, resp.TzText (сгенерировано AI)
|
||
|
||
// Подтверждение ТЗ
|
||
approveReq := &request.ApproveTZRequest{
|
||
RequestId: resp.RequestId,
|
||
FinalTz: "Отредактированное ТЗ",
|
||
UserId: 123,
|
||
}
|
||
approveResp, err := requestClient.ApproveTZ(ctx, approveReq)
|
||
// approveResp.MailingStatus (поставщики найдены)
|
||
```
|
||
|
||
### 5. SupplierService
|
||
|
||
**Proto:** `api/proto/supplier/supplier.proto`
|
||
|
||
| Метод | Описание |
|
||
|-------|----------|
|
||
| `ExportExcel` | Экспортировать список поставщиков в Excel |
|
||
|
||
**Пример:**
|
||
|
||
```go
|
||
req := &supplier.ExportExcelRequest{
|
||
RequestId: "uuid-string",
|
||
UserId: 123,
|
||
}
|
||
resp, err := supplierClient.ExportExcel(ctx, req)
|
||
// resp.FileData, resp.FileName, resp.MimeType
|
||
```
|
||
|
||
## Обработка ошибок
|
||
|
||
Все ошибки из service layer автоматически мапятся в gRPC статусы через `errors.ToGRPCError()`:
|
||
|
||
| Код ошибки | gRPC Status |
|
||
|------------|-------------|
|
||
| `AUTH_INVALID_CREDENTIALS` | `Unauthenticated` |
|
||
| `AUTH_INVALID_TOKEN` | `Unauthenticated` |
|
||
| `USER_NOT_FOUND` | `NotFound` |
|
||
| `REQUEST_NOT_FOUND` | `NotFound` |
|
||
| `INVITE_LIMIT_REACHED` | `ResourceExhausted` |
|
||
| `INVITE_INVALID_OR_EXPIRED` | `FailedPrecondition` |
|
||
| `EMAIL_ALREADY_EXISTS` | `AlreadyExists` |
|
||
| `INSUFFICIENT_BALANCE` | `FailedPrecondition` |
|
||
| Внутренние ошибки | `Internal` (без деталей) |
|
||
|
||
## Регистрация сервисов
|
||
|
||
В `cmd/server/main.go`:
|
||
|
||
```go
|
||
authHandler, userHandler, inviteHandler, requestHandler, supplierHandler :=
|
||
grpcServer.NewHandlers(pool, openAIKey, perplexityKey)
|
||
|
||
grpcEntry.AddRegFuncGrpc(func(s *grpc.Server) {
|
||
grpcServer.RegisterServices(s,
|
||
authHandler, userHandler, inviteHandler,
|
||
requestHandler, supplierHandler)
|
||
})
|
||
```
|
||
|
||
## Context Flow
|
||
|
||
```
|
||
gRPC Request → Handler(ctx) → Service(ctx) → Repository(ctx) → pgx(ctx)
|
||
```
|
||
|
||
Все gRPC handlers принимают `context.Context` из gRPC request и прокидывают его через все слои.
|
||
|
||
## Тестирование
|
||
|
||
Для тестирования gRPC методов можно использовать:
|
||
|
||
### 1. grpcurl (CLI)
|
||
|
||
```bash
|
||
# Получить список сервисов
|
||
grpcurl -plaintext localhost:9091 list
|
||
|
||
# Вызвать метод
|
||
grpcurl -plaintext \
|
||
-d '{"email":"user@example.com","password":"password","ip":"127.0.0.1","user_agent":"test"}' \
|
||
localhost:9091 auth.AuthService/Login
|
||
```
|
||
|
||
### 2. Unit тесты с моками
|
||
|
||
```go
|
||
// Создать mock service
|
||
mockRequestService := &mockRequestService{
|
||
createTZFunc: func(ctx context.Context, userID int, txt string) (uuid.UUID, string, error) {
|
||
return uuid.New(), "Mock TZ", nil
|
||
},
|
||
}
|
||
|
||
// Создать handler с mock
|
||
handler := &grpc.RequestHandler{
|
||
requestService: mockRequestService,
|
||
}
|
||
|
||
// Тестировать
|
||
resp, err := handler.CreateTZ(ctx, &request.CreateTZRequest{...})
|
||
```
|
||
|
||
## Метрики и трейсинг
|
||
|
||
Все gRPC методы автоматически отслеживаются через `rk-boot`:
|
||
|
||
- **Метрики**: запросы, ошибки, latency
|
||
- **Трейсинг**: distributed tracing через OpenTelemetry
|
||
- **Логирование**: все запросы логируются с request ID
|
||
|
||
Доступно на:
|
||
- Метрики: `http://localhost:9091/metrics`
|
||
- Health: `http://localhost:9091/health`
|
||
|
||
## Reflection
|
||
|
||
gRPC Reflection включен в `config/boot.yaml`, что позволяет использовать `grpcurl` и другие инструменты без .proto файлов:
|
||
|
||
```yaml
|
||
grpc:
|
||
- name: smart-search-service
|
||
enableReflection: true
|
||
```
|
||
|
||
## Статистика
|
||
|
||
- **Всего gRPC методов**: 17
|
||
- **Всего handlers**: 5
|
||
- **Строк кода handlers**: ~390
|
||
- **Proto файлов**: 5
|