This commit is contained in:
root
2025-12-05 01:16:16 +05:00
parent 0a37e9a681
commit 95ba34e7fa
2 changed files with 140 additions and 51 deletions
+101 -51
View File
@@ -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,