minio
This commit is contained in:
+101
-51
@@ -12,25 +12,26 @@
|
|||||||
|
|
||||||
В объекте `PostCalendarItem` добавлены два новых поля для работы с изображениями:
|
В объекте `PostCalendarItem` добавлены два новых поля для работы с изображениями:
|
||||||
|
|
||||||
| Поле | Тип | Описание | Обязательное |
|
| Поле | Тип | Описание | Обязательное |
|
||||||
| --------------- | ------ | ------------------------------------------------------------------- | ------------ |
|
| --------------- | ------ | ------------------------------------------------------------ | ------------ |
|
||||||
| `imageUrl` | string | Путь к изображению в MinIO (используется для получения изображения) | Нет |
|
| `imageUrl` | string | Имя файла изображения (используется для получения через API) | Нет |
|
||||||
| `imageFilename` | string | Имя файла изображения в MinIO | Нет |
|
| `imageFilename` | string | Имя файла изображения | Нет |
|
||||||
|
|
||||||
**Важно:**
|
**Важно:**
|
||||||
|
|
||||||
- Поля могут быть `null`, если генерация изображения не удалась
|
- Поля могут быть `null`, если генерация изображения не удалась
|
||||||
- В этом случае пост публикуется без изображения
|
- В этом случае пост публикуется без изображения
|
||||||
- Оба поля содержат одинаковое значение (имя файла в MinIO)
|
- Оба поля содержат одинаковое значение (имя файла)
|
||||||
|
- Для получения изображения используйте API эндпоинт `/api/marketing/analysis/images/{imageFilename}`
|
||||||
|
|
||||||
### 2. PostingTask (Задачи публикации)
|
### 2. PostingTask (Задачи публикации)
|
||||||
|
|
||||||
В объекте `PostingTask` также добавлены поля для изображений:
|
В объекте `PostingTask` также добавлены поля для изображений:
|
||||||
|
|
||||||
| Поле | Тип | Описание | Обязательное |
|
| Поле | Тип | Описание | Обязательное |
|
||||||
| --------------- | ------ | ----------------------------- | ------------ |
|
| --------------- | ------ | ------------------------------------------------------------ | ------------ |
|
||||||
| `imageUrl` | string | Путь к изображению в MinIO | Нет |
|
| `imageUrl` | string | Имя файла изображения (используется для получения через API) | Нет |
|
||||||
| `imageFilename` | string | Имя файла изображения в MinIO | Нет |
|
| `imageFilename` | string | Имя файла изображения | Нет |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -76,29 +77,71 @@
|
|||||||
|
|
||||||
## Получение изображений
|
## Получение изображений
|
||||||
|
|
||||||
### Способ 1: Через MinIO API (рекомендуется)
|
### Через бэкенд API
|
||||||
|
|
||||||
Если у вас есть доступ к MinIO API, используйте `imageUrl` или `imageFilename` для получения изображения:
|
Все изображения должны получаться через бэкенд API. Прямой доступ к MinIO с фронтенда не предусмотрен.
|
||||||
|
|
||||||
```
|
#### Эндпоинт для получения изображения
|
||||||
GET {minio-endpoint}/{bucket-name}/{imageFilename}
|
|
||||||
|
**GET** `/api/marketing/analysis/images/{imageFilename}`
|
||||||
|
|
||||||
|
Возвращает изображение поста в формате PNG.
|
||||||
|
|
||||||
|
#### Параметры пути
|
||||||
|
|
||||||
|
| Параметр | Тип | Описание |
|
||||||
|
| --------------- | ------ | --------------------- |
|
||||||
|
| `imageFilename` | string | Имя файла изображения |
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
| Заголовок | Тип | Обязательный | Описание |
|
||||||
|
| --------------- | ------ | ------------ | ------------------------------------ |
|
||||||
|
| `Authorization` | string | Да | JWT токен в формате `Bearer {token}` |
|
||||||
|
|
||||||
|
#### Пример запроса
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const imageUrl = `/api/marketing/analysis/images/${post.imageFilename}`;
|
||||||
|
|
||||||
|
fetch(imageUrl, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.blob();
|
||||||
|
}
|
||||||
|
throw new Error('Failed to load image');
|
||||||
|
})
|
||||||
|
.then((blob) => {
|
||||||
|
const imageObjectUrl = URL.createObjectURL(blob);
|
||||||
|
// Используйте imageObjectUrl для отображения
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
Где:
|
#### Пример ответа
|
||||||
|
|
||||||
- `minio-endpoint` - адрес вашего MinIO сервера (например, `http://92.38.48.166:9000`)
|
- **Успешный ответ (200 OK):**
|
||||||
- `bucket-name` - имя bucket (обычно `konturai`)
|
|
||||||
- `imageFilename` - значение из поля `imageFilename` или `imageUrl`
|
|
||||||
|
|
||||||
### Способ 2: Через бэкенд API (если реализован)
|
- Content-Type: `image/png`
|
||||||
|
- Body: бинарные данные изображения PNG
|
||||||
|
|
||||||
Если бэкенд предоставляет эндпоинт для получения изображений, используйте его:
|
- **Ошибка 401 Unauthorized:**
|
||||||
|
|
||||||
```
|
- Токен отсутствует или невалиден
|
||||||
GET /api/marketing/images/{imageFilename}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Примечание:** На данный момент такой эндпоинт не реализован. При необходимости его можно добавить.
|
- **Ошибка 404 Not Found:**
|
||||||
|
|
||||||
|
- Изображение не найдено на сервере
|
||||||
|
|
||||||
|
- **Ошибка 500 Internal Server Error:**
|
||||||
|
- Внутренняя ошибка сервера при загрузке изображения
|
||||||
|
|
||||||
|
#### Кэширование
|
||||||
|
|
||||||
|
Сервер возвращает заголовок `Cache-Control: public, max-age=3600`, что позволяет браузеру кэшировать изображения на 1 час.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -110,22 +153,26 @@ GET /api/marketing/images/{imageFilename}
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Пример на JavaScript/TypeScript
|
// Пример на JavaScript/TypeScript
|
||||||
const PostCard = ({ post }) => {
|
const PostCard = ({ post, apiBaseUrl, authToken }) => {
|
||||||
const hasImage = post.imageUrl && post.imageFilename;
|
const hasImage = post.imageUrl && post.imageFilename;
|
||||||
|
const imageUrl = hasImage
|
||||||
|
? `${apiBaseUrl}/api/marketing/analysis/images/${post.imageFilename}`
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='post-card'>
|
<div className='post-card'>
|
||||||
<h3>{post.theme}</h3>
|
<h3>{post.theme}</h3>
|
||||||
<p>{post.postText}</p>
|
<p>{post.postText}</p>
|
||||||
|
|
||||||
{hasImage ? (
|
{imageUrl ? (
|
||||||
<img
|
<img
|
||||||
src={`${MINIO_ENDPOINT}/${BUCKET_NAME}/${post.imageFilename}`}
|
src={imageUrl}
|
||||||
alt={post.theme}
|
alt={post.theme}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
// Fallback если изображение не загрузилось
|
// Fallback если изображение не загрузилось
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
|
// Если требуется авторизация, используйте fetch с заголовками
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className='no-image-placeholder'>Изображение не сгенерировано</div>
|
<div className='no-image-placeholder'>Изображение не сгенерировано</div>
|
||||||
@@ -146,12 +193,17 @@ const PostCard = ({ post }) => {
|
|||||||
Всегда предусматривайте fallback для случаев, когда:
|
Всегда предусматривайте fallback для случаев, когда:
|
||||||
|
|
||||||
- Изображение не сгенерировано (`imageUrl` = `null`)
|
- Изображение не сгенерировано (`imageUrl` = `null`)
|
||||||
- Изображение не найдено в MinIO
|
- Изображение не найдено на сервере
|
||||||
- Ошибка при загрузке изображения
|
- Ошибка при загрузке изображения
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const [imageError, setImageError] = useState(false);
|
const [imageError, setImageError] = useState(false);
|
||||||
|
|
||||||
|
const getImageUrl = (imageFilename) => {
|
||||||
|
if (!imageFilename) return null;
|
||||||
|
return `${API_BASE_URL}/api/marketing/analysis/images/${imageFilename}`;
|
||||||
|
};
|
||||||
|
|
||||||
const handleImageError = () => {
|
const handleImageError = () => {
|
||||||
setImageError(true);
|
setImageError(true);
|
||||||
};
|
};
|
||||||
@@ -182,7 +234,7 @@ return (
|
|||||||
<img
|
<img
|
||||||
src={
|
src={
|
||||||
post.imageUrl
|
post.imageUrl
|
||||||
? `${MINIO_ENDPOINT}/${BUCKET_NAME}/${post.imageFilename}`
|
? `${API_BASE_URL}/api/marketing/analysis/images/${post.imageFilename}`
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
alt={post.theme}
|
alt={post.theme}
|
||||||
@@ -228,20 +280,15 @@ interface PostCalendarItem {
|
|||||||
|
|
||||||
interface PostCardProps {
|
interface PostCardProps {
|
||||||
post: PostCalendarItem;
|
post: PostCalendarItem;
|
||||||
minioEndpoint: string;
|
apiBaseUrl: string;
|
||||||
bucketName: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PostCard: React.FC<PostCardProps> = ({
|
const PostCard: React.FC<PostCardProps> = ({ post, apiBaseUrl }) => {
|
||||||
post,
|
|
||||||
minioEndpoint,
|
|
||||||
bucketName,
|
|
||||||
}) => {
|
|
||||||
const [imageError, setImageError] = useState(false);
|
const [imageError, setImageError] = useState(false);
|
||||||
|
|
||||||
const getImageUrl = (): string | null => {
|
const getImageUrl = (): string | null => {
|
||||||
if (!post.imageFilename) return null;
|
if (!post.imageFilename) return null;
|
||||||
return `${minioEndpoint}/${bucketName}/${post.imageFilename}`;
|
return `${apiBaseUrl}/api/marketing/analysis/images/${post.imageFilename}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const imageUrl = getImageUrl();
|
const imageUrl = getImageUrl();
|
||||||
@@ -360,15 +407,14 @@ interface PostCalendarItem {
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
post: PostCalendarItem;
|
post: PostCalendarItem;
|
||||||
minioEndpoint: string;
|
apiBaseUrl: string;
|
||||||
bucketName: string;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const imageError = ref(false);
|
const imageError = ref(false);
|
||||||
|
|
||||||
const imageUrl = computed(() => {
|
const imageUrl = computed(() => {
|
||||||
if (!props.post.imageFilename) return null;
|
if (!props.post.imageFilename) return null;
|
||||||
return `${props.minioEndpoint}/${props.bucketName}/${props.post.imageFilename}`;
|
return `${props.apiBaseUrl}/api/marketing/analysis/images/${props.post.imageFilename}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
const formatDate = (dateString: string) => {
|
||||||
@@ -389,10 +435,11 @@ const formatDate = (dateString: string) => {
|
|||||||
|
|
||||||
### 2. Хранение изображений
|
### 2. Хранение изображений
|
||||||
|
|
||||||
- Все изображения хранятся в MinIO
|
- Все изображения хранятся на бэкенде
|
||||||
- Формат изображений: **PNG**
|
- Формат изображений: **PNG**
|
||||||
- Размер изображений: **1024x1024 пикселей**
|
- Размер изображений: **1024x1024 пикселей**
|
||||||
- Имя файла уникально для каждого поста
|
- Имя файла уникально для каждого поста
|
||||||
|
- Доступ к изображениям только через API эндпоинт
|
||||||
|
|
||||||
### 3. Публикация постов
|
### 3. Публикация постов
|
||||||
|
|
||||||
@@ -410,16 +457,13 @@ const formatDate = (dateString: string) => {
|
|||||||
|
|
||||||
## Конфигурация
|
## Конфигурация
|
||||||
|
|
||||||
Для работы с изображениями вам понадобятся следующие настройки:
|
Для работы с изображениями вам понадобится базовый URL вашего API:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// config.ts
|
// config.ts
|
||||||
export const MINIO_CONFIG = {
|
export const API_CONFIG = {
|
||||||
endpoint: 'http://92.38.48.166:9000', // или ваш MinIO endpoint
|
baseUrl: 'http://your-backend-url', // URL вашего бэкенда
|
||||||
bucketName: 'konturai',
|
// Например: 'http://localhost:8080' или 'https://api.example.com'
|
||||||
// Если требуется авторизация:
|
|
||||||
// accessKey: 'your-access-key',
|
|
||||||
// secretKey: 'your-secret-key',
|
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -439,7 +483,12 @@ export const MINIO_CONFIG = {
|
|||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
{
|
{
|
||||||
hasImage && <img src={getImageUrl(post)} alt={post.theme} />;
|
hasImage && (
|
||||||
|
<img
|
||||||
|
src={`${API_BASE_URL}/api/marketing/analysis/images/${post.imageFilename}`}
|
||||||
|
alt={post.theme}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -459,9 +508,10 @@ export const MINIO_CONFIG = {
|
|||||||
При возникновении проблем:
|
При возникновении проблем:
|
||||||
|
|
||||||
1. Проверьте, что `imageUrl` и `imageFilename` не `null`
|
1. Проверьте, что `imageUrl` и `imageFilename` не `null`
|
||||||
2. Убедитесь, что MinIO доступен и изображение существует
|
2. Убедитесь, что используете правильный API эндпоинт: `/api/marketing/analysis/images/{imageFilename}`
|
||||||
3. Проверьте консоль браузера на наличие ошибок CORS (если обращаетесь к MinIO напрямую)
|
3. Проверьте, что JWT токен валиден и передается в заголовке `Authorization`
|
||||||
4. Убедитесь, что используете правильный endpoint и bucket name
|
4. Проверьте консоль браузера на наличие ошибок сети или авторизации
|
||||||
|
5. Убедитесь, что используете правильный базовый URL API
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -201,6 +201,45 @@ public class MarketingController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает изображение поста по имени файла
|
||||||
|
*
|
||||||
|
* @param authHeader Authorization header с JWT токеном
|
||||||
|
* @param imageFilename Имя файла изображения
|
||||||
|
* @return Изображение в формате PNG
|
||||||
|
*/
|
||||||
|
@GetMapping("/images/{imageFilename}")
|
||||||
|
public ResponseEntity<byte[]> getPostImage(
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
|
@PathVariable String imageFilename) {
|
||||||
|
|
||||||
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
|
if (userId == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем существование файла в MinIO
|
||||||
|
try {
|
||||||
|
if (!minIOService.fileExists(imageFilename)) {
|
||||||
|
logger.warn("Image file not found: {}", imageFilename);
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загружаем изображение из MinIO
|
||||||
|
InputStream inputStream = minIOService.downloadFile(imageFilename);
|
||||||
|
byte[] bytes = inputStream.readAllBytes();
|
||||||
|
inputStream.close();
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE)
|
||||||
|
.header(HttpHeaders.CACHE_CONTROL, "public, max-age=3600")
|
||||||
|
.body(bytes);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Error loading image {}: {}", imageFilename, e.getMessage(), e);
|
||||||
|
return ResponseEntity.internalServerError().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/strategy/generate")
|
@PostMapping("/strategy/generate")
|
||||||
public ResponseEntity<?> generateStrategy(
|
public ResponseEntity<?> generateStrategy(
|
||||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
|
|||||||
Reference in New Issue
Block a user