Język

Napisz do mnie

SaaS · od 2025

Botoscope®

SaaS, który zamienia sklep WooCommerce w bota Telegram z katalogiem, koszykiem, kasą i zamówieniami zsynchronizowanymi ze sklepem.

Uruchomiony
2025
Status
Zamknięty w 2026
Platforma
Telegram
Sklep
WooCommerce
  • Node.js (moduły ES)
  • TDLib przez tdl / tdl-tdlib-addon
  • MySQL / MariaDB (knex, mysql2)
  • Redis (ioredis)
  • PM2
  • axios
  • moment
  • Telegram Web Apps
  • WooCommerce przez proxy Botoscope
Otwórz botoscope.com botoscope.com

O mnie

Botoscope prowadzi sklep WooCommerce jako bota Telegram. Prace rozpoczęły się w 2024 roku; usługa ruszyła publicznie w 2025. Klienci przeglądają kategorie, wybierają warianty, wypełniają koszyk, składają zamówienie i płacą w czacie, a zamówienie trafia do WooCommerce. Opisany tu silnik to proces Node.js obsługujący jednego bota. Rozmawia z Telegramem przez TDLib w trybie bota, ze sklepem przez proxy Botoscope, trzyma kopię katalogu w MySQL, a stan każdego czatu w Redis.

Większość trudności wynika z samego czatu. ID wiadomości zmieniają się po wysłaniu, klawiatur odpowiedzi nie da się edytować, każde naciśnięcie przycisku to krótki ładunek callback, wpisany tekst może być albo danymi, albo etykietą menu, a stare wiadomości się piętrzą. Duża część silnika istnieje po to, by strumień wiadomości zachowywał się jak ekrany aplikacji.

Reszta to spójność ze sklepem na innym serwerze: import katalogu, który nie uruchamia się przy starcie, aktualizacje pojedynczych produktów wypychane z WordPressa, stany i ceny zgodne ze sklepem, jedno zamówienie WooCommerce na jedną kasę, które podąża za zmianami klienta, oraz płatność przez procesory kart, bramki kryptowalutowe i Telegram Stars zapisana w tym zamówieniu.

Botoscope · sklep w Telegramie

co rozwiązano

ZAMKNIĘTYod 2025

Silnik i model procesu

w produkcie

Co to dajeKażdy sklep dostał własnego bota, oddzielonego od innych sklepów, który działał tygodniami na małym serwerze i przejmował zmiany wprowadzane przez właściciela w sklepie.

Problem
Każdy sklep potrzebuje własnego bota z własną sesją Telegram, katalogiem i ustawieniami, z jednej bazy kodu. Proces musi działać tygodniami na małych serwerach i przyjmować polecenia od strony sklepu.
Rozwiązanie

Jedna instancja silnika na bota: klient TDLib, kopia katalogu w MySQL i stan w Redis, konfigurowane przez zmienne środowiskowe i uruchamiane pod PM2 z limitami pamięci. Moduły rozmawiają przez szynę zdarzeń i rejestry hooków; funkcje płatne ładują się jako rozszerzenia; strona sklepu dociera do bota przez mały punkt końcowy HTTP z poleceniami.

Technologie
  • Node.js
  • TDLib (tdl)
  • EventEmitter
  • PM2
  • knex
  • ioredis
Zadania · 6
Izolować sklepy bez ścieżki kodu wielodzierżawnejJeden proces na bota; token, domena sklepu, klucze i limity planu pochodzą ze środowiska

W silniku nigdzie nie ma identyfikatora dzierżawcy (tenant ID). init() loguje jednego klienta bota TDLib, otwiera jedną pulę knex i jedno połączenie Redis oraz odczytuje domenę sklepu, klucz API klienta, limit produktów i flagę planu darmowego ze zmiennych środowiskowych. Izolacja między sklepami to kwestia wdrożenia: każdy bot dostaje własną instancję z własnym katalogiem bazy TDLib i własnym schematem MySQL.

Dzięki temu zapytania i klucze Redis są proste (chat:<id>:<key>), kosztem jednego procesu na sklep. Klucze Redis nie mają prefiksu bota, więc dwie instancje nie mogą dzielić jednej bazy Redis.

Uruchomić bota na TDLib zamiast na HTTP Bot APIKlient TDLib w trybie bota: jedno trwałe połączenie, jeden strumień aktualizacji w obu kierunkach, lokalne cache

Bot loguje się przez TDLib (tdl z tdl-tdlib-addon) tokenem bota i utrzymuje jedno długotrwałe połączenie z Telegramem. Aktualizacje są wypychane do client.on('update'), więc silnik nie potrzebuje adresu URL webhooka osiągalnego z Telegramu ani pętli odpytywania getUpdates.

Głównym powodem jest model ekranów. TDLib zgłasza własne wychodzące wiadomości bota jako updateNewMessage i updateMessageSendSucceeded w tym samym strumieniu co wiadomości klienta. Jeden handler rejestruje każde ID wiadomości na czacie prywatnym, wysłanej lub odebranej, a gdy ekran się zmienia, silnik usuwa je przez deleteMessages (revoke: true). Czat zachowuje tylko bieżący ekran, a wpisane dane, takie jak zapytanie wyszukiwania czy adres, znikają razem z nim. Ten sam strumień pozwala silnikowi poczekać, aż album zdjęć faktycznie dotrze, zanim wyśle panel produktu.

TDLib obejmuje też wywołania, których HTTP Bot API nie ma: getMessage odczytuje tekst wiadomości wysłanej wcześniej przez bota, czego używa panel produktów grupowanych do przerysowania przycisków. getChat i getUser obsługuje klient TDLib z włączoną bazą informacji o czatach, a media wychodzą ze ścieżek lokalnych lub po zdalnym ID pliku przez bazę plików. Baza wiadomości jest wyłączona, ponieważ bot nie przechowuje historii, optymalizator miejsca przycina pliki TDLib, a TDLib działa na jednym wątku roboczym.

Koszt jest taki, że semantyka TDLib przecieka do aplikacji: ID wiadomości są tymczasowe, dopóki serwer ich nie potwierdzi (zob. zadanie mapowania ID), a katalog sesji wiąże bota z jednym procesem.

Możliwe kolejne kroki, które TDLib już oferuje: włączenie bazy wiadomości dla historii i wyszukiwania na czacie oraz wysyłanie plików większych niż limit wysyłania 50 MB w HTTP Bot API. Silnik dziś nie robi żadnego z nich; ogranicza dokumenty do 50 MB.

Wiele modułów reaguje na te same aktualizacje i ekranySzyna zdarzeń dla aktualizacji oraz rejestry hooków dla czatu i globalne, uporządkowane według priorytetu

Answer zamienia aktualizacje TDLib na zdarzenia szyny: do_callback_query dla naciśnięć przycisków, do_command dla tekstu, pre_checkout_query i successful_payment dla płatności. Koszyk, ulubione, rezerwacje, wsparcie i reszta subskrybują i obsługują tylko własne akcje, więc nie importują nawzajem swoich handlerów.

Do składania interfejsu są dwa rejestry hooków. Hooks trzyma callbacki dla chat_id (bieżące menu kontekstowe, dodatkowe przyciski produktu) posortowane według priorytetu, przy czym dodanie hooka z istniejącym priorytetem zastępuje go. GlobalHooks robi to samo dla wszystkich czatów; rozszerzenie wsparcia używa go, by dodać przycisk do paneli produktów, których nie jest właścicielem. Hooki czatu są usuwane, gdy czat zostaje usunięty.

Włączać i wyłączać funkcje planu dla każdego sklepuLista rozszerzeń synchronizowana ze sklepu; klasa budowana ze sluga; restart PM2, aby wyładować

Lista aktywnych rozszerzeń (kupony, wysyłka, wsparcie, rezerwacje, marketing, b2b i inne) jest synchronizowana do tabeli opcji. Extensions.init() zamienia każdy slug na nazwę klasy i tworzy instancję przed zbudowaniem modułów rdzenia, aby ich hooki istniały wcześniej. Moduły rdzenia sprawdzają rozszerzenie przez opcjonalne gettery i zmieniają zachowanie; na przykład kasa pomija krok kodu promocyjnego, gdy kupony są wyłączone.

Wyłączenie rozszerzenia nie może wyładować kodu, który już zarejestrował nasłuchiwacze, więc zresetowanie cache rozszerzeń restartuje proces przez PM2. Komentarze w kodzie zaznaczają, że przerywa to użytkowników w trakcie sesji.

Pozwolić stronie sklepu wypychać zmiany i zapytania do działającego botaPunkt końcowy HTTP /command; znane polecenia obsługiwane bezpośrednio, reszta kierowana do modułów przez szynę

Commander uruchamia zwykły serwer http z /health i /command. Znane polecenia (payment_success, order_is_done, reset_cache, update_product_cache) są obsługiwane bezpośrednio. Wszystko inne jest emitowane jako exec_command; moduł, który je posiada, zapisuje is_executed i answer we wspólnym obiekcie body, który punkt końcowy zwraca po krótkim stałym oczekiwaniu.

reset_cache odświeża jedno cache ustawień albo wszystkie, z rozszerzeniami zawsze na końcu, bo to jedno restartuje proces. reset_db_cache() każdego modułu pobiera ponownie swój blob JSON do tabeli opcji i czyści swoją kopię w pamięci.

Utrzymać w zdrowiu jeden długo działający proces na małym serwerzePM2 w trybie fork, sterta 450 MB, restart przy 1 GB, 2 do 5 połączeń z bazą, resetowanie cache według czasu

ecosystem.config.cjs uruchamia pojedynczą instancję fork (tryb cluster jest zakomentowany; jeden token bota oznacza jedną sesję TDLib), ogranicza starą przestrzeń V8, restartuje przy wzroście pamięci i wymaga 30 s działania, zanim start zostanie uznany za stabilny. Handlery SIGTERM, SIGINT i SIGQUIT niszczą pulę knex przed wyjściem, a timer loguje użycie puli, gdy jest aktywność.

Cache w pamięci, takie jak koszyki i wybrane waluty, są czyszczone według timerów, a mapa ID wiadomości jest opróżniana przy każdej zmianie ekranu.

Czat jako interfejs aplikacji

w produkcie

Co to dajeKlienci poruszali się po katalogu, koszyku i kasie jak po ekranach aplikacji, a czat pozostawał uporządkowany zamiast zapełniać się starymi wiadomościami.

Problem
Czat Telegram to strumień wiadomości, ale produkt potrzebuje ekranów: strony katalogu, koszyka, kroku kasy, z których każdy zastępuje poprzedni, z przyciskami aktualizowanymi w miejscu.
Rozwiązanie

Śledzić każdą wiadomość na czacie, podmieniać ekrany przez usuwanie poprzedniego zestawu po narysowaniu nowego, mapować tymczasowe ID z TDLib na ID serwera dla późniejszych edycji i trzymać stan nawigacji w kontekście czatu w Redis.

Technologie
  • TDLib
  • Redis
  • Klawiatury odpowiedzi i inline Telegrama
Zadania · 6
Edytować wiadomość, której ID zmienia się po wysłaniuMapować tymczasowe ID na ID serwera z updateMessageSendSucceeded, dla każdego czatu w pamięci RAM

sendMessage w TDLib zwraca tymczasowe lokalne ID; prawdziwe ID przychodzi później w updateMessageSendSucceeded razem z old_message_id. Silnik zapisuje ID zwrócone przez send() w kontekście czatu (top_message_id, taxonomies_btn_mgs_id i podobne) i rozwiązuje je przez MessageMapping, gdy musi edytować lub usunąć tę wiadomość.

Mapowanie najpierw żyło w Redis, a potem zostało przeniesione do mapy w pamięci dla każdego czatu, czyszczonej przy każdej zmianie ekranu, aby pozostawała mała. Bez niego edycja klawiatury inline po kliknięciu celowałaby w ID, który już nie istnieje.

Sprawić, by grupa wiadomości zachowywała się jak jeden ekranLogować wysłane ID w Redis; przy nowym ekranie odłożyć je na bok i usunąć po narysowaniu

Każde nowe ID wiadomości na czacie prywatnym jest wpychane do chat:<id>:messages. make_messages_castling() przenosi tę listę do chat:<id>:old_messages i resetuje mapowanie ID, moduł rysuje nowy ekran, a clear_display() usuwa stare wiadomości. Nowa treść pojawia się, zanim stara zniknie, więc czat nigdy nie jest pusty między ekranami.

Pełne czyszczenie czatu zostało wypróbowane i pozostaje w kodzie tylko jako ostrzeżenie: na niektórych telefonach wyrzucało użytkownika z bota.

Wiadomości z klawiaturą odpowiedzi nie da się edytowaćWysłać nową górną wiadomość z zaktualizowaną klawiaturą, potem usunąć starą

Trwałe dolne menu pokazuje przycisk koszyka z bieżącą liczbą pozycji. Telegram nie pozwala edytować tekstu wiadomości z klawiaturą odpowiedzi, więc Cart.redraw_menu() wysyła świeżą górną wiadomość i usuwa poprzednią po jej zmapowanym ID, a dolne podsumowanie, wiadomość inline, jest edytowane w miejscu po krótkim opóźnieniu.

Bieżące menu jest trzymane jako hook context_menu dla czatu. change_menu_buttons_text() przepisuje tylko przycisk zaczynający się od emoji koszyka w zserializowanym menu, więc każdy ekran może odświeżyć licznik koszyka, nie znając własnego układu.

Teksty sklepu to HTML; TDLib chce zwykłego tekstu z przesunięciami encjiWłasny konwerter: dekodowanie encji, usunięcie podzbioru tagów, emisja przesunięć textEntity

format_string_before_send() obsługuje strong, em, u, s, code, pre i a href. Znajduje najbliższy tag otwierający, usuwa go razem z zamykającym i zapisuje pozycje początku i końca w oczyszczonym ciągu; stają się one przesunięciami textEntity w formattedText. Nazwane i numeryczne encje HTML z WooCommerce są dekodowane najpierw. Źle zbudowany lub niezamknięty tag sprawia, że funkcja zwraca oryginalny tekst bez encji, zamiast wysyłać zepsute formatowanie.

Obok są dwa triki układu: ciąg pustych znaków Braille'a dopełnia wiadomości, aby karty produktów miały tę samą szerokość, a łączący znak nakładki rysuje przekreśloną starą cenę w etykietach przycisków, gdzie encje nie są dozwolone.

Wpisany tekst może być danymi użytkownika albo zlokalizowanym przyciskiem menuFlaga oczekiwanego wejścia dla czatu; znane etykiety przycisków zawsze z niego wyrywają

Przyciski klawiatury odpowiedzi przychodzą jako zwykły tekst w języku użytkownika, więc routing porównuje przychodzący tekst z etykietami zbudowanymi przez get_menu_command_text() dla tego czatu. Gdy krok czeka na tekst swobodny (zapytanie wyszukiwania, SKU, adres, kod promocyjny, wiadomość do wsparcia), kontekst zawiera is_waiting_user_input i polecenie do wznowienia. Answer zapisuje tekst jako user_message_text i odtwarza to polecenie, chyba że tekst jest poleceniem ze slashem albo etykietą nawigacji.

Handlery wejścia w kasie powtarzają sprawdzenie względem etykiet własnego kroku, więc naciśnięcie Wstecz, gdy oczekiwany jest adres, cofa nawigację zamiast zapisać to słowo jako adres.

Karty produktów muszą przychodzić po kolei i wytrzymać szybkie przeglądanie stronCzekać na własną aktualizację albumu przed jego panelem; dopuszczać jedną akcję przeglądania naraz

Karta produktu to album multimedialny, po którym następuje panel tekstowy z przyciskami. print_products_blocks() wysyła album, czeka do 10 s na zdarzenie new_message_coming tego czatu, potem robi pauzę przed wysłaniem panelu, więc panel nigdy nie ląduje nad swoimi zdjęciami. Krótkie stałe pauzy przed każdym wysłaniem i usunięciem rozkładają żądania w czasie.

Szybkie stuknięcia w następną i poprzednią strony uruchomiłyby drugą podmianę ekranu, zanim pierwsza usunęłaby swoje wiadomości. Flaga nowdo dla czatu ignoruje nawigację w trakcie podmiany ekranu i jest zwalniana sekundę po usunięciu starych wiadomości.

Synchronizacja katalogu WooCommerce

w produkcie

Co to dajeKlienci szybko przeglądali katalog i widzieli aktualne ceny oraz stany. Zmiany w sklepie docierały do bota same, a subskrybenci dowiadywali się, gdy towar wracał.

Problem
Sklep działa na witrynie WordPress na innym serwerze. Bot musi szybko przeglądać katalog stronami, pokazywać aktualne ceny i stany oraz przejmować zmiany bez odpytywania sklepu przy każdym stuknięciu.
Rozwiązanie

Trzymać kopię produktów i powiązań z kategoriami w MySQL, wypełnianą stronicowanym pełnym importem i aktualizowaną dla pojedynczego produktu, gdy sklep wypchnie zmianę. Ustawienia sklepu przychodzą jako bloby JSON w tabeli opcji z cache w pamięci.

Technologie
  • MySQL (MyISAM)
  • knex
  • WooCommerce REST przez proxy
  • Telegram Web Apps
Zadania · 6
Dotrzeć do danych każdego sklepu bez przechowywania jego danych dostępowych RESTWszystkie wywołania sklepu idą do jednego proxy; domena, punkt końcowy, klucz API i HMAC przechodzą w nagłówkach

get_rest_data() i send_remote_data() wysyłają POST na jeden adres proxy Botoscope. Sklep jest wskazany w nagłówku client-domain, punkt końcowy w innym nagłówku, a żądanie jest uwierzytelniane kluczem API klienta i hasłem wyprowadzonym przez HMAC-SHA256. Pobieranie plików używa tego samego proxy przez jego punkt końcowy fetch-file.

Silnik nigdy nie potrzebuje własnych kluczy REST sklepu, a strona WordPress musi tylko odpowiadać proxy. Błędy zwracają null, a wywołujący traktują null jako brak danych, więc awaria sklepu przechodzi w puste listy zamiast zawieszać ekran.

Importować pełny katalog na żądanie, a nie przy każdym restarcieTruncate, wyłączenie kluczy, pobieranie stron po 100, wstawianie partiami, ponowne włączenie kluczy w finally

Products.reset_db_cache() odczytuje liczbę produktów, czyści products i products_vs_terms, uruchamia ALTER TABLE ... DISABLE KEYS na obu tabelach MyISAM i pobiera products/{offset}/{limit} stronami po 100. Każda strona jest wstawiana partiami po 100 wierszy; błąd na jednej stronie jest logowany i import przechodzi do następnej. MyISAM nie ma transakcji, więc wiersze już zapisane z nieudanej strony zostają. Klucze są włączane ponownie w bloku finally, więc nieudany import wciąż zostawia tabele z indeksami. W trakcie importu bot pokazuje dotąd załadowaną część katalogu.

Import nie uruchamia się przy starcie. Komentarz wyjaśnia, że przy dużym katalogu mógłby zawiesić serwer po każdym restarcie, więc działa tylko na jawne polecenie reset_cache od sklepu.

Ładunki produktów różnią się według typu i stale rosnąKolumny dla tego, co filtrowane lub sortowane, wszystko inne w kolumnie JSON data

insert_product_batch() destrukturyzuje pola, o które pyta silnik (price, sale_price, flagi stanu, type, parent_id, publish_date, menu_order, minimum, maksimum i krok koszyka), do kolumn, a resztę ładunku serializuje do data. Listy mediów, pliki do pobrania, dni dostępu czy zewnętrzny adres produktu można dodać po stronie sklepu bez zmiany schematu. ID kategorii trafiają zarówno do listy rozdzielonej przecinkami w wierszu, jak i do products_vs_terms dla indeksowanych wyszukiwań po kategorii.

install.sql uruchamia się przy każdym starcie z CREATE TABLE IF NOT EXISTS, więc nowa instancja sama tworzy swój schemat.

Stosować edycje pojedynczych produktów bez pełnego ponownego importuSklep wypycha update_product_cache; produkt, warianty i terminy przepisywane z jednej odpowiedzi sklepu

Gdy produkt zmienia się w WooCommerce, strona sklepu wysyła update_product_cache na /command. update_product_cache() pobiera products_filter/{id}, co zwraca produkt z jego wariantami, następnie usuwa wiersz produktu, wiersze z tym parent_id i jego powiązania z kategoriami, po czym wstawia nowe wiersze. Jedno żądanie niesie produkt i jego warianty, więc wiersze są przepisywane raz na zmianę.

Limit planu liczy tylko produkty nadrzędne. Pełny import przycina produkty główne na stronę i zawsze zachowuje ich warianty; ścieżka przyrostowa pomija nowy produkt, gdy liczba różnych produktów w products_vs_terms osiągnie limit.

Flagi stanu rozjeżdżają się, a klienci chcą alertów o powrocie towaruUzgadniać flagi dostępności z ilości co 3 min; powiadamiać subskrybentów co 2 h

Dla produktów z zarządzanym stanem timer co trzy minuty ustawia is_in_stock na podstawie stock_quantity w obu kierunkach, więc przycisk zakupu podąża za faktyczną ilością, nawet gdy wypchnięta flaga była nieaktualna. Klienci mogą subskrybować niedostępny produkt lub pojedynczy wariant; stock_notify przechowuje product_id i chat_id. Co dwie godziny silnik znajduje subskrybowane produkty, które znów są dostępne, wysyła każdemu subskrybentowi kartę ponownie i usuwa subskrypcję.

Każde zapytanie do katalogu porównuje też publish_date z bieżącym czasem, więc zaplanowane produkty pozostają ukryte do swojej daty.

Filtrowanie fasetowe nie mieści się w przyciskach czatuOtworzyć stronę filtrów sklepu jako Telegram Web App; odsyła dopasowane ID

Polecenie /filter i przyciski Filter otwierają filter_page_url jako Web App z dołączonym chat_id. Filtrowanie odbywa się po stronie WordPressa. Wynik wraca do silnika jako polecenie filtered_products_ids na /command, a Products wypisuje te ID jako zwykły stronicowany katalog na tym czacie.

Dzięki temu liczenie faset i logika atrybutów pozostają w jednym miejscu, w sklepie, a czat nadal pokazuje wyniki we własnym formacie kart.

Koszyk, warianty i kasa

w produkcie

Co to dajeBot podążał za własnymi regułami sklepu dla ilości, wariantów, rezerwacji i rabatów, a zamówienie w sklepie odpowiadało temu, co klient widział na czacie.

Problem
Koszyk i kasa muszą egzekwować reguły sklepu (kroki ilości, minima, stany, warianty, terminy rezerwacji, rabaty) wyłącznie przyciskami i krótkimi odpowiedziami tekstowymi, a zamówienie WooCommerce musi odpowiadać temu, co widział klient.
Rozwiązanie

Koszyki żyją w Redis za cache w RAM, warianty wybiera się kaskadowym selektorem inline, a kasa to maszyna kroków przycinana zawartością koszyka i włączonymi rozszerzeniami. Zamówienie jest tworzone raz i poprawiane, gdy klient edytuje.

Technologie
  • Redis
  • Node.js
  • moment
  • Zamówienia WooCommerce przez proxy
Zadania · 6
Koszyk jest odczytywany przy niemal każdym renderowaniuRedis na czat za cache RAM write-through; wygasłe i usunięte pozycje przycinane

Cart.get() obsługuje z statycznej mapy dla czatu i wraca do Redis (chat:<id>:cart); update() zapisuje do obu. Etykieta przycisku koszyka, sumy i każdy panel produktu czytają koszyk, więc cache oszczędza za każdym razem jedno przejście do Redis. Pozycje mogą mieć termin ważności, używany przez blokady rezerwacji, i są usuwane przy odczycie po wygaśnięciu. Rysowanie koszyka najpierw usuwa ID, które nie istnieją już w lokalnym katalogu, więc produkt usunięty w WooCommerce nie może dotrzeć do kasy.

Przyciski ilości respektują quantity_step, min_cart_count i zarządzany stan, a wariant bez własnych wartości dziedziczy je z produktu nadrzędnego.

Wybrać wariant przyciskami, gdy nie istnieje każda kombinacjaKaskadowe wiersze atrybutów; każdy wiersz oferuje tylko terminy, które nadal tworzą prawdziwy wariant

get_buy_buttons_panel() odczytuje kombinację atrybutów każdego wariantu i porządkuje atrybuty według kolejności atrybutów sklepu. Pierwszy wiersz wymienia każdy używany termin; każdy kolejny wiersz wymienia tylko terminy, które razem z wyborami powyżej pasują do co najmniej jednego istniejącego wariantu. Atrybut z trzema lub więcej opcjami może być wyrenderowany jako zwarty wiersz poprzedni / bieżący / następny zamiast siatki.

Zmiana górnego atrybutu czyści dolne i przeskakuje do pierwszej pasującej kombinacji. Produkt otwiera się na swoim najtańszym wariancie. Wybór jest zapisywany dla czatu i produktu w kontekście, a panel jest edytowany w miejscu przy każdym stuknięciu.

Kroki kasy zależą od tego, co jest w koszykuLista kroków przycinana dla czatu: bez wysyłki dla koszyków cyfrowych, bez kroku płatności, gdy za darmo

Kroki bazowe to kod promocyjny, adres wysyłki, metoda wysyłki, metoda płatności, komentarz i potwierdzenie. Kroki wyłączonych rozszerzeń są usuwane przy starcie. get_checkout_steps() następnie sprawdza koszyk, przechodząc po produktach grupowanych i wariantach ich dzieci z wariantami, by znaleźć jakąkolwiek pozycję fizyczną; koszyk bez niej pomija oba kroki wysyłki. Suma zero przełącza na bramkę prezentu i pomija wybór płatności, sklep z jedną bramką wybiera ją z góry, a płatność w Stars wymusza bramkę Stars.

Kroki wymagane blokują next(), dopóki wartość nie zostanie zapisana, a każdy krok można ponownie otworzyć w trybie edycji z ekranu potwierdzenia i wrócić prosto do niego.

Zamówienie sklepu musi podążać za edycjami wykonanymi po jego utworzeniuUtworzyć zamówienie raz na kroku płatności, trzymać jego ID w kontekście, wypychać każdą późniejszą edycję

generate_payment_button() wywołuje create_order tylko wtedy, gdy kontekst nie ma jeszcze order_id, po czym zapisuje ID. Jeśli klient wróci i zmieni metodę płatności, adres, kod promocyjny lub komentarz, każdy handler wywołuje orders.edit(order_id, ...), więc zamówienie WooCommerce jest poprawiane zamiast duplikowane.

Zamówienie niesie chat_id Telegrama i nazwę bota, więc historia zamówień jest pobierana dla czatu, a link pay-for-me może później zlokalizować zamówienie.

Dwóch klientów bota nie może zarezerwować tego samego terminuBlokady w pamięci na termin przez 600 s, liczone względem pozostałej pojemności terminu

Terminy pochodzą ze sklepu dla produktu i dnia z capacity i capacity_used, a terminy oznaczone przez sklep jako zarezerwowane są odfiltrowywane. Gdy klient wybiera termin, silnik zapisuje blokadę z terminem ważności i dodaje rezerwację do koszyka z tym samym czasem blokady. is_freezed() liczy aktywne blokady dla tego terminu, daty i klucza i blokuje termin, gdy blokady osiągną pojemność minus zajęte miejsca. Usunięcie pozycji z koszyka zwalnia jej blokadę przez zdarzenie done_remove_from_cart.

Daty kalendarza i filtrowanie minionych terminów używają strefy czasowej sklepu z ustawień, a nie zegara serwera. Blokady żyją w pamięci procesu bota, więc dotyczą tylko klientów tego bota i resetują się przy restarcie; rezerwacje dokonane w tej samej chwili na stronie internetowej nie są blokowane.

Promocje typu 'co trzecia sztuka 25% taniej' potrzebują formatu regułMały język formuł: warunki liczby pozycji; rabat; n-ta, najtańsza lub najdroższa, limit

Kampanie marketingowe używają formuł takich jak 'c >= 6 & c < 12; 25%; 3n, me, cap=3'. parse_formula() dzieli je na warunki liczby pozycji, rabat procentowy lub stały i modyfikatory (co n-ta sztuka, najtańsza lub najdroższa najpierw, limit). get_applied_amount() rozwija koszyk do jednego wpisu na sztukę produktów kampanii, stosuje formuły po kolei i oznacza użyte sztuki, aby kolejna formuła nie mogła ich ponownie obniżyć.

Okna kampanii są sprawdzane w strefie czasowej sklepu, kampanie w trybie testowym działają tylko na czacie administratora, a rabat kampanii jest pomijany, gdy kupon już obowiązuje. Kupony przypisane do produktu rozszerzają się na dzieci produktów grupowanych i z wariantami.

Płatności i waluty

w produkcie

Co to dajeKlienci widzieli ceny w swojej walucie i płacili kartą, kryptowalutą albo Telegram Stars, lub prosili znajomego o zapłatę. Sklep otrzymywał opłacone zamówienie.

Problem
Płatność zaczyna się na czacie, kończy na stronie bramki albo w Telegramie i musi zakończyć się opłaconym zamówieniem WooCommerce, przez kilka bramek i walut wyświetlania.
Rozwiązanie

Kwoty pozostają w walucie bazowej sklepu i są przeliczane tylko do wyświetlenia i obciążenia. Silnik sam tworzy sesje bramek i odpytuje bramki, które nie mają webhooka dla bota.

Technologie
  • PayPal (sandbox)
  • Stripe Checkout
  • LiqPay
  • CoinGate
  • CryptoBot
  • Telegram Stars (faktury TDLib)
Zadania · 5
Ceny pokazywane w jednej walucie, pobierane w innejKwoty w walucie bazowej, przeliczane przy wyświetlaniu i przy obciążeniu, zamówienia normalizowane z powrotem

Sumy koszyka, rabaty i wysyłka są liczone w walucie bazowej. format_price() przelicza na walutę wybraną na czacie według zsynchronizowanego kursu i stosuje pozycję symbolu oraz miejsca dziesiętne, zaokrąglając w górę, gdy grosze są ukryte. generate_payment_button() przelicza sumę na wybraną walutę przed wywołaniem bramki. Zamówienia pobrane z WooCommerce w innej walucie są dzielone przez zapisany kurs zamówienia, więc historia i sumy zgadzają się z własną arytmetyką bota.

Gdy sklep nie pozwala płacić w innych walutach, wybór jest resetowany do bazowej na kroku płatności, a klient dowiaduje się dlaczego.

Przyjmować Telegram Stars jako metodę płatnościStars jako waluta sklepu XTR z kursem; faktura TDLib, której ładunek wskazuje zamówienie

Stars są skonfigurowane jako waluta o nazwie XTR na liście walut sklepu, więc ich kurs przychodzi tą samą synchronizacją. Kwota to suma bazowa razy kurs XTR, zaokrąglona w górę. Faktura jest wysyłana jako inputMessageInvoice z pustym tokenem dostawcy i ładunkiem JSON w base64 zawierającym order_id, chat_id, amount i gateway. Zapytanie pre-checkout jest obsługiwane, a przy messagePaymentSuccessfulBot ładunek jest dekodowany i sklep otrzymuje powiadomienie, że zamówienie jest opłacone.

Bez waluty XTR opcja Stars zgłasza błąd zamiast faktury.

Bramka kryptowalutowa bez zarejestrowanego webhooka dla botaOdpytywanie co 8 s dla czatu przez maksymalnie 15 min, anulowane po ponownym otwarciu koszyka

Dla CryptoBot silnik tworzy fakturę denominowaną w walucie fiat z wygaśnięciem po 900 s i odpytuje getInvoices co 8 s. Map timerów kluczowana przez chat_id utrzymuje jedno aktywne odpytywanie na klienta, a otwarcie koszyka je anuluje. Po zapłacie zamówienie jest ustawiane na processing, a koszyk czyszczony; po wygaśnięciu lub anulowaniu klient dostaje wiadomość.

CoinGate działa w drugą stronę: jego URL callbacku wskazuje proxy Botoscope, które przekazuje webhook na /command jako payment_success, a zamówienie jest kończone, gdy webhook zgłosi opłacenie.

Każda bramka kartowa potrzebuje własnego formatu sesjiSesje Stripe i LiqPay budowane w silniku, otwierane jako Web App lub URL; PayPal w sandboxie

Każda bramka ma własny konstruktor: Stripe przez Checkout Session, LiqPay przez blob base64 podpisany SHA-1 z klucza prywatnego, danych i klucza prywatnego. Link jest wysyłany jako przycisk Web App lub URL, opcjonalnie poprzedzony przyciskami polityki zwrotów, prywatności i wysyłki. Konstruktor PayPal (OAuth client credentials, potem płatność z adresami powrotu i anulowania) wysyła płatność do punktu końcowego sandbox PayPal w tym kodzie, więc działa tylko do testów.

Metody offline (dane karty bankowej, SWIFT, prezent, pay-for-me) pokazują przetłumaczone instrukcje i przycisk potwierdzenia zamiast linku.

Pozwolić innemu użytkownikowi Telegrama zapłacić za zamówienieLink pay-for-me otwiera zamówienie na czacie płacącego i powiadamia kupującego

Wybór pay-for-me kończy kasę linkiem t.me, który uruchamia bota z payforme_<order_id>. Gdy otworzy go inny użytkownik, silnik ładuje zamówienie ze sklepu, stosuje walutę zamówienia, usuwa metody offline i pay-for-me oraz pokazuje pozostałe bramki dla sumy zamówienia. Po płatności czat kupującego dostaje wiadomość z nazwą płacącego.

Media, pliki i języki

w produkcie

Co to dajeZdjęcia i filmy produktów otwierały się szybko, kupione pliki docierały wprost na czat, a klienci czytali menu i katalog w swoim języku.

Problem
Zdjęcia i pliki produktów żyją w sklepie, a wysyłanie ich przez Telegram przy każdym wyświetleniu jest wolne. Bot pobiera też adresy URL pochodzące z danych sklepu. Interfejs i katalog muszą być czytelne w języku klienta.
Rozwiązanie

Lokalne cache mediów z haszami, strzeżone pobrania przez proxy, ponowne użycie plików już hostowanych przez Telegram oraz warstwowe tłumaczenia: dołączone teksty interfejsu, nadpisania właściciela i tłumaczenia poszczególnych pól w danych katalogu.

Technologie
  • API plików TDLib
  • Strumienie Node
  • crypto (MD5, HMAC)
  • Paczki językowe JSON
Zadania · 6
Ponowne pobieranie zdjęć produktów przy każdym wyświetleniu kartyLokalne cache kluczowane przez MD5 adresu URL, podzielone na foldery z dwóch znaków hex, wysyłane jako pliki lokalne

send_media_gallery() haszuje adres URL każdego medium, przy pierwszym użyciu zapisuje plik w media_files_cache/<first two hex chars>/<hash>, a potem przekazuje TDLib lokalną ścieżkę. Pliki zerobajtowe są usuwane i pomijane, album jest ograniczony do 8 pozycji, a wariant bez własnych obrazów wraca do mediów produktu nadrzędnego.

Linki do wpisów t.me omijają cache i trafiają do TDLib jako pliki zdalne.

Bot pobiera adresy URL wzięte z danych sklepuLista dozwolonych domen, zablokowane rozszerzenia, limity rozmiaru według typu, pliki częściowe usuwane

downloadFileToLocal() akceptuje tylko adresy http(s) w domenie klienta lub na hostach Telegrama, odrzuca rozszerzenia wykonywalne i skryptowe w linkach bezpośrednich, wysyła żądanie HEAD, by odrzucić pliki ponad limit (10 MB dla obrazów i audio, 50 MB dla wideo i dokumentów), a następnie strumieniuje plik przez punkt końcowy fetch-file proxy z tym samym limitem. Puste, zbyt duże i nieudane pobrania są usuwane z dysku.

Duże filmy produktów są wolne do wysłania ze sklepuOpublikować film na kanale; bot odpowiada jego ID pliku Telegrama do ponownego użycia

Answer obserwuje wpisy na kanałach. Gdy film pojawia się na kanale, do którego należy bot, bot odpowiada zdalnym ID pliku tego filmu. Link do wpisu t.me lub to ID można potem użyć jako medium produktu, a silnik wysyła je jako plik zdalny, więc Telegram serwuje go z własnego magazynu, bez wysyłania dla każdego klienta.

Dostarczać płatne pliki cyfrowe na czaciePo płatności wysłać każde pobranie przez strzeżone pobieranie, z cache pod zamaskowaną nazwą

Gdy koszyk zawierał produkty do pobrania, a zamówienie jest opłacone online lub darmowe, do_after_pay() wysyła każdy plik do pobrania tych produktów jako dokument. Pliki są cache'owane pod nazwą wyprowadzoną z ID pobrania przez shiftUUID(), więc przechowywana nazwa nie jest ID sklepu, a dokumenty powyżej 50 MB są odrzucane. Produkty z ograniczonym dostępem pokazują pozostałe dni, liczone od daty zapłaty zamówienia.

Faktury PDF z punktu końcowego faktur sklepu idą tą samą ścieżką.

Tekst interfejsu w 7 językach, edytowalny przez właściciela sklepuDołączony JSON na język, nakładany przez zsynchronizowane nadpisania właściciela, z powrotem do angielskiego

Siedem plików językowych (en, es, pt, ru, pl, kz, ua) z tymi samymi 215 kluczami ładuje się przy starcie. __() scala plik językowy czatu z nadpisaniami, które właściciel wpisał w panelu administracyjnym (synchronizowanymi jako interface_translations i indeksowanymi jako s<number>); jeśli klucza nadal brakuje, ponawia z angielskim plus nadpisania. Wybrany język jest zapisywany dla czatu; przy pierwszym kontakcie pochodzi z language_code klienta Telegram, gdy ten język jest włączony, w przeciwnym razie z domyślnego języka sklepu.

Ponieważ przyciski menu są dopasowywane po zlokalizowanym tekście, etykiety dla czatu są rozwiązywane równolegle przez Promise.all przy każdym sprawdzeniu routingu.

Treść katalogu i wyszukiwanie muszą działać w każdym językuTłumaczenia poszczególnych pól na produktach i terminach; wyszukiwanie dopasowuje tytuł lub tłumaczenia

Produkty, kategorie, marki, pola meta, metody wysyłki i płatności niosą obiekt translations. translate_to() zwraca 0, gdy czat używa języka domyślnego, więc typowy przypadek pomija każde wyszukiwanie; w przeciwnym razie get_field_translation() wybiera przetłumaczone pole i wraca do oryginału. Wyszukiwanie po tytule uruchamia LIKE zarówno na kolumnie tytułu, jak i na zapisanym JSON z tłumaczeniami, z minimum 3 znaków, więc zapytanie w dowolnym aktywnym języku znajduje produkt.

Daty w meta produktu i okresy dostępu są formatowane z językiem czatu jako ustawieniami regionalnymi.

Liczby

z produktu
  • Języki bota 7 Angielski, hiszpański, portugalski, rosyjski, polski, kazachski, ukraiński.
  • Metody płatności online 5 Stripe, LiqPay, CoinGate, CryptoBot i Telegram Stars.
  • Ścieżka odpowiedzi push, bez odpytywania TDLib utrzymuje jedno trwałe połączenie z Telegramem: każde stuknięcie dociera do bota od razu, bez przeskoku przez webhook i bez opóźnienia odpytywania.