add restaurants check connection
This commit is contained in:
@@ -90,7 +90,17 @@
|
|||||||
"useHttps": "Use HTTPS",
|
"useHttps": "Use HTTPS",
|
||||||
"confirmDelete": "Delete Restaurant",
|
"confirmDelete": "Delete Restaurant",
|
||||||
"noRestaurants": "No restaurants found. Click \"Add Restaurant\" to create one.",
|
"noRestaurants": "No restaurants found. Click \"Add Restaurant\" to create one.",
|
||||||
"deleteConfirmation": "Are you sure you want to delete this restaurant? This action cannot be undone."
|
"deleteConfirmation": "Are you sure you want to delete this restaurant? This action cannot be undone.",
|
||||||
|
"check": "Check connection",
|
||||||
|
"checkError": "Error",
|
||||||
|
"loadError": "Failed to load restaurants",
|
||||||
|
"createSuccess": "Restaurant created successfully",
|
||||||
|
"updateSuccess": "Restaurant updated successfully",
|
||||||
|
"deleteSuccess": "Restaurant deleted",
|
||||||
|
"httpsUpdateSuccess": "HTTPS status updated",
|
||||||
|
"httpsUpdateError": "Failed to update HTTPS",
|
||||||
|
"passwordRequired": "Password is required for new restaurant",
|
||||||
|
"checkNetworkError": "Network error while checking"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Application Settings",
|
"title": "Application Settings",
|
||||||
|
|||||||
@@ -90,7 +90,17 @@
|
|||||||
"useHttps": "Использовать HTTPS",
|
"useHttps": "Использовать HTTPS",
|
||||||
"confirmDelete": "Удалить ресторан",
|
"confirmDelete": "Удалить ресторан",
|
||||||
"noRestaurants": "Ресторанов не найдено. Нажмите \"Добавить ресторан\", чтобы создать его.",
|
"noRestaurants": "Ресторанов не найдено. Нажмите \"Добавить ресторан\", чтобы создать его.",
|
||||||
"deleteConfirmation": "Вы уверены, что хотите удалить этот ресторан? Это действие необратимо."
|
"deleteConfirmation": "Вы уверены, что хотите удалить этот ресторан? Это действие необратимо.",
|
||||||
|
"check": "Проверить подключение",
|
||||||
|
"checkError": "Ошибка",
|
||||||
|
"loadError": "Ошибка загрузки списка ресторанов",
|
||||||
|
"createSuccess": "Ресторан успешно создан",
|
||||||
|
"updateSuccess": "Ресторан успешно обновлён",
|
||||||
|
"deleteSuccess": "Ресторан удалён",
|
||||||
|
"httpsUpdateSuccess": "Статус HTTPS обновлён",
|
||||||
|
"httpsUpdateError": "Не удалось обновить HTTPS",
|
||||||
|
"passwordRequired": "Пароль обязателен для нового ресторана",
|
||||||
|
"checkNetworkError": "Ошибка сети при проверке"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Настройки приложения",
|
"title": "Настройки приложения",
|
||||||
|
|||||||
@@ -43,17 +43,40 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ rest.login }}</td>
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ rest.login }}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ formatDate(rest.created) }}</td>
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ formatDate(rest.created) }}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-3">
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
<div class="flex items-center justify-end space-x-3">
|
||||||
|
<!-- Кнопка проверки -->
|
||||||
|
<button
|
||||||
|
@click="checkRestaurant(rest)"
|
||||||
|
:disabled="rest.checking"
|
||||||
|
class="text-green-600 hover:text-green-800 transition-colors disabled:opacity-50"
|
||||||
|
:title="t('restaurants.check')"
|
||||||
|
>
|
||||||
|
<svg v-if="!rest.checking" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else class="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<!-- Кнопка редактирования -->
|
||||||
<button @click="openModal('edit', rest)" class="text-blue-600 hover:text-blue-800 transition-colors">
|
<button @click="openModal('edit', rest)" class="text-blue-600 hover:text-blue-800 transition-colors">
|
||||||
<svg class="w-5 h-5 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- Кнопка удаления -->
|
||||||
<button @click="confirmDelete(rest.id)" class="text-red-600 hover:text-red-800 transition-colors">
|
<button @click="confirmDelete(rest.id)" class="text-red-600 hover:text-red-800 transition-colors">
|
||||||
<svg class="w-5 h-5 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- Результат проверки только при успехе (ms) -->
|
||||||
|
<span v-if="rest.checkResult" class="text-xs text-gray-500 ml-1 whitespace-nowrap">
|
||||||
|
{{ rest.checkResult }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="restaurants.length === 0">
|
<tr v-if="restaurants.length === 0">
|
||||||
@@ -64,7 +87,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Модалка создания/редактирования ресторана -->
|
<!-- Модалка создания/редактирования (без изменений) -->
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<div v-if="modalOpen" class="fixed inset-0 z-50 overflow-y-auto" @click.self="closeModal">
|
<div v-if="modalOpen" class="fixed inset-0 z-50 overflow-y-auto" @click.self="closeModal">
|
||||||
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm"></div>
|
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm"></div>
|
||||||
@@ -116,7 +139,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
<!-- Модалка подтверждения удаления ресторана -->
|
<!-- Модалка подтверждения удаления -->
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<div v-if="deleteConfirm.show" class="fixed inset-0 z-50 overflow-y-auto" @click.self="deleteConfirm.show = false">
|
<div v-if="deleteConfirm.show" class="fixed inset-0 z-50 overflow-y-auto" @click.self="deleteConfirm.show = false">
|
||||||
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm"></div>
|
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm"></div>
|
||||||
@@ -132,13 +155,26 @@
|
|||||||
<p class="text-sm text-gray-500 mb-6">{{ t('restaurants.deleteConfirmation') }}</p>
|
<p class="text-sm text-gray-500 mb-6">{{ t('restaurants.deleteConfirmation') }}</p>
|
||||||
<div class="flex justify-center space-x-3">
|
<div class="flex justify-center space-x-3">
|
||||||
<button @click="deleteConfirm.show = false" class="btn-secondary">{{ t('app.cancel') }}</button>
|
<button @click="deleteConfirm.show = false" class="btn-secondary">{{ t('app.cancel') }}</button>
|
||||||
<button @click="deleteUser(deleteConfirm.id)" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors">{{ t('app.delete') }}</button>
|
<button @click="deleteRestaurant(deleteConfirm.id)" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors">{{ t('app.delete') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
|
<!-- Уведомления (toast) -->
|
||||||
|
<Transition name="slide">
|
||||||
|
<div v-if="notification.show" class="fixed bottom-4 right-4 z-50 flex items-center space-x-2 px-4 py-3 rounded-lg shadow-lg" :class="notification.type === 'success' ? 'bg-green-50 text-green-800 border border-green-200' : 'bg-red-50 text-red-800 border border-red-200'">
|
||||||
|
<svg v-if="notification.type === 'success'" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<span>{{ notification.message }}</span>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -148,16 +184,49 @@ import AppLayout from '../components/Layout/AppLayout.vue';
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const restaurants = ref([]);
|
|
||||||
|
type Restaurant = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
host: string;
|
||||||
|
https: boolean;
|
||||||
|
login: string;
|
||||||
|
created: string;
|
||||||
|
checking?: boolean;
|
||||||
|
checkResult?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const restaurants = ref<Restaurant[]>([]);
|
||||||
const modalOpen = ref(false);
|
const modalOpen = ref(false);
|
||||||
const modalMode = ref<'create' | 'edit'>('create');
|
const modalMode = ref<'create' | 'edit'>('create');
|
||||||
const form = ref({ id: null, name: '', login: '', password: '', host: '', https: false });
|
const form = ref({ id: null, name: '', login: '', password: '', host: '', https: false });
|
||||||
const modalTitle = ref('');
|
const modalTitle = ref('');
|
||||||
const deleteConfirm = ref({ show: false, id: null });
|
const deleteConfirm = ref({ show: false, id: null });
|
||||||
|
|
||||||
|
// Уведомления
|
||||||
|
const notification = ref({ show: false, type: 'success' as 'success' | 'error', message: '' });
|
||||||
|
|
||||||
|
function showNotification(message: string, type: 'success' | 'error') {
|
||||||
|
notification.value = { show: true, type, message };
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.value.show = false;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadRestaurants() {
|
async function loadRestaurants() {
|
||||||
|
try {
|
||||||
const res = await fetch('/api/admin/restaurants');
|
const res = await fetch('/api/admin/restaurants');
|
||||||
restaurants.value = await res.json();
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
const data = await res.json();
|
||||||
|
restaurants.value = data.map((r: any) => ({
|
||||||
|
...r,
|
||||||
|
checking: false,
|
||||||
|
checkResult: null
|
||||||
|
}));
|
||||||
|
} catch (e: any) {
|
||||||
|
showNotification(t('restaurants.loadError'), 'error');
|
||||||
|
restaurants.value = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(dateStr: string) {
|
function formatDate(dateStr: string) {
|
||||||
@@ -165,12 +234,37 @@ function formatDate(dateStr: string) {
|
|||||||
return new Date(dateStr).toLocaleString();
|
return new Date(dateStr).toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function openModal(mode: 'create' | 'edit', rest: any = null) {
|
async function checkRestaurant(rest: Restaurant) {
|
||||||
|
rest.checking = true;
|
||||||
|
rest.checkResult = null; // сбрасываем
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/restaurants/${rest.id}/check`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
rest.checkResult = `${data.latency_ms} ms`; // показываем в таблице
|
||||||
|
} else {
|
||||||
|
const errorText = data.error || 'Unknown error';
|
||||||
|
showNotification(`${t('restaurants.checkError')}: ${errorText}`, 'error');
|
||||||
|
// rest.checkResult остаётся null -> ничего не показываем
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
const msg = t('restaurants.checkNetworkError');
|
||||||
|
showNotification(`${msg}: ${error.message}`, 'error');
|
||||||
|
// rest.checkResult остаётся null
|
||||||
|
} finally {
|
||||||
|
rest.checking = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal(mode: 'create' | 'edit', rest: Restaurant | null = null) {
|
||||||
modalMode.value = mode;
|
modalMode.value = mode;
|
||||||
if (mode === 'create') {
|
if (mode === 'create') {
|
||||||
form.value = { id: null, name: '', login: '', password: '', host: '', https: false };
|
form.value = { id: null, name: '', login: '', password: '', host: '', https: false };
|
||||||
modalTitle.value = t('restaurants.add');
|
modalTitle.value = t('restaurants.add');
|
||||||
} else {
|
} else {
|
||||||
|
if (rest) {
|
||||||
form.value = {
|
form.value = {
|
||||||
id: rest.id,
|
id: rest.id,
|
||||||
name: rest.name,
|
name: rest.name,
|
||||||
@@ -181,6 +275,7 @@ function openModal(mode: 'create' | 'edit', rest: any = null) {
|
|||||||
};
|
};
|
||||||
modalTitle.value = t('restaurants.edit');
|
modalTitle.value = t('restaurants.edit');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
modalOpen.value = true;
|
modalOpen.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +283,7 @@ function closeModal() {
|
|||||||
modalOpen.value = false;
|
modalOpen.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleHttps(rest: any) {
|
async function toggleHttps(rest: Restaurant) {
|
||||||
const newHttps = !rest.https;
|
const newHttps = !rest.https;
|
||||||
const payload = {
|
const payload = {
|
||||||
name: rest.name,
|
name: rest.name,
|
||||||
@@ -204,15 +299,22 @@ async function toggleHttps(rest: any) {
|
|||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
rest.https = newHttps;
|
rest.https = newHttps;
|
||||||
|
showNotification(t('restaurants.httpsUpdateSuccess'), 'success');
|
||||||
} else {
|
} else {
|
||||||
alert('Failed to update HTTPS status');
|
const errText = await res.text();
|
||||||
|
showNotification(`${t('restaurants.httpsUpdateError')}: ${errText}`, 'error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
alert('Network error');
|
showNotification(`${t('restaurants.httpsUpdateError')}: ${e.message}`, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitRestaurant() {
|
async function submitRestaurant() {
|
||||||
|
if (modalMode.value === 'create' && !form.value.password) {
|
||||||
|
showNotification(t('restaurants.passwordRequired'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
name: form.value.name,
|
name: form.value.name,
|
||||||
@@ -221,29 +323,28 @@ async function submitRestaurant() {
|
|||||||
login: form.value.login,
|
login: form.value.login,
|
||||||
...(form.value.password ? { password: form.value.password } : {})
|
...(form.value.password ? { password: form.value.password } : {})
|
||||||
};
|
};
|
||||||
|
let response;
|
||||||
if (modalMode.value === 'create') {
|
if (modalMode.value === 'create') {
|
||||||
if (!form.value.password) {
|
response = await fetch('/api/admin/restaurants', {
|
||||||
alert('Password is required');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const res = await fetch('/api/admin/restaurants', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error('Create failed');
|
if (!response.ok) throw new Error('Create failed');
|
||||||
|
showNotification(t('restaurants.createSuccess'), 'success');
|
||||||
} else {
|
} else {
|
||||||
const res = await fetch(`/api/admin/restaurants/${form.value.id}`, {
|
response = await fetch(`/api/admin/restaurants/${form.value.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error('Update failed');
|
if (!response.ok) throw new Error('Update failed');
|
||||||
|
showNotification(t('restaurants.updateSuccess'), 'success');
|
||||||
}
|
}
|
||||||
await loadRestaurants();
|
await loadRestaurants();
|
||||||
closeModal();
|
closeModal();
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
alert('Operation failed: ' + e.message);
|
showNotification(e.message, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,15 +353,28 @@ function confirmDelete(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteRestaurant(id: number) {
|
async function deleteRestaurant(id: number) {
|
||||||
await fetch(`/api/admin/restaurants/${id}`, { method: 'DELETE' });
|
try {
|
||||||
|
const res = await fetch(`/api/admin/restaurants/${id}`, { method: 'DELETE' });
|
||||||
|
if (!res.ok) throw new Error('Delete failed');
|
||||||
|
showNotification(t('restaurants.deleteSuccess'), 'success');
|
||||||
await loadRestaurants();
|
await loadRestaurants();
|
||||||
|
} catch (e: any) {
|
||||||
|
showNotification(e.message, 'error');
|
||||||
|
} finally {
|
||||||
deleteConfirm.value.show = false;
|
deleteConfirm.value.show = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadRestaurants);
|
onMounted(loadRestaurants);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
.animate-spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
.fade-enter-active,
|
.fade-enter-active,
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
@@ -269,4 +383,13 @@ onMounted(loadRestaurants);
|
|||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
.slide-enter-active,
|
||||||
|
.slide-leave-active {
|
||||||
|
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
.slide-enter-from,
|
||||||
|
.slide-leave-to {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import su.xserver.iikocon.handler.AdminHandler;
|
|||||||
import su.xserver.iikocon.handler.AuthHandler;
|
import su.xserver.iikocon.handler.AuthHandler;
|
||||||
import su.xserver.iikocon.handler.SecurityHandler;
|
import su.xserver.iikocon.handler.SecurityHandler;
|
||||||
import su.xserver.iikocon.handler.SetupHandler;
|
import su.xserver.iikocon.handler.SetupHandler;
|
||||||
|
import su.xserver.iikocon.iiko.IikoOlapClient;
|
||||||
import su.xserver.iikocon.service.*;
|
import su.xserver.iikocon.service.*;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -350,6 +351,24 @@ public class MainVerticle extends AbstractVerticle {
|
|||||||
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
|
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get("/api/admin/restaurants/:id/check").handler(rc -> {
|
||||||
|
int id = Integer.parseInt(rc.pathParam("id"));
|
||||||
|
restaurantService.findById(id)
|
||||||
|
.onSuccess(rest -> {
|
||||||
|
if (rest == null) {
|
||||||
|
rc.response().setStatusCode(404).end();
|
||||||
|
} else {
|
||||||
|
IikoOlapClient iiko = new IikoOlapClient(vertx, rest);
|
||||||
|
|
||||||
|
iiko.checkConnection()
|
||||||
|
.onSuccess(res -> rc.response().putHeader("Content-Type", "application/json").end(res.encode()))
|
||||||
|
.onFailure(err -> rc.response().putHeader("Content-Type", "application/json").end(
|
||||||
|
new JsonObject().put("success", false).put("error", err.getMessage()).encode()));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
|
||||||
|
});
|
||||||
|
|
||||||
router.post("/api/admin/restaurants").handler(rc -> {
|
router.post("/api/admin/restaurants").handler(rc -> {
|
||||||
JsonObject body = rc.body().asJsonObject();
|
JsonObject body = rc.body().asJsonObject();
|
||||||
String name = body.getString("name");
|
String name = body.getString("name");
|
||||||
|
|||||||
139
src/main/java/su/xserver/iikocon/iiko/IikoOlapClient.java
Normal file
139
src/main/java/su/xserver/iikocon/iiko/IikoOlapClient.java
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package su.xserver.iikocon.iiko;
|
||||||
|
|
||||||
|
import io.vertx.core.Future;
|
||||||
|
import io.vertx.core.Promise;
|
||||||
|
import io.vertx.core.Vertx;
|
||||||
|
import io.vertx.core.json.JsonObject;
|
||||||
|
import io.vertx.ext.web.client.WebClient;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class IikoOlapClient {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(IikoOlapClient.class);
|
||||||
|
private final WebClient webClient;
|
||||||
|
private final String iikoHost;
|
||||||
|
private final String iikoLogin;
|
||||||
|
private final String iikoPassHash;
|
||||||
|
|
||||||
|
public IikoOlapClient(Vertx vertx, String host, String login, String passHash, boolean https) {
|
||||||
|
this.webClient = WebClient.create(vertx);
|
||||||
|
this.iikoHost = (https ? "https://" : "http://") + host + (https ? ":443" : ":80");
|
||||||
|
this.iikoLogin = login;
|
||||||
|
this.iikoPassHash = passHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IikoOlapClient(Vertx vertx, JsonObject rest) {
|
||||||
|
this.webClient = WebClient.create(vertx);
|
||||||
|
this.iikoHost = (rest.getBoolean("https") ? "https://" : "http://") + rest.getString("host") + (rest.getBoolean("https") ? ":443" : ":80");
|
||||||
|
this.iikoLogin = rest.getString("login");
|
||||||
|
this.iikoPassHash = rest.getString("password");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Future<String> authenticate() {
|
||||||
|
Promise<String> promise = Promise.promise();
|
||||||
|
String url = iikoHost + "/resto/api/auth"; //?login=" + iikoLogin + "&pass=" + iikoPassHash;
|
||||||
|
|
||||||
|
webClient.getAbs(url)
|
||||||
|
.addQueryParam("login", iikoLogin)
|
||||||
|
.addQueryParam("pass", iikoPassHash)
|
||||||
|
.send()
|
||||||
|
.onSuccess(resp -> {
|
||||||
|
if (resp.statusCode() == 200) {
|
||||||
|
String token = resp.bodyAsString();
|
||||||
|
log.info("Authenticated, token: {}", token);
|
||||||
|
promise.complete(token);
|
||||||
|
} else {
|
||||||
|
promise.fail("Auth failed for " + iikoLogin + ": " + resp.statusCode());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onFailure(promise::fail);
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Future<Void> logout(String token) {
|
||||||
|
if (token == null || token.isEmpty()) {
|
||||||
|
return Future.succeededFuture();
|
||||||
|
}
|
||||||
|
Promise<Void> promise = Promise.promise();
|
||||||
|
String url = iikoHost + "/resto/api/logout";
|
||||||
|
webClient.getAbs(url)
|
||||||
|
.addQueryParam("key", token)
|
||||||
|
.send()
|
||||||
|
.onSuccess(resp -> {
|
||||||
|
// log.info("Logout completed for token, status {}", resp.statusCode());
|
||||||
|
log.info(resp.bodyAsString());
|
||||||
|
promise.complete();
|
||||||
|
})
|
||||||
|
.onFailure(err -> {
|
||||||
|
log.error("Logout request failed: {}", err.getMessage());
|
||||||
|
promise.complete();
|
||||||
|
});
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Future<JsonObject> checkConnection() {
|
||||||
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
|
long time = System.currentTimeMillis();
|
||||||
|
|
||||||
|
authenticate()
|
||||||
|
.onSuccess(token -> {
|
||||||
|
logout(token).mapEmpty();
|
||||||
|
promise.complete(new JsonObject()
|
||||||
|
.put("success", true)
|
||||||
|
.put("latency_ms", System.currentTimeMillis() - time)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.onFailure(promise::fail);
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Future<JsonObject> handleGet(String uri, JsonObject params) {
|
||||||
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
|
authenticate()
|
||||||
|
.onSuccess(token -> {
|
||||||
|
String url = appendQueryParams(iikoHost + uri, params.put("key", token));
|
||||||
|
log.info("Request to : {}", url);
|
||||||
|
webClient.getAbs(url)
|
||||||
|
.send()
|
||||||
|
.onSuccess(resp -> {
|
||||||
|
if (resp.statusCode() == 200) {
|
||||||
|
JsonObject body = resp.bodyAsJsonObject();
|
||||||
|
// Если есть обёртка data, распаковываем
|
||||||
|
JsonObject data = body.containsKey("data") && body.getValue("data") instanceof JsonObject
|
||||||
|
? body.getJsonObject("data")
|
||||||
|
: body;
|
||||||
|
logout(token).mapEmpty();
|
||||||
|
promise.complete(data);
|
||||||
|
} else {
|
||||||
|
promise.fail("Failed request to " + iikoHost + ": HTTP " + resp.statusCode());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onFailure(promise::fail);
|
||||||
|
})
|
||||||
|
.onFailure(promise::fail);
|
||||||
|
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toQueryString(JsonObject params) {
|
||||||
|
if (params == null || params.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return "?" + params.stream()
|
||||||
|
.map(entry -> URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8) + "=" +
|
||||||
|
URLEncoder.encode(entry.getValue().toString(), StandardCharsets.UTF_8))
|
||||||
|
.collect(Collectors.joining("&"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String appendQueryParams(String url, JsonObject params) {
|
||||||
|
return url + toQueryString(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,50 +1,30 @@
|
|||||||
package su.xserver.iikocon.test;
|
package su.xserver.iikocon.iiko;
|
||||||
|
|
||||||
import io.vertx.core.Future;
|
import io.vertx.core.Future;
|
||||||
import io.vertx.core.Promise;
|
import io.vertx.core.Promise;
|
||||||
import io.vertx.core.Vertx;
|
import io.vertx.core.Vertx;
|
||||||
import io.vertx.core.json.JsonArray;
|
import io.vertx.core.json.JsonArray;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
import io.vertx.ext.web.client.WebClient;
|
|
||||||
import io.vertx.ext.web.client.WebClientOptions;
|
|
||||||
import io.vertx.mysqlclient.MySQLConnectOptions;
|
import io.vertx.mysqlclient.MySQLConnectOptions;
|
||||||
import io.vertx.sqlclient.*;
|
import io.vertx.sqlclient.Pool;
|
||||||
|
import io.vertx.sqlclient.PoolOptions;
|
||||||
|
import io.vertx.sqlclient.Tuple;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HexFormat;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class IikoOlapColumnsImporter {
|
public class IikoOlapColumnsImporter {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(IikoOlapColumnsImporter.class);
|
private static final Logger log = LoggerFactory.getLogger(IikoOlapColumnsImporter.class);
|
||||||
private final WebClient httpClient;
|
|
||||||
private final Pool dbPool;
|
private final Pool dbPool;
|
||||||
private final String iikoServer;
|
private final IikoOlapClient iikoOlapClient;
|
||||||
private final String iikoLogin;
|
|
||||||
private final String iikoPassword;
|
|
||||||
private static long time;
|
|
||||||
|
|
||||||
private static final List<String> REPORT_TYPES = List.of("SALES", "TRANSACTIONS", "DELIVERIES");
|
private static final List<String> REPORT_TYPES = List.of("SALES", "TRANSACTIONS", "DELIVERIES");
|
||||||
|
|
||||||
public IikoOlapColumnsImporter(Vertx vertx,
|
public IikoOlapColumnsImporter(Vertx vertx, String iikoServer, String iikoLogin, String iikoPassword, String dbHost, int dbPort, String dbName, String dbUser, String dbPassword) {
|
||||||
String iikoServer,
|
this.iikoOlapClient = new IikoOlapClient(vertx, iikoServer, iikoLogin, iikoPassword, true);
|
||||||
String iikoLogin,
|
|
||||||
String iikoPassword,
|
|
||||||
String dbHost, int dbPort,
|
|
||||||
String dbName, String dbUser, String dbPassword) {
|
|
||||||
WebClientOptions options = new WebClientOptions()
|
|
||||||
.setSsl(true)
|
|
||||||
.setTrustAll(true)
|
|
||||||
.setVerifyHost(false);
|
|
||||||
this.httpClient = WebClient.create(vertx, options);
|
|
||||||
this.iikoServer = iikoServer;
|
|
||||||
this.iikoLogin = iikoLogin;
|
|
||||||
this.iikoPassword = iikoPassword;
|
|
||||||
|
|
||||||
MySQLConnectOptions connectOptions = new MySQLConnectOptions()
|
MySQLConnectOptions connectOptions = new MySQLConnectOptions()
|
||||||
.setHost(dbHost)
|
.setHost(dbHost)
|
||||||
.setPort(dbPort)
|
.setPort(dbPort)
|
||||||
@@ -56,7 +36,6 @@ public class IikoOlapColumnsImporter {
|
|||||||
this.dbPool = Pool.pool(vertx, connectOptions, poolOptions);
|
this.dbPool = Pool.pool(vertx, connectOptions, poolOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Главный метод: последовательно для каждого reportType делаем auth -> fetch -> store -> logout
|
|
||||||
public Future<Void> fetchAndStoreAll() {
|
public Future<Void> fetchAndStoreAll() {
|
||||||
return createTablesIfNotExist()
|
return createTablesIfNotExist()
|
||||||
.compose(v -> processAllReportTypesSequentially())
|
.compose(v -> processAllReportTypesSequentially())
|
||||||
@@ -65,7 +44,6 @@ public class IikoOlapColumnsImporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Future<Void> processAllReportTypesSequentially() {
|
private Future<Void> processAllReportTypesSequentially() {
|
||||||
time = System.currentTimeMillis();
|
|
||||||
Future<Void> result = Future.succeededFuture();
|
Future<Void> result = Future.succeededFuture();
|
||||||
for (String reportType : REPORT_TYPES) {
|
for (String reportType : REPORT_TYPES) {
|
||||||
result = result.compose(v -> processOneReportType(reportType));
|
result = result.compose(v -> processOneReportType(reportType));
|
||||||
@@ -75,91 +53,19 @@ public class IikoOlapColumnsImporter {
|
|||||||
|
|
||||||
private Future<Void> processOneReportType(String reportType) {
|
private Future<Void> processOneReportType(String reportType) {
|
||||||
log.info("Processing report type: {}", reportType);
|
log.info("Processing report type: {}", reportType);
|
||||||
return authenticate()
|
return fetchColumnsFromIiko(reportType)
|
||||||
.compose(token -> {
|
.compose(columnsJson -> storeColumnsToDb(reportType, columnsJson));
|
||||||
return fetchColumnsFromIiko(reportType, token)
|
|
||||||
.compose(columnsJson -> storeColumnsToDb(reportType, columnsJson))
|
|
||||||
.onComplete(ignored -> logout(token)); // logout всегда, даже при ошибке
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Аутентификация: GET /resto/api/auth?login=...&pass=SHA1
|
|
||||||
private Future<String> authenticate() {
|
|
||||||
Promise<String> promise = Promise.promise();
|
|
||||||
String passHash = sha1(iikoPassword);
|
|
||||||
String url = "https://" + iikoServer + ":443/resto/api/auth?login=" + iikoLogin + "&pass=" + passHash;
|
|
||||||
|
|
||||||
httpClient.getAbs(url)
|
|
||||||
.send()
|
|
||||||
.onSuccess(resp -> {
|
|
||||||
if (resp.statusCode() == 200) {
|
|
||||||
String token = resp.bodyAsString();
|
|
||||||
log.info("Authenticated, token: {}", token);
|
|
||||||
promise.complete(token);
|
|
||||||
} else {
|
|
||||||
promise.fail("Auth failed for " + iikoLogin + ": " + resp.statusCode());
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onFailure(promise::fail);
|
|
||||||
return promise.future();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout: GET /resto/api/logout?key=токен
|
|
||||||
private Future<Void> logout(String token) {
|
|
||||||
if (token == null || token.isEmpty()) {
|
|
||||||
return Future.succeededFuture();
|
|
||||||
}
|
|
||||||
Promise<Void> promise = Promise.promise();
|
|
||||||
String url = "https://" + iikoServer + "/resto/api/logout?key=" + token;
|
|
||||||
httpClient.getAbs(url)
|
|
||||||
.send()
|
|
||||||
.onSuccess(resp -> {
|
|
||||||
log.info("Logout completed for token, status {}", resp.statusCode());
|
|
||||||
promise.complete();
|
|
||||||
})
|
|
||||||
.onFailure(err -> {
|
|
||||||
log.error("Logout request failed: {}", err.getMessage());
|
|
||||||
promise.complete(); // не ломаем цепочку
|
|
||||||
});
|
|
||||||
return promise.future();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Запрос полей для конкретного reportType
|
// Запрос полей для конкретного reportType
|
||||||
private Future<JsonObject> fetchColumnsFromIiko(String reportType, String token) {
|
private Future<JsonObject> fetchColumnsFromIiko(String reportType) {
|
||||||
Promise<JsonObject> promise = Promise.promise();
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
String url = "https://" + iikoServer + "/resto/api/v2/reports/olap/columns?key=" + token + "&reportType=" + reportType;
|
|
||||||
|
|
||||||
log.info("Connect to : {}", url);
|
iikoOlapClient.handleGet("/resto/api/v2/reports/olap/columns", new JsonObject().put("reportType", reportType))
|
||||||
|
.onSuccess(promise::complete)
|
||||||
httpClient.getAbs(url)
|
|
||||||
.send()
|
|
||||||
.onSuccess(resp -> {
|
|
||||||
if (resp.statusCode() == 200) {
|
|
||||||
JsonObject body = resp.bodyAsJsonObject();
|
|
||||||
// Если есть обёртка data, распаковываем
|
|
||||||
JsonObject data = body.containsKey("data") && body.getValue("data") instanceof JsonObject
|
|
||||||
? body.getJsonObject("data")
|
|
||||||
: body;
|
|
||||||
promise.complete(data);
|
|
||||||
|
|
||||||
log.info("time: {}", (System.currentTimeMillis() - time) + "ms");
|
|
||||||
} else {
|
|
||||||
promise.fail("Failed to fetch columns for " + reportType + ": HTTP " + resp.statusCode());
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onFailure(promise::fail);
|
.onFailure(promise::fail);
|
||||||
return promise.future();
|
|
||||||
}
|
|
||||||
|
|
||||||
// SHA-1
|
return promise.future();
|
||||||
private String sha1(String input) {
|
|
||||||
try {
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
|
||||||
byte[] digest = md.digest(input.getBytes());
|
|
||||||
return HexFormat.of().formatHex(digest).toLowerCase();
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Методы работы с БД (с префиксом iiko_) ----------
|
// ---------- Методы работы с БД (с префиксом iiko_) ----------
|
||||||
@@ -366,9 +272,4 @@ public class IikoOlapColumnsImporter {
|
|||||||
default -> "string";
|
default -> "string";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() {
|
|
||||||
dbPool.close();
|
|
||||||
httpClient.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package su.xserver.iikocon.test;
|
package su.xserver.iikocon.iiko;
|
||||||
|
|
||||||
import io.vertx.core.Vertx;
|
import io.vertx.core.Vertx;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -16,7 +16,7 @@ public class Main {
|
|||||||
vertx,
|
vertx,
|
||||||
"folk-amber-co.iiko.it", // без https://
|
"folk-amber-co.iiko.it", // без https://
|
||||||
"4444",
|
"4444",
|
||||||
"4444",
|
"92f2fd99879b0c2466ab8648afb63c49032379c1",
|
||||||
"phpmyadmin.xserver.su", // хост MariaDB
|
"phpmyadmin.xserver.su", // хост MariaDB
|
||||||
3306,
|
3306,
|
||||||
"test", // имя БД
|
"test", // имя БД
|
||||||
@@ -5,6 +5,7 @@ import io.vertx.core.json.JsonObject;
|
|||||||
import io.vertx.ext.healthchecks.Status;
|
import io.vertx.ext.healthchecks.Status;
|
||||||
import io.vertx.ext.web.Router;
|
import io.vertx.ext.web.Router;
|
||||||
import io.vertx.ext.web.healthchecks.HealthCheckHandler;
|
import io.vertx.ext.web.healthchecks.HealthCheckHandler;
|
||||||
|
import su.xserver.iikocon.iiko.IikoOlapClient;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
@@ -55,6 +56,20 @@ public class HealthCheckService {
|
|||||||
.onFailure(err -> future.tryFail("DataBase ping failed: " + err.getMessage()));
|
.onFailure(err -> future.tryFail("DataBase ping failed: " + err.getMessage()));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// healthCheckHandler.register("iiko", future -> {
|
||||||
|
//
|
||||||
|
// IikoOlapClient iiko = new IikoOlapClient(vertx, "folk-amber-co.iiko.it", "4444", "92f2fd99879b0c2466ab8648afb63c49032379c1", true);
|
||||||
|
//
|
||||||
|
// iiko.checkConnection()
|
||||||
|
// .onSuccess(res -> {
|
||||||
|
// JsonObject data = new JsonObject()
|
||||||
|
// .put("name", "iiko")
|
||||||
|
// .put("latency_ms", res.getLong("latency_ms"));
|
||||||
|
// future.complete(Status.OK(data));
|
||||||
|
// })
|
||||||
|
// .onFailure(err -> future.tryFail("iiko ping failed: " + err.getMessage()));
|
||||||
|
// });
|
||||||
|
|
||||||
// Регистрируем endpoint /api/health
|
// Регистрируем endpoint /api/health
|
||||||
router.get("/api/health").handler(healthCheckHandler);
|
router.get("/api/health").handler(healthCheckHandler);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ import java.time.format.DateTimeParseException;
|
|||||||
|
|
||||||
public class DateRangeSetup {
|
public class DateRangeSetup {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
// Параметры по умолчанию
|
|
||||||
String login = "4444";
|
|
||||||
String password = "4444";
|
|
||||||
String server = "folk-amber-co.iiko.it";
|
|
||||||
String presetId = "7ddc40c3-9d5f-408f-aa1e-652964b36c6c";
|
|
||||||
|
|
||||||
// Вычисление dateFrom и dateTo
|
// Вычисление dateFrom и dateTo
|
||||||
LocalDate today = LocalDate.now();
|
LocalDate today = LocalDate.now();
|
||||||
@@ -39,11 +34,6 @@ public class DateRangeSetup {
|
|||||||
String formattedDateFrom = dateFrom.format(formatter);
|
String formattedDateFrom = dateFrom.format(formatter);
|
||||||
String formattedDateTo = dateTo.format(formatter);
|
String formattedDateTo = dateTo.format(formatter);
|
||||||
|
|
||||||
// Вывод переменных (можно заменить на дальнейшее использование)
|
|
||||||
System.out.println("login=" + login);
|
|
||||||
System.out.println("password=" + password);
|
|
||||||
System.out.println("server=" + server);
|
|
||||||
System.out.println("presetId=" + presetId);
|
|
||||||
System.out.println("dateFrom=" + formattedDateFrom);
|
System.out.println("dateFrom=" + formattedDateFrom);
|
||||||
System.out.println("dateTo=" + formattedDateTo);
|
System.out.println("dateTo=" + formattedDateTo);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
package su.xserver.iikocon.test;
|
|
||||||
|
|
||||||
import io.vertx.core.Future;
|
|
||||||
import io.vertx.core.Promise;
|
|
||||||
import io.vertx.core.Vertx;
|
|
||||||
import io.vertx.core.buffer.Buffer;
|
|
||||||
import io.vertx.core.json.JsonObject;
|
|
||||||
import io.vertx.ext.web.client.HttpResponse;
|
|
||||||
import io.vertx.ext.web.client.WebClient;
|
|
||||||
|
|
||||||
public class IikoOlapClient {
|
|
||||||
|
|
||||||
private final WebClient webClient;
|
|
||||||
private final String baseUrl;
|
|
||||||
private final String login;
|
|
||||||
private final String password;
|
|
||||||
|
|
||||||
// Конструктор клиента
|
|
||||||
public IikoOlapClient(Vertx vertx, String baseUrl, String login, String password) {
|
|
||||||
this.webClient = WebClient.create(vertx);
|
|
||||||
this.baseUrl = baseUrl;
|
|
||||||
this.login = login;
|
|
||||||
this.password = password;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Основной метод для получения OLAP-отчета
|
|
||||||
public Future<JsonObject> getOlapReport(JsonObject reportRequest) {
|
|
||||||
Promise<JsonObject> promise = Promise.promise();
|
|
||||||
|
|
||||||
// 1. Аутентификация
|
|
||||||
authenticate()
|
|
||||||
.compose(this::getOrganizations) // 2. Получение организаций
|
|
||||||
.compose(orgId -> executeReport(reportRequest, orgId)) // 3. Запрос отчета
|
|
||||||
.onSuccess(promise::complete)
|
|
||||||
.onFailure(promise::fail);
|
|
||||||
|
|
||||||
return promise.future();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Аутентификация и получение токена
|
|
||||||
private Future<String> authenticate() {
|
|
||||||
Promise<String> promise = Promise.promise();
|
|
||||||
|
|
||||||
JsonObject authRequest = new JsonObject()
|
|
||||||
.put("login", login)
|
|
||||||
.put("password", password);
|
|
||||||
|
|
||||||
webClient.post(443, baseUrl, "/resto/api/auth")
|
|
||||||
.ssl(true)
|
|
||||||
.putHeader("Content-Type", "application/json")
|
|
||||||
.sendJson(authRequest)
|
|
||||||
.onSuccess(response -> {
|
|
||||||
if (response.statusCode() == 200) {
|
|
||||||
String token = response.bodyAsJsonObject().getString("token");
|
|
||||||
promise.complete(token);
|
|
||||||
} else {
|
|
||||||
promise.fail("Authentication failed: " + response.statusMessage());
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onFailure(promise::fail);
|
|
||||||
|
|
||||||
return promise.future();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получение ID организации (для отчета)
|
|
||||||
private Future<String> getOrganizations(String token) {
|
|
||||||
Promise<String> promise = Promise.promise();
|
|
||||||
|
|
||||||
webClient.get(443, baseUrl, "/resto/api/organizations")
|
|
||||||
.ssl(true)
|
|
||||||
.putHeader("Authorization", "Bearer " + token)
|
|
||||||
.send()
|
|
||||||
.onSuccess(response -> {
|
|
||||||
if (response.statusCode() == 200) {
|
|
||||||
// Берем ID первой организации из списка
|
|
||||||
String orgId = response.bodyAsJsonArray()
|
|
||||||
.getJsonObject(0)
|
|
||||||
.getString("id");
|
|
||||||
promise.complete(orgId);
|
|
||||||
} else {
|
|
||||||
promise.fail("Failed to get organizations: " + response.statusMessage());
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onFailure(promise::fail);
|
|
||||||
|
|
||||||
return promise.future();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Выполнение запроса OLAP-отчета
|
|
||||||
private Future<JsonObject> executeReport(JsonObject reportRequest, String organizationId) {
|
|
||||||
Promise<JsonObject> promise = Promise.promise();
|
|
||||||
|
|
||||||
// Добавляем ID организации в тело запроса
|
|
||||||
JsonObject fullRequest = reportRequest.copy()
|
|
||||||
.put("organizationId", organizationId);
|
|
||||||
|
|
||||||
webClient.post(443, baseUrl, "/resto/api/v2/reports/olap")
|
|
||||||
.ssl(true)
|
|
||||||
.putHeader("Content-Type", "application/json")
|
|
||||||
.sendJson(fullRequest)
|
|
||||||
.onSuccess(response -> {
|
|
||||||
if (response.statusCode() == 200) {
|
|
||||||
promise.complete(response.bodyAsJsonObject());
|
|
||||||
} else {
|
|
||||||
promise.fail("OLAP report request failed: " + response.statusMessage());
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onFailure(promise::fail);
|
|
||||||
|
|
||||||
return promise.future();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
package su.xserver.iikocon.test;
|
|
||||||
|
|
||||||
import io.vertx.core.AbstractVerticle;
|
|
||||||
import io.vertx.core.Promise;
|
|
||||||
import io.vertx.core.http.HttpMethod;
|
|
||||||
import io.vertx.core.json.Json;
|
|
||||||
import io.vertx.core.json.JsonObject;
|
|
||||||
import io.vertx.ext.web.Router;
|
|
||||||
import io.vertx.ext.web.RoutingContext;
|
|
||||||
import io.vertx.ext.web.client.WebClient;
|
|
||||||
import io.vertx.ext.web.client.WebClientOptions;
|
|
||||||
import io.vertx.ext.web.codec.BodyCodec;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.HexFormat;
|
|
||||||
|
|
||||||
public class ProxyVerticlev2 extends AbstractVerticle {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(ProxyVerticlev2.class);
|
|
||||||
private WebClient webClient;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start(Promise<Void> startPromise) {
|
|
||||||
webClient = WebClient.create(vertx, new WebClientOptions()
|
|
||||||
.setSsl(true)
|
|
||||||
.setTrustAll(true)
|
|
||||||
.setVerifyHost(false));
|
|
||||||
|
|
||||||
Router router = Router.router(vertx);
|
|
||||||
router.get("/").handler(this::handleGet);
|
|
||||||
|
|
||||||
int port = 80;
|
|
||||||
vertx.createHttpServer()
|
|
||||||
.requestHandler(router)
|
|
||||||
.listen(port).onComplete(http -> {
|
|
||||||
if (http.succeeded()) {
|
|
||||||
log.info("Proxy server started on port {}", port);
|
|
||||||
startPromise.complete();
|
|
||||||
} else {
|
|
||||||
startPromise.fail(http.cause());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleGet(RoutingContext ctx) {
|
|
||||||
String server = "folk-amber-co.iiko.it";
|
|
||||||
String password = "4444";
|
|
||||||
String login = "4444";
|
|
||||||
String reportType = "DELIVERIES";
|
|
||||||
|
|
||||||
String signature = sha1(password);
|
|
||||||
String authUrl = "https://" + server + ":443/resto/api/auth?login=" + login + "&pass=" + signature;
|
|
||||||
webClient.getAbs(authUrl)
|
|
||||||
.as(BodyCodec.string())
|
|
||||||
.send()
|
|
||||||
.onSuccess(authResp -> {
|
|
||||||
if (authResp.statusCode() != 200) {
|
|
||||||
fail(ctx, authResp.statusCode(), "Authentication failed: " + authResp.statusMessage());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String token = authResp.body();
|
|
||||||
String dataUrl = "https://" + server + "/resto/api/v2/reports/olap/columns" +
|
|
||||||
"?key=" + token + "&reportType=" + reportType;
|
|
||||||
log.info("URL: {}", dataUrl);
|
|
||||||
webClient.getAbs(dataUrl)
|
|
||||||
.as(BodyCodec.jsonObject())
|
|
||||||
.send()
|
|
||||||
.onSuccess(dataResp -> {
|
|
||||||
// logout (fire and forget)
|
|
||||||
webClient.getAbs("https://" + server + ":443/resto/api/logout?key=" + token)
|
|
||||||
.send()
|
|
||||||
.onFailure(err -> log.error("Logout failed: {}", err.getMessage()));
|
|
||||||
if (dataResp.statusCode() == 200) {
|
|
||||||
JsonObject responseBody = dataResp.body();
|
|
||||||
log.info(dataResp.headers().toString());
|
|
||||||
Object data = responseBody.getValue("data");
|
|
||||||
if (data == null) {
|
|
||||||
ctx.response().setStatusCode(200).end(responseBody.encode());
|
|
||||||
} else {
|
|
||||||
// data может быть массивом, объектом или другим типом
|
|
||||||
ctx.response().setStatusCode(200).end(Json.encode(data));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fail(ctx, dataResp.statusCode(), "External API error: " + dataResp.statusMessage());
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onFailure(err -> fail(ctx, 500, "Data request failed: " + err.getMessage()));
|
|
||||||
})
|
|
||||||
.onFailure(err -> fail(ctx, 500, "Auth request failed: " + err.getMessage()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String sha1(String input) {
|
|
||||||
try {
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
|
||||||
byte[] digest = md.digest(input.getBytes());
|
|
||||||
return HexFormat.of().formatHex(digest).toLowerCase();
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fail(RoutingContext ctx, int status, String message) {
|
|
||||||
log.info("Error: {}", message);
|
|
||||||
ctx.response().setStatusCode(status).end(new JsonObject().put("error", message).encode());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user