From 7d2ffb54b5274f62c36317a50859ef15c8ddb1cd Mon Sep 17 00:00:00 2001 From: SERTY Date: Sun, 25 Jan 2026 06:46:05 +0300 Subject: [PATCH] =?UTF-8?q?2512-=D0=B5=D1=81=D1=82=D1=8C=20Maintenance=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B6=D0=B8=D0=BC,=20=D0=BD=D0=BE=20=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=20=D0=B2=D1=81=D1=91=20=D1=80=D0=B0=D0=B2=D0=BD=D0=BE=20?= =?UTF-8?q?=D0=BE=D1=82=D0=B2=D0=B5=D1=87=D0=B0=D0=B5=D1=82=20=D1=84=D1=83?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/main.go | 6 +++- internal/services/ocr/service.go | 37 +++++++++++++++++++--- internal/transport/http/middleware/auth.go | 17 +++++++++- internal/transport/telegram/bot.go | 19 +++++++++++ rmser-view/src/App.tsx | 12 ++++++- rmser-view/src/pages/MaintenancePage.tsx | 28 ++++++++++++++++ rmser-view/src/services/api.ts | 10 +++++- 7 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 rmser-view/src/pages/MaintenancePage.tsx diff --git a/cmd/main.go b/cmd/main.go index 83f2c5e..7753ac0 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -96,6 +96,8 @@ func main() { syncService := sync.NewService(rmsFactory, accountRepo, catalogRepo, recipesRepo, invoicesRepo, opsRepo, supplierRepo) recService := recServicePkg.NewService(recRepo) ocrService := ocrServicePkg.NewService(ocrRepo, catalogRepo, draftsRepo, accountRepo, photosRepo, pyClient, cfg.App.StoragePath) + // Устанавливаем DevIDs для OCR сервиса + ocrService.SetDevIDs(cfg.App.DevIDs) draftsService := draftsServicePkg.NewService(draftsRepo, ocrRepo, catalogRepo, accountRepo, supplierRepo, photosRepo, invoicesRepo, rmsFactory, billingService) invoicesService := invoicesServicePkg.NewService(invoicesRepo, draftsRepo, supplierRepo, rmsFactory) photosService := photosServicePkg.NewService(photosRepo, draftsRepo, accountRepo) @@ -117,6 +119,8 @@ func main() { } billingService.SetNotifier(bot) settingsHandler.SetNotifier(bot) + // Устанавливаем нотификатор для OCR сервиса + ocrService.SetNotifier(bot) go bot.Start() defer bot.Stop() } @@ -141,7 +145,7 @@ func main() { api := r.Group("/api") - api.Use(middleware.AuthMiddleware(accountRepo, cfg.Telegram.Token)) + api.Use(middleware.AuthMiddleware(accountRepo, cfg.Telegram.Token, cfg.App.MaintenanceMode, cfg.App.DevIDs)) { // Drafts & Invoices api.GET("/drafts", draftsHandler.GetDrafts) diff --git a/internal/services/ocr/service.go b/internal/services/ocr/service.go index 3c4e23c..6d350f3 100644 --- a/internal/services/ocr/service.go +++ b/internal/services/ocr/service.go @@ -19,6 +19,11 @@ import ( "rmser/internal/infrastructure/ocr_client" ) +// DevNotifier - интерфейс для уведомления разработчиков +type DevNotifier interface { + NotifyDevs(devIDs []int64, photoPath string, serverName string, serverID string) +} + type Service struct { ocrRepo ocr.Repository catalogRepo catalog.Repository @@ -27,6 +32,8 @@ type Service struct { photoRepo photos.Repository pyClient *ocr_client.Client storagePath string + notifier DevNotifier + devIDs []int64 } func NewService( @@ -49,6 +56,16 @@ func NewService( } } +// SetNotifier - устанавливает notifier для уведомлений разработчиков +func (s *Service) SetNotifier(n DevNotifier) { + s.notifier = n +} + +// SetDevIDs - устанавливает список ID разработчиков для уведомлений +func (s *Service) SetDevIDs(ids []int64) { + s.devIDs = ids +} + // checkWriteAccess - вспомогательный метод проверки прав func (s *Service) checkWriteAccess(userID, serverID uuid.UUID) error { role, err := s.accountRepo.GetUserRole(userID, serverID) @@ -74,15 +91,20 @@ func (s *Service) ProcessReceiptImage(ctx context.Context, userID uuid.UUID, img draftID := uuid.New() fileName := fmt.Sprintf("receipt_%s.jpg", photoID.String()) - filePath := filepath.Join(s.storagePath, fileName) + filePath := filepath.Join(s.storagePath, serverID.String(), fileName) - // 2. Сохраняем файл + // 2. Создаем директорию если не существует + if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { + return nil, fmt.Errorf("failed to create directory: %w", err) + } + + // 3. Сохраняем файл if err := os.WriteFile(filePath, imgData, 0644); err != nil { return nil, fmt.Errorf("failed to save image: %w", err) } fileURL := "/uploads/" + fileName - // 3. Создаем запись ReceiptPhoto + // 4. Создаем запись ReceiptPhoto photo := &photos.ReceiptPhoto{ ID: photoID, RMSServerID: serverID, @@ -98,7 +120,7 @@ func (s *Service) ProcessReceiptImage(ctx context.Context, userID uuid.UUID, img return nil, fmt.Errorf("failed to create photo record: %w", err) } - // 4. Создаем черновик + // 5. Создаем черновик draft := &drafts.DraftInvoice{ ID: draftID, UserID: userID, @@ -112,7 +134,12 @@ func (s *Service) ProcessReceiptImage(ctx context.Context, userID uuid.UUID, img return nil, fmt.Errorf("failed to create draft: %w", err) } - // 5. Отправляем в Python OCR + // Уведомляем разработчиков если devIDs заданы + if len(s.devIDs) > 0 && s.notifier != nil { + s.notifier.NotifyDevs(s.devIDs, filePath, server.Name, serverID.String()) + } + + // 6. Отправляем в Python OCR rawResult, err := s.pyClient.ProcessImage(ctx, imgData, "receipt.jpg") if err != nil { draft.Status = drafts.StatusError diff --git a/internal/transport/http/middleware/auth.go b/internal/transport/http/middleware/auth.go index 4880393..86cdfcb 100644 --- a/internal/transport/http/middleware/auth.go +++ b/internal/transport/http/middleware/auth.go @@ -17,7 +17,7 @@ import ( ) // AuthMiddleware проверяет initData от Telegram -func AuthMiddleware(accountRepo account.Repository, botToken string) gin.HandlerFunc { +func AuthMiddleware(accountRepo account.Repository, botToken string, maintenanceMode bool, devIDs []int64) gin.HandlerFunc { return func(c *gin.Context) { // 1. Извлекаем данные авторизации authHeader := c.GetHeader("Authorization") @@ -55,6 +55,21 @@ func AuthMiddleware(accountRepo account.Repository, botToken string) gin.Handler return } + // Проверка режима обслуживания: если включен, разрешаем доступ только разработчикам + if maintenanceMode { + isDev := false + for _, devID := range devIDs { + if tgID == devID { + isDev = true + break + } + } + if !isDev { + c.AbortWithStatusJSON(503, gin.H{"error": "maintenance_mode", "message": "Сервис на обслуживании"}) + return + } + } + // 4. Ищем пользователя в БД user, err := accountRepo.GetUserByTelegramID(tgID) if err != nil { diff --git a/internal/transport/telegram/bot.go b/internal/transport/telegram/bot.go index 7145908..4ff5f1e 100644 --- a/internal/transport/telegram/bot.go +++ b/internal/transport/telegram/bot.go @@ -928,6 +928,25 @@ func (bot *Bot) renderDeleteServerMenu(c tele.Context) error { return c.EditOrSend("⚙️ Управление серверами\n\nЗдесь вы можете удалить сервер или пригласить сотрудников.", menu, tele.ModeHTML) } +// NotifyDevs отправляет фото разработчикам для отладки +func (bot *Bot) NotifyDevs(devIDs []int64, photoPath string, serverName string, serverID string) { + // Формируем подпись для фото + caption := fmt.Sprintf("🛠 **Debug Capture**\nServer: %s (`%s`)\nFile: %s", serverName, serverID, photoPath) + + // В цикле отправляем фото каждому разработчику + for _, id := range devIDs { + photo := &tele.Photo{ + File: tele.FromDisk(photoPath), + Caption: caption, + } + // Отправляем фото пользователю + _, err := bot.b.Send(&tele.User{ID: id}, photo) + if err != nil { + logger.Log.Error("Failed to send debug photo", zap.Int64("userID", id), zap.Error(err)) + } + } +} + func parseUUID(s string) uuid.UUID { id, _ := uuid.Parse(s) return id diff --git a/rmser-view/src/App.tsx b/rmser-view/src/App.tsx index 0264732..e780c29 100644 --- a/rmser-view/src/App.tsx +++ b/rmser-view/src/App.tsx @@ -8,7 +8,8 @@ import { InvoiceDraftPage } from "./pages/InvoiceDraftPage"; import { InvoiceViewPage } from "./pages/InvoiceViewPage"; import { DraftsList } from "./pages/DraftsList"; import { SettingsPage } from "./pages/SettingsPage"; -import { UNAUTHORIZED_EVENT } from "./services/api"; +import { UNAUTHORIZED_EVENT, MAINTENANCE_EVENT } from "./services/api"; +import MaintenancePage from "./pages/MaintenancePage"; // Компонент-заглушка для внешних браузеров const NotInTelegramScreen = () => ( @@ -37,6 +38,7 @@ const NotInTelegramScreen = () => ( function App() { const [isUnauthorized, setIsUnauthorized] = useState(false); + const [isMaintenance, setIsMaintenance] = useState(false); const tg = window.Telegram?.WebApp; // Проверяем, есть ли данные от Telegram @@ -44,7 +46,9 @@ function App() { useEffect(() => { const handleUnauthorized = () => setIsUnauthorized(true); + const handleMaintenance = () => setIsMaintenance(true); window.addEventListener(UNAUTHORIZED_EVENT, handleUnauthorized); + window.addEventListener(MAINTENANCE_EVENT, handleMaintenance); if (tg) { tg.expand(); // Расширяем приложение на все окно @@ -52,6 +56,7 @@ function App() { return () => { window.removeEventListener(UNAUTHORIZED_EVENT, handleUnauthorized); + window.removeEventListener(MAINTENANCE_EVENT, handleMaintenance); }; }, [tg]); @@ -80,6 +85,11 @@ function App() { ); } + // Если бэкенд вернул 503 (режим технического обслуживания) + if (isMaintenance) { + return ; + } + return ( diff --git a/rmser-view/src/pages/MaintenancePage.tsx b/rmser-view/src/pages/MaintenancePage.tsx new file mode 100644 index 0000000..49b7bb5 --- /dev/null +++ b/rmser-view/src/pages/MaintenancePage.tsx @@ -0,0 +1,28 @@ +import { Result, Button } from "antd"; + +// Страница-заглушка для режима технического обслуживания +const MaintenancePage = () => ( +
+ window.location.reload()}> + Попробовать снова + + } + /> +
+); + +export default MaintenancePage; diff --git a/rmser-view/src/services/api.ts b/rmser-view/src/services/api.ts index f78a390..92bb9ec 100644 --- a/rmser-view/src/services/api.ts +++ b/rmser-view/src/services/api.ts @@ -46,6 +46,9 @@ const tg = window.Telegram?.WebApp; // Событие для глобальной обработки 401 export const UNAUTHORIZED_EVENT = 'rms_unauthorized'; +// Событие для режима технического обслуживания (503) +export const MAINTENANCE_EVENT = 'rms_maintenance'; + const apiClient = axios.create({ baseURL: API_BASE_URL, headers: { @@ -80,9 +83,14 @@ apiClient.interceptors.response.use( description: 'Ваша сессия в Telegram истекла или данные неверны. Попробуйте перезапустить бота.', placement: 'top', }); - + window.dispatchEvent(new Event(UNAUTHORIZED_EVENT)); } + + if (error.response && error.response.status === 503) { + // Режим технического обслуживания + window.dispatchEvent(new Event(MAINTENANCE_EVENT)); + } // Если запрос был отменен нами (нет initData), не выводим стандартную ошибку API if (error.message === 'MISSING_TELEGRAM_DATA') {