Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions LukinykhDO/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Генератор QR-кодов на Docker Compose

Микросервисное веб-приложение для генерации QR-кодов.

## Архитектура

Сервисы проекта:

- `nginx` — единая внешняя точка входа. Проксирует frontend и API.
- `frontend` — интерфейс на Vue 3, запускается через Bun + Vite.
- `gateway` — API на Go + Gin. Принимает запросы, создаёт задачи и кладёт их в Redis через Asynq.
- `qrgen` — фоновый обработчик задач на Go. Забирает задания из очереди и генерирует PNG с QR-кодом.
- `redis` — брокер очередей и хранилище состояния задач/результатов.

## Как это работает

1. Пользователь открывает веб-интерфейс.
2. Frontend отправляет `POST /api/tasks` с текстом или ссылкой.
3. `gateway` создаёт запись задачи в Redis и ставит её в очередь Asynq.
4. `qrgen` получает задачу, генерирует QR-код и сохраняет PNG в Redis.
5. Frontend опрашивает `GET /api/tasks/:id` и показывает изображение после завершения.

## Запуск

```bash
docker compose up
```

После запуска приложение доступно по адресу:

- `http://localhost:8080`

## API

### Создать задачу

```http
POST /api/tasks
Content-Type: application/json

{
"content": "https://example.com",
"size": 256
}
```

### Получить статус задачи

```http
GET /api/tasks/{id}
```

### Скачать PNG

```http
GET /api/tasks/{id}/image
```
44 changes: 44 additions & 0 deletions LukinykhDO/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
services:
nginx:
image: nginx:1.27-alpine
depends_on:
- frontend
- gateway
ports:
- "8080:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro

frontend:
build:
context: ./frontend
depends_on:
- gateway

gateway:
build:
context: .
dockerfile: gateway/Dockerfile
environment:
PORT: "8080"
REDIS_ADDR: redis:6379
depends_on:
- redis

qrgen:
build:
context: .
dockerfile: qrgen/Dockerfile
environment:
REDIS_ADDR: redis:6379
depends_on:
- redis

redis:
image: redis:7.4-alpine
command: redis-server --save 60 1 --loglevel warning
volumes:
- redis-data:/data

volumes:
redis-data:
8 changes: 8 additions & 0 deletions LukinykhDO/frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM oven/bun:1.1.38
WORKDIR /app
COPY package.json ./
RUN bun install
COPY . .
RUN bun run build
EXPOSE 4173
CMD ["bunx", "vite", "preview", "--host", "0.0.0.0", "--port", "4173"]
12 changes: 12 additions & 0 deletions LukinykhDO/frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>QR Generator</title>
<script type="module" src="/src/main.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
18 changes: 18 additions & 0 deletions LukinykhDO/frontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "qrgen-frontend",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0 --port 4173",
"build": "vite build",
"preview": "vite preview --host 0.0.0.0 --port 4173"
},
"dependencies": {
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.2.6"
}
}
151 changes: 151 additions & 0 deletions LukinykhDO/frontend/src/App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<script setup>
import { computed, onBeforeUnmount, ref } from 'vue'

const form = ref({ content: '', size: 256 })
const task = ref(null)
const loading = ref(false)
const error = ref('')
const imageVersion = ref(Date.now())
let pollHandle = null

const statusText = computed(() => task.value?.status ?? 'idle')
const imageUrl = computed(() => {
if (!task.value || task.value.status !== 'completed') {
return ''
}
return `${task.value.image_url}?t=${imageVersion.value}`
})

async function createTask() {
stopPolling()
error.value = ''
task.value = null
loading.value = true

try {
const response = await fetch('/api/tasks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: form.value.content,
size: Number(form.value.size),
}),
})

const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'Не удалось создать задачу')
}

task.value = data.task
startPolling(task.value.id)
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}

async function refreshTask(taskId) {
const response = await fetch(`/api/tasks/${taskId}`)
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'Не удалось получить статус')
}

task.value = data.task
if (task.value.status === 'completed' || task.value.status === 'failed') {
imageVersion.value = Date.now()
stopPolling()
}
}

function startPolling(taskId) {
refreshTask(taskId).catch((err) => {
error.value = err.message
stopPolling()
})

pollHandle = window.setInterval(async () => {
try {
await refreshTask(taskId)
} catch (err) {
error.value = err.message
stopPolling()
}
}, 1500)
}

function stopPolling() {
if (pollHandle) {
window.clearInterval(pollHandle)
pollHandle = null
}
}

onBeforeUnmount(() => {
stopPolling()
})
</script>

<template>
<main class="page-shell">
<section class="hero-card">
<p class="eyebrow">Docker Compose Microservices</p>
<h1>Генератор QR-кодов</h1>
<p class="lead">
Vue-интерфейс отправляет задачу в Go API, Redis + Asynq ставят её в очередь,
а сервис <code>qrgen</code> собирает PNG в фоне.
</p>

<form class="generator-form" @submit.prevent="createTask">
<label>
Текст или ссылка
<textarea
v-model="form.content"
rows="4"
maxlength="600"
placeholder="Например: https://example.com/invite/42"
required
/>
</label>

<label>
Размер QR-кода
<input v-model="form.size" type="range" min="128" max="1024" step="32" />
<span class="range-value">{{ form.size }} px</span>
</label>

<button :disabled="loading" type="submit">
{{ loading ? 'Создание задачи...' : 'Сгенерировать QR-код' }}
</button>
</form>
</section>

<section class="result-card">
<div class="result-header">
<div>
<p class="eyebrow">Статус задачи</p>
<h2>{{ statusText }}</h2>
</div>
<span v-if="task" class="task-id">{{ task.id }}</span>
</div>

<p v-if="error" class="error-box">{{ error }}</p>
<p v-else-if="!task" class="muted">Создайте задачу, и здесь появится результат.</p>
<p v-else-if="task.status === 'queued' || task.status === 'processing'" class="muted">
Задача поставлена в очередь и обрабатывается сервисом <code>qrgen</code>.
</p>
<p v-else-if="task.status === 'failed'" class="error-box">
Обработка завершилась ошибкой: {{ task.error || 'неизвестная ошибка' }}
</p>

<div v-if="task?.status === 'completed'" class="preview-block">
<img :src="imageUrl" alt="Generated QR code" class="qr-image" />
<a class="download-link" :href="imageUrl" download="qr-code.png">Скачать PNG</a>
</div>
</section>
</main>
</template>
5 changes: 5 additions & 0 deletions LukinykhDO/frontend/src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createApp } from 'vue'
import App from './App.vue'
import './styles.css'

createApp(App).mount('#app')
Loading