diff --git a/README.md b/README.md index 6215350..21ad0e0 100644 --- a/README.md +++ b/README.md @@ -111,3 +111,45 @@ RPS = 10 000 000 * 0.4 / 86 400 = ~47 Расчет одновременных соединений Connections = 10 000 000 * 0.1 = 1 000 000 + +Посты. + +Трафик. Публикация постов (мета) - 0,14 МБ/с +Трафик. Публикация постов (медиа) - 470 МБ/с +Трафик. Просмотр ленты (мета) - 18 МБ/с +Трафик. Просмотр ленты (медиа) - 2 222 МБ/с +Трафик. Поиск и просмотр постов/мест (мета) - 9 МБ/с +Трафик. Поиск и просмотр постов/мест (медиа) - 3 819 МБ/с +Трафик. Подписки - 0,05 МБ/с + +Capacity = 470,14 МБ/с * 86 400 * 365 = 15 000 ТБ +SSD: +Disk_for_capacity = 15 000 ТБ/ 90 ТБ = 167 +Disks_for_throughput = 6 540 / 500 МБ/с = 13 +Disks_for_iops = 3 172 / 1 000 = 3 +Disks max (167, 13, 3) = 167 + +Реакции. + +Трафик. Создание комментария - 0,04 МБ/с +Трафик. Чтение комментария - 0,36 МБ/с + +Capacity = 0,04 МБ/с * 86 400 * 365 = 1,3 ТБ +SSD: +Disk_for_capacity = 1,3 ТБ/ 1 ТБ = 1,3 +Disks_for_throughput = 0,4 / 500 МБ/с = 0,0008 +Disks_for_iops = 556 / 1 000 = 0,556 +Disks max (1,3, 0,0008, 0,556) = 1 + +Комментарии. + +Трафик. Создание лайка - 0,16 МБ/с +Трафик. Чтение лайка - 1,44 МБ/с + +Capacity = 0,16 МБ/с * 86 400 * 365 = 5 ТБ +SSD: +Disk_for_capacity = 5 ТБ/ 2 ТБ = 2,5 +Disks_for_throughput = 1,6 / 500 МБ/с = 0,0032 +Disks_for_iops = 139 / 1 000 = 0,139 +Disks max (2,5, 0,0032, 0,136) = 3 + diff --git a/api/rest_api.yml b/api/rest_api.yml new file mode 100644 index 0000000..11b320c --- /dev/null +++ b/api/rest_api.yml @@ -0,0 +1,721 @@ +openapi: 3.0.0 + +tags: + - name: Posts + - name: Subscriptions + - name: Engagement + - name: Feed + - name: Comments + - name: Reactions + - name: Media + +info: + title: Travel Platform API + description: API для работы с приложением для путешественников + version: 1.0.0 + +paths: + /api/v1/posts: + post: + summary: Создать новый пост о путешествии + description: Если пользователь не лайкал пост - лайк добавляется. Если пользователь лайкал пост - лакй удаляется + tags: + - Posts + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + description: + type: string + maxLength: 1000 + example: "Текст поста" + photoUrls: + type: array + items: + type: string + items: + type: string + format: uri + minItems: 1 + maxItems: 10 + example: ["https://cdn.example.com/photo1.jpg", "https://cdn.example.com/photo2.jpg"] + location: + type: object + properties: + id: + type: string + example: "123" + name: + type: string + example: "Какое-то описание" + latitude: + type: number + format: float + example: 44.4444 + longitude: + type: number + format: float + example: 33.3333 + required: + - name + required: + - description + - photoUrls + - location + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Post' + 400: + description: Некорректные данные + 401: + description: Пользователь не авторизован + 500: + description: Внутренняя ошибка сервера + + /api/v1/users/{id}/subscribes: + post: + summary: Подписаться на путешественника + description: Создает подписку текущего пользователя на указанного путешественника + tags: + - Subscriptions + parameters: + - name: id + in: path + required: true + schema: + type: string + description: Идентификатор путешественника + responses: + 200: + description: Ok + content: + application/json: + schema: + type: object + properties: + subscribed: + type: boolean + example: true + subscribersCount: + type: integer + example: 10 + required: + - subscribed + - subscribersCount + 401: + description: Пользователь не авторизован + 404: + description: Пользователь не найден + 500: + description: Внутренняя ошибка сервера + + delete: + summary: Отписаться от путешественника + description: Удаляет подписку текущего пользователя на указанного путешественника + tags: + - Subscriptions + parameters: + - name: id + in: path + required: true + schema: + type: string + description: Идентификатор путешественника + responses: + 200: + description: Ок + content: + application/json: + schema: + type: object + properties: + subscribed: + type: boolean + example: false + subscribersCount: + type: integer + example: 9 + required: + - subscribed + - subscribersCount + 400: + description: Некорректные данные + 401: + description: Пользователь не авторизован + 404: + description: Путешественник с указанным ID не найден + 500: + description: Внутрення ошибка сервера + + /api/v1/posts/{id}/reaction: + post: + summary: Поставить лайк на пост + description: Создает лайк текущего пользователя у указанного поста + tags: + - Engagement + parameters: + - name: id + in: path + required: true + schema: + type: string + description: Идентификатор поста + responses: + 200: + description: Ok + content: + application/json: + schema: + type: object + properties: + reaction: + type: boolean + example: true + reactionsCount: + type: integer + example: 1 + required: + - reaction + - reactionsCount + examples: + response: + value: + reaction: true + reactionsCount: 1 + 401: + description: Пользователь не авторизован + 404: + description: Пост не найден + 500: + description: Внутренняя ошибка сервера + + delete: + summary: Убрать реакцию с поста + description: Удаляет лайк текущего пользователя с указанного поста + tags: + - Engagement + parameters: + - name: id + in: path + required: true + schema: + type: string + description: Идентификатор поста + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + reaction: + type: boolean + example: false + reactionsCount: + type: integer + example: 0 + required: + - reaction + - reactionsCount + examples: + response: + value: + reaction: false + reactionsCount: 0 + 400: + description: Невозможно удалить реакцию + 401: + description: Пользователь не авторизован + 404: + description: Пост не найден + 500: + description: Внутренняя ошибка сервера + + /api/v1/posts/{id}/comments: + post: + summary: Добавить комменарий к посту + description: Добавляет комментарий к посту пользователя + tags: + - Engagement + parameters: + - name: id + in: path + required: true + schema: + type: string + description: Идентификатор поста + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + content: + type: string + minLength: 1 + maxLength: 1000 + example: "Какой-то комментарий" + required: + - content + responses: + 201: + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + 400: + description: Некорректные данные + 401: + description: Пользователь не авторизован + 404: + description: Пост не найден + 500: + description: Внутренняя ошибка сервера + + /api/v1/posts: + get: + summary: Получить список постов с возможностью фильтрации + description: + Возвращает список постов + - Если указан `destinationId`, возвращаются посты, привязанные к этому месту + - Если `destinationId` не указан — возвращается глобальная лента (все посты) + - Если указан `feed=popular-destinations` — возвращает посты из популярных мест + Поддерживает пагинацию. + tags: + - Posts + parameters: + - name: destinationId + in: query + required: false + schema: + type: string + description: Идентификатор места. Если не указан — возвращаются все посты + - name: feed + in: query + required: false + schema: + type: string + enum: + - popular-destinations + description: Специальные типы лент. Например, "popular-destinations" — посты из популярных мест. + - name: limit + in: query + description: Количество постов на странице + schema: + type: integer + default: 20 + minimum: 1 + maximum: 50 + - name: offset + in: query + schema: + type: integer + default: 0 + minimum: 0 + description: Смещение для пагинации + responses: + 200: + description: Список постов + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + $ref: '#/components/schemas/Post' + total: + type: integer + example: 150 + limit: + type: integer + example: 20 + offset: + type: integer + example: 0 + required: + - items + - total + - limit + - offset + 400: + description: Некорректные параметры запроса + 404: + description: Указанное место не найдено + 500: + description: Внутренняя ошибка сервера + + /api/v1/feed/subscriptions: + get: + summary: Получить ленту по подписчикам + description: Возвращает ленту путешесвенников, на которых подписан текущий пользователь, в обратном хронологическом порядке + tags: + - Feed + parameters: + - name: limit + in: query + description: Количество записей на странице (макс. 30) + schema: + type : integer + default: 10 + minimum: 1 + maximum: 30 + - name: offset + in: query + description: Смещение для пагинации + schema: + type : integer + default: 0 + minimum: 0 + responses: + 200: + description: Список постов + 401: + description: Пользователь не авторизован + 500: + description: Внутренняя ошибка сервера + + /api/v1/posts/{postId}/comments: + get: + summary: Получить комментарии к посту + description: Возвращает комментарии, созданные к посту + tags: + - Comments + parameters: + - name: postId + in: path + required: true + schema: + type: string + description: Идентификатор поста + - name: limit + in: query + schema: + type: integer + default: 20 + minimum: 1 + maximum: 100 + description: Количество комментариев на странице + - name: offset + in: query + schema: + type: integer + default: 0 + minimum: 0 + description: Смещение для пагинации + responses: + 200: + description: Список комментариев + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + $ref: '#/components/schemas/Comment' + total: + type: integer + example: 42 + limit: + type: integer + example: 20 + offset: + type: integer + example: 0 + required: + - items + - total + - limit + - offset + 404: + description: Пост не найден + 500: + description: Внутренняя ошибка сервера + + /api/v1/posts/{postId}/reactions: + get: + summary: Получить реакции (лайки) на пост + description: Возвращает лайки к посту + tags: + - Reactions + parameters: + - name: postId + in: path + required: true + schema: + type: string + description: Идентификатор поста + - name: limit + in: query + schema: + type: integer + default: 50 + minimum: 1 + maximum: 200 + description: Максимальное количество реакций + - name: offset + in: query + schema: + type: integer + default: 0 + minimum: 0 + description: Смещение для пагинации + responses: + 200: + description: Список реакций + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + $ref: '#/components/schemas/Reaction' + total: + type: integer + example: 128 + limit: + type: integer + example: 50 + offset: + type: integer + example: 0 + required: + - items + - total + - limit + - offset + 404: + description: Пост не найден + 500: + description: Внутренняя ошибка сервера + + /api/v1/posts/{postId}/media: + get: + summary: Получить медиафайлы поста + description: Возвращает медиафайлы для поста + tags: + - Media + parameters: + - name: postId + in: path + required: true + schema: + type: string + description: Идентификатор поста + responses: + 200: + description: Список медиафайлов + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Media' + 404: + description: Пост не найден или у поста нет медиа + 500: + description: Внутренняя ошибка сервера + +components: + schemas: + Comment: + type: object + properties: + id: + type: string + example: "123" + authorId: + type: string + example: "456" + content: + type: string + example: "Какой-то комментарий" + createdAt: + type: string + format: date-time + example: "2024-06-10T15:45:00Z" + postId: + type: string + example: "789" + required: + - id + - authorId + - authorName + - content + - createdAt + - postId + + Post: + type: object + properties: + id: + type: string + example: "123" + authorId: + type: string + example: "456" + authorName: + type: string + example: "Иванов Иван" + description: + type: string + example: "Какое-то описание" + photoUrls: + type: array + items: + type: string + format: uri + example: + - "https://cdn.example.com/photo1.jpg" + location: + type: object + properties: + id: + type: string + example: "123" + name: + type: string + example: "Наименование локации" + latitude: + type: number + format: float + example: 44.4444 + longitude: + type: number + format: float + example: 33.3333 + publishedAt: + type: string + format: date-time + example: "2024-06-12T11:00:00Z" + required: + - id + - authorId + - authorName + - description + - photoUrls + - location + - publishedAt + + Comment: + type: object + properties: + id: + type: string + author: + $ref: '#/components/schemas/User' + content: + type: string + createdAt: + type: string + format: date-time + required: + - id + - author + - content + - createdAt + + Reaction: + type: object + properties: + userId: + type: string + type: + type: string + example: "like" + createdAt: + type: string + format: date-time + required: + - userId + - type + + Media: + type: object + properties: + id: + type: string + url: + type: string + format: uri + type: + type: string + enum: + - image + - video + width: + type: integer + height: + type: integer + User: + type: object + description: Информация о пользователе (путешественнике) + properties: + id: + type: string + format: uuid + example: "usr_5f8e3a1b2c9d" + description: Уникальный идентификатор пользователя + username: + type: string + example: "alex_traveler" + description: Уникальное имя пользователя + displayName: + type: string + example: "Алексей Петров" + description: Отображаемое имя + avatarUrl: + type: string + format: uri + nullable: true + example: "https://example.com/avatars/alex.jpg" + description: Ссылка на аватар + isVerified: + type: boolean + example: true + description: Подтверждён ли аккаунт (галочка верификации) + bio: + type: string + nullable: true + example: "Люблю горы, море и старые города" + description: Краткая биография + followersCount: + type: integer + minimum: 0 + example: 142 + description: Количество подписчиков + followingCount: + type: integer + minimum: 0 + example: 87 + description: Количество подписок + postsCount: + type: integer + minimum: 0 + example: 23 + description: Количество опубликованных постов + createdAt: + type: string + format: date-time + example: "2023-05-12T10:30:00Z" + description: Дата регистрации + required: + - id + - username + - displayName + diff --git a/database/database.io_Content_Service b/database/database.io_Content_Service new file mode 100644 index 0000000..5f5cadb --- /dev/null +++ b/database/database.io_Content_Service @@ -0,0 +1,29 @@ +// Content Service - посты, медиа, места посещения + +Table destinations { + id uuid [primary key] + name varchar [not null] + country varchar [not null] + city varchar + lat double precision + lng double precision + popularity_score integer [default: 0] + created_at timestamp [default: `now()`] +} + +Table posts { + id uuid [primary key] + user_id uuid [not null] // ID из User Service + destination_id uuid [not null, ref: > destinations.id] + content text + created_at timestamp [default: `now()`] +} + +Table media { + id uuid [primary key] + post_id uuid [not null, ref: > posts.id] + url varchar [not null] // URL в S3 + media_type varchar [note: "'image'"] + order_index integer [default: 0] + created_at timestamp [default: `now()`] +} \ No newline at end of file diff --git a/database/database.io_Discovery_Service b/database/database.io_Discovery_Service new file mode 100644 index 0000000..3533cf1 --- /dev/null +++ b/database/database.io_Discovery_Service @@ -0,0 +1,8 @@ +// Discovery Service - популярные места + +Table popular_destinations { + destination_id uuid [primary key] // ID из Content Service + post_count integer [default: 0] + reaction_count integer [default: 0] + last_updated timestamp [default: `now()`] +} \ No newline at end of file diff --git a/database/database.io_Engagement_Service b/database/database.io_Engagement_Service new file mode 100644 index 0000000..0d4c3bb --- /dev/null +++ b/database/database.io_Engagement_Service @@ -0,0 +1,24 @@ +// Engagement Service - лайки и комментарии + +Table reactions { + user_id uuid [not null] // ID из User Service + post_id uuid [not null] // ID из Content Service + created_at timestamp [default: `now()`] + + indexes { + (user_id, post_id) [unique] + (post_id) + } +} + +Table comments { + id uuid [primary key] + post_id uuid [not null] // ID из Content Service + user_id uuid [not null] // ID из User Service + content text [not null] + created_at timestamp [default: `now()`] + + indexes { + (post_id, created_at) + } +} \ No newline at end of file diff --git a/database/database.io_User_Service b/database/database.io_User_Service new file mode 100644 index 0000000..f02bb07 --- /dev/null +++ b/database/database.io_User_Service @@ -0,0 +1,22 @@ +// User Service - управление пользователями и подписками + +Table users { + id uuid [primary key] + username varchar [unique, not null] + display_name varchar [not null] + avatar_url varchar + bio text + created_at timestamp [default: `now()`] +} + +Table subscriptions { + follower_id uuid [not null] + followee_id uuid [not null] + created_at timestamp [default: `now()`] + + indexes { + (follower_id, followee_id) [unique] + (follower_id) + (followee_id) + } +} \ No newline at end of file diff --git a/database/redis_Feed_Service b/database/redis_Feed_Service new file mode 100644 index 0000000..b09f6ac --- /dev/null +++ b/database/redis_Feed_Service @@ -0,0 +1,10 @@ +// Feed Service (Redis) - лента +// Описание ключей + +Table feed_structure { + key_pattern varchar [note: "'user_feed:{user_id}'"] + data_type varchar [note: "Sorted Set"] + score double [note: "timestamp (в миллисекундах)"] + value varchar [note: "post_id (UUID из Content Service)"] + ttl varchar [note: "Бессрочно или по политике"] +} \ No newline at end of file