Strona główna Publikacje "Sklepy internetowe" Software 2.0 3/2002 część 1 część 2

Strona główna
O mnie
Projekty
Publikacje
Linki



 

Sklepy internetowe"
Autor: Adam Major

Wersja do druku
Artykul został pierwotnie opublikowany w magazynie Software 2.0 3/2002 - "Programowanie dla Internetu". Software 2.0
www.software.com.pl


W dzisiejszych czasach handel elektroniczny niekiedy przynosi firmie dużo większe zyski niż tradycyjna sprzedaż towarów i usług. Jest przy tym znacznie tańszy (odpadają koszty wynajmu i utrzymania budynków oraz zatrudnienia sprzedawców, transport) i otwarty 24h na dobę przez 365 dni w roku. Dlatego wiele firm mimo recesji jaka ogarnęła naszą gospodarkę próbuje dotrzeć do klientów uruchamiając sklepy internetowe.
Zadaniem tego artykułu jest praktyczne przedstawienie najważniejszych problemów związanych z planowaniem i projektowaniem tego typu aplikacji przy wykorzystaniu PHP 4 i serwera baz danych MySQL.
      Najpierw musimy sobie postawić podstawowe pytanie czym tak naprawdę jest sklep internetowy i jakie powinien spełniać funkcje? Dobrze zaprojektowany sklep powinien przypominać w swoim działaniu placówkę handlową ze "świata realnego". Po wejściu klienta na stronę WWW aplikacja powinna kierować go w ten sposób aby mógł szybko znaleźć poszukiwany produkt lub grupę produktów, obejrzeć dostępne informacje o danym produkcie, a na końcu gdy będzie zdecydowany dokonać zakupu, pozwolić szybko, łatwo i bezpiecznie złożyć zamówienie.
Sklepy składają się więc z dwóch powiązanych ze sobą części: modułu klienckiego oraz panelu administracyjnego, za pomocą którego osoba zarządzająca może wpływać na asortyment sklepu oraz realizować zamówienia klientów.
      Głównymi problemami, które stoją przed programistą, który ma zrealizować sklep internetowy są: stworzenie łatwego w użyciu, szybkiego, przyjaznego w nawigacji i bezpiecznego modułu klienckiego oraz powiązanego z nim panelu administracyjnego, pozwalającego osobie nie obeznanej ze sztuką tworzenia stron WWW prowadzić sklep.
W artykule tym poruszam najważniejsze zagadnienia z którymi przyjdzie się zmierzyć osobie tworzącej sklep internetowy tj. zaprojektowanie struktury bazy danych, koszyka oraz struktury katalogów serwisu i pliku konfiguracyjnego. Zostały także poruszone problemy związane z bezpieczeństwem.

Struktura bazy danych

Projektowanie struktury bazy danych zaczniemy od zastanowienia się jakie dane musimy przechowywać i jak najkorzystniej je pogrupować, w taki sposób aby nie dublować ich (jeśli jest to tylko możliwe) w kilku tabelach.
Możemy wyodrębnić grupy danych odpowiedzialnych za:

  1. Nawigację klienta po sklepie
  2. Przechowywanie danych o produktach
  3. Składowanie zamówień
  4. Funkcje administracyjne

Nawigacja

W większości sklepów internetowych asortyment pogrupowany jest w działy tematyczne (np. komputery, samochody), które dzielą się dodatkowo na kategorie (np. procesory, karty graficzne, monitory). My przyjmiemy także takie rozwiązanie ponieważ jest ono najbardziej rozpowszechnione i dość naturalne dla potencjalnego klienta naszego sklepu. Projektowany system nawigacji będzie umożliwiał zakładanie do maksymalnie 255 działów, zawierających po max 255 kategorii. Tego typu ograniczenia (wygodne ze względów technicznych) praktycznie nie powinny przeszkadzać w niczym osobie prowadzącej sklep.
      System nawigacji w bazie danych będziemy przechowywać w dwóch tabelach: cat będzie składować dane o działach, subcat natomiast informacje o kategoriach w danym dziale. Oprócz identyfikatora działu i kategorii zapisujemy także czy dany dział/kategoria jest aktywny, czyli czy ma być widoczny dla klienta.

W tabeli cat przechowujemy identyfikator, pole informujące czy dany dział jest aktywny (widoczny) oraz nazwę działu. W identyfikatorze id_ct nie używamy własności auto_increment, ponieważ chcemy zapobiec powstawaniu "dziur" w numeracji, na skutek kasowania i tworzenia nowych działów. Niestety wtedy o poprawną numerację będziemy musieli zadbać sami. Za aktywne działy będziemy uznawać te, który w polu ct_akt mają wpisaną cyfrę 1. Zakładamy indeksy (key) dla wszystkich pól, które będą przeszukiwane w celu wyświetlenia klientowi w której pod kategorii asortymentu się znajduje.
      Pole pid_ct tabeli subcat przechowuje identyfikator działu do którego dana kategoria należy. Id_sc to identyfikator danej kategorii o jego poprawną numerację także musimy sami zadbać w naszej aplikacji.
      Najprościej można to zrealizować sprawdzając przed dodaniem nowego działu jaki maksymalny identyfikator ma pole id_ct lub id_sc (w przypadku kategorii), a następnie wstawienie nowego wiersza z identyfikatorem powiększonym o 1. Warto także sprawdzić przedtem czy wstawiany rekord będzie miał identyfikator mniejszy od 255.
W ten sposób nie ustrzeżemy się jednak powstawania luk w przypadku skasowania innego rekordu niż ostatniego.
 
CREATE TABLE cat
(
 id_ct        tinyint unsigned not null,
 ct_act       tinyint unsigned not null,
 name_ct      char (30),
 key          ct_act (ct_act),
 key          name_ct (name_ct(6)),
 primary key (id_ct)
);

CREATE TABLE subcat
(
 id_subcat    smallint unsigned not null
              auto_increment,
 sc_act       tinyint unsigned not null,
 pid_ct       tinyint unsigned not null,
 id_sc        tinyint unsigned not null,
 name_sc      char(30),
 key          sc_act (sc_act),
 key          pid_ct (pid_ct),
 key          id_sc (id_sc),
 key          name_sc (name_sc(6)),
 primary key (id_subcat)
);
Listing 1. Polecenia SQL tworzące tablice cat i subcat.
Aby ominąć ta niedogodność można wczytać do tablicy posortowane rosnąco id_ct wszystkich działów, następnie przejrzeć tablicę w pętli i znaleźć pierwszy wiersz o nie kolejnym identyfikatorze. Jest to dość czasochłonne działanie, jednak przy 255 rekordów możemy sobie na nie pozwolić.

Tworzenie drzewka kategorii

Obok przedstawiam listing skryptu pozwalającego wyświetlić klientowi drzewko działów z wyróżnionym wybranym działem oraz rozwiniętymi jego kategoriami. Po dodaniu grafiki i stylów CSS można uzyskać taki efekt jak pokazano na rysunku 1.

Rysunek 1. Przedstawienie działów i kategorii w postaci drzewka.
 
<?php
function pconnect_to_mysql()
{
 global $db, $conn;

 $conn = @mysql_pconnect($db[host], $db[user], $db[pass]);
 if (!@mysql_select_db($db[base], $conn))
    { $err_ln=__LINE__; include('db_error.php'); }
 return $conn;
}

function show_cat_tree()
{
  global $HTTP_GET_VARS;

  if ($HTTP_GET_VARS[d] <= 0 OR
    !is_numeric($HTTP_GET_VARS[d])) $HTTP_GET_VARS[d] = 1;
  $conn = pconnect_to_mysql();
  $sql = "select id_ct, name_ct from cat where ct_act=1 order
   by name_ct";
  if (!($res = @mysql_query($sql,$conn)))
     { $err_ln=__LINE__; include('db_error.php'); }
  if (mysql_num_rows($res) == 0)
     { echo 'Brak kategorii'; return 0; }

 while ($row = @mysql_fetch_row($res))
     {
      if ($row[0] == $HTTP_GET_VARS[d]) echo "- $row[1]<br>";
         else $other_cat[]= "+ $row[1]<br>";
     }
 // podkategorie
 $sql = "select id_sc, name_sc from subcat where
  pid_ct='$HTTP_GET_VARS[d]' and sc_act=1 order by name_sc";
 if (!($res = @mysql_query($sql,$conn)))
    { $err_ln=__LINE__; include('db_error.php'); }
 while ($row = @mysql_fetch_row($res))
   echo "   $row[1]<br>";

 $cnt_ot = count($other_cat);
 for ($x = 0; $x < $cnt_ot; $x++) echo $other_cat[$x];
}
?>
Listing 2. Tworzenie drzewka kategorii

Przechowywanie danych o produktach

Tego typu dane najlepiej zgrupować w dwie tabele: prod i prod2. Pierwsza z nich będzie przechowywać informacje potrzebne do transakcji (tj. nazwa, cena netto, cena brutto itp.) oraz przyporządkowanie do działu i kategorii. Druga tabela będzie zawierać dłuższe opisy produktów. Podział na dwie tabele jest korzystny ponieważ znacznie zmniejszamy ilość danych, które musi przetworzyć baza dany w przypadku wyszukiwania produktu np. wg ceny.

Jednym z najważniejszych kryteriów profesjonalnego tworzenia sklepów jest zapewnienie dużej szybkości działania, dlatego w tabeli prod zapisujemy nazwę produktu w polu typu char oraz zakładamy indeksy na wszystkie pola, które będą przeszukiwane. Również ze względów wydajnościowych nie przechowujemy w bazie zdjęć produktów a jedynie informacje o nich. Pola mini* zawierają dane dotyczące miniaturki zdjęcia produktu.
Mini_t - format zdjęcia (0 - brak, 1 - GIF, 2-JPEG, 3 - PNG), ten sposób przechowywania tej informacji pozwala w razie potrzeby na dodanie kolejnego formatu grafiki np. 4-SWF. Mini_w i Mini_h to odpowiednio szerokość i wysokość obrazka. Dane te uzupełniamy przy dodawaniu nowego produktu lub jego edycji.
Rid_ct i Rid_sc zawierają identyfikator działu i kategorii.
      Aby wyświetlić wszystkie informacje o danym produkcie możemy się posłużyć następującym zapytaniem:

select * from prod, prod2 where id_pr='$HTTP_GET_VARS[id]' AND id_pr2=id_pr

 
CREATE TABLE prod
(
 id_pr       smallint unsigned not null
             auto_increment,
 pr_act	     tinyint unsigned not null,
 rid_ct	     tinyint unsigned not null,
 rid_sc	     tinyint unsigned not null,
 name        char(40) not null,
 price_n     float(4,2) not null,
 price_b     float(4,2) not null,
 vat         tinyint,
 mini_t      tinyint unsigned,
 mini_w      tinyint unsigned,
 mini_h      tinyint unsigned,
 key         pr_act (pr_act),
 key         rid_ct (rid_ct),
 key         rid_sc (rid_sc),
 key         name (name),
 key         price_n (price_n),
 key         price_b (price_b),
 primary key (id_pr)
);

CREATE TABLE prod2
(
 id_pr2      smallint unsigned not null
             auto_increment,
 big_t       tinyint unsigned,
 big_w       tinyint unsigned,
 big_h       tinyint unsigned,
 info        text,
 primary key (id_pr2)
);
Listing 3. Polecenia SQL tworzące strukturę tabel prod i prod2.

Składowanie zamówień

Jest praktycznie najistotniejszym elementem działania sklepu. W tej grupie danych musimy przechowywać wszystkie informacje konieczne do zrealizowania zamówienia oraz przydatne do generowania wszelkiego rodzaju statystyk np. dynamiki sprzedaży itp. Również tutaj najkorzystniejsze jest stworzenie dwóch tabel. W jednej będziemy przechowywać dane teleadresowe klienta w drugiej spis produktów, które zakupił.
W przypadku drugiej tabeli oprócz identyfikatora produktu, przechowujemy również jego cenę oraz wartość podatku VAT jaka była w czasie składania zamówienia. Jest to bardzo istotne ze względu na stosunkowo szybką zmianę cen czy też okresowe promocje, które mogą cechować projektowany sklep. Klient powinien płacić tyle ile wynosiła cena produktów w chwili zakupu, a nie realizacji zamówienia.

W tabeli buy oprócz danych teleadresowych przechowujemy: identyfikator zamówienia id_b, status - określa w jakim stadium realizacji jest dane zamówienie, można np. przyjąć trzy poziomy realizacji: oczekiwanie, pakowanie, wysyłka (zrealizowane). Jeśli zamierzamy udostępnić użytkownikom stronę z ich "historią zamówień" bardzo istotnym elementem jej byłaby informacja o postępach w realizacji zamówienia. Po zmianie stanu zamówienia (przez operatora sklepu) na zrealizowany, powinna zostać ustawiona data wysyłki (send_date).
      Oprócz wyżej wymienionych pól istotne są także pola: total, zawierające wartość zamówienia - można co prawda ją wyliczyć na podstawie tabeli buy2, ale jest to dość czasochłonne w przypadku generowania np. statystyk miesięcznych - i bad_post, które przechowuje informacje o liczbie błędnych prób wysłania formularza zamówieniowego. Na podstawie tej informacji operator sklepu może zdecydować czy warto wysłać produkt do klienta.
Więcej o sposobach zabezpieczenia się przed nieuczciwymi klientami w dalszej części artykułu.
W tabeli buy2 przechowywane są dane dotyczące poszczególnych produktów w zamówieniu. Id_z to identyfikator zamówienia, qt ilość sztuk produktu o identyfikatorze id_p.
 
CREATE TABLE buy
(
 id_b        smallint unsigned not null
             auto_increment,
 status      tinyint unsigned not null,
 total       float (6,2) not null,
 firm        varchar (40),
 fname       varchar (20),
 lname       varchar (30) not null,
 street      varchar (40),
 post        char (6),
 city        varchar (30),
 region      tinyint unsigned not null,
 phone       varchar (22),
 email       varchar (40),
 pesel       varchar (11),
 nip         varchar (13),
 bad_post    tinyint unsigned,
 buy_date    timestamp not null,
 send_date   timestamp not null default '0',
 host        varchar (200),
 key         status (status),
 key         total (total),
 key         firm (firm),
 key         lname (lname),
 key         region (region),
 key         buy_date (buy_date),
 key         send_date (send_date),
 primary key (id_b)
);

CREATE TABLE buy2
(
 id_b2       mediumint unsigned not null
             auto_increment,
 id_z        smallint unsigned not null,
 id_p        smallint unsigned not null,
 qt          smallint unsigned,
 price_n     float (4,2),
 price_b     float (4,2),
 vat         tinyint,
 key         id_z (id_z),
 key         id_p (id_p),
 primary key (id_b2)
);
Listing 4. Polecenia SQL tworzące tabele buy i buy2.

Funkcje administracyjne

Ta grupa danych odpowiada za logowanie się opiekunów sklepu oraz nadanie im odpowiednich uprawnień do zarządzania poszczególnymi częściami sklepu. W najprostszym przypadku, kiedy wszyscy administratorzy są sobie równi możemy zastosować następującą strukturę bazy.  
CREATE TABLE admin
(
 id_a        tinyint unsigned not null
             auto_increment,
 login       char(12) not null,
 pass        char(14) binary,
 primary key (id_a)
);
Listing 5. Polecenia SQL tworzące tabelę admin

Mamy już zaprojektowaną strukturę bazy danych teraz przychodzi czas na zaplanowanie sposobu przechowywania informacji o tym co dany klient chce kupić, czyli

Koszyk

Istnieje wiele możliwości przechowywania informacji o stanie koszyka klienta. Można zapisywać informacje po stronie klienta np. w ciastkach lub ukrytych polach formularza, jednak nie jest to bezpieczne, ponieważ użytkownik może manipulować przy tych danych. Część rozwiązań opiera się o bazę danych lub pliki tymczasowe jednak taki sposób choć o wiele bezpieczniejszy jest dość nie wygodny oraz w większości w/w rozwiązań silnie obciąża serwer. Najlepszym rozwiązaniem łączącym bezpieczeństwo i prostotę użytkowania jest zastosowanie sesji. Wszelkie informacje o stanie koszyka są przechowywane po stronie serwera, standardowo w plikach. Dane o ilości egzemplarzy produktu o danym identyfikatorze można zapisywać w tablicy dwuwymiarowej lub skorzystać z dość powszechnie znanej klasy koszyka na zakupy.
My skorzystamy właśnie z pewnej odmiany takiej klasy.

Dla części czytelników zastanawiający może być fragment od if (ini_get... oraz linia $HTTP_SESSION_VARS[cart] = $cart =...
Ze względów bezpieczeństwa powinniśmy się odwoływać do zmiennych przez tablice asocjacyjne $HTTP_*_VARS, pozwala nam to jednoznacznie określić skąd pochodzą dane.
Abyśmy mogli korzystać z globalnej przestrzeni zmiennych (wtedy wszystkie zmienne również są widoczne jako $zmienna) należy mieć włączoną w pliku php.ini opcję register_globals. PHP ewoluuje w kierunku w którym w/w opcja będzie domyślnie wyłączona. Jeśli chcemy zapewnić działanie w/w funkcji w przypadku kiedy register_globals jest włączone lub wyłączone należy posłużyć się właśnie takim trikiem, ponieważ gdy ta opcja jest włączona i koszyk nie jest zainicjowany to nie można się odwołać do metody add koszyka, którego nie ma jeszcze w sesji.
Musimy więc sprawdzić czy PHP jest skonfigurowane do tworzenia globalnej przestrzeni zmiennych jeśli tak to odwołujemy się do zmiennej globalnej, jeśli nie to odwołujemy się normalnie poprzez zmienną $HTTP_SESSION_VARS[cart].
Przynajmniej mi się nie udało tego obejść w wersji 4.0.6 inaczej [ przyp. autora po opublikowaniu tego artykułu zauważyłem bardzo proste rozwiązanie problemu. Należy sprawdzić czy istnieje zmienna sesyjna koszyk jeśli nie to zarejestrować koszyk i przekierować (wywołać ponownie) skrypt tak aby nastąpiło dodanie produktu. Jeśli natomiast jest już zarejestrowana zmienna sesyjna wystarczy wykonać metodę add i dodać produkt do koszyka], oczywiście po za skorzystaniem z funkcji
ini_set ('register_globals', '0').
      Kod z listngu 6 zapisujemy do pliku o nazwie np. dokoszyka.php, odpowiednie wywołanie tego pliku spowoduje
 
<?php
class cart
{
 var $za;
 function add ($element) { $this->za[$element]++; }
 function del ($element)
          { unset($this->za[$element]); }
 function edit ($element, $val)
          { $this->za[$element] = $val; }
 function show_cart() { return $this->za; }
 function drop_cart() { unset($this->za); }
}

function add_cart()
{
 global $HTTP_GET_VARS, $HTTP_SESSION_VARS, $cart,
  $c_total, $c_bad;

 $conn = pconnect_to_mysql();
 $sql = "select id_pr from prod where id_pr =
 '$HTTP_GET_VARS[id_p]'";
 if (!($res = @mysql_query($sql,$conn)))
   {$err_ln=__LINE__; include('db_error.php'); }
 if (@mysql_num_rows($res) == 0) return 0;

 if (!$HTTP_SESSION_VARS[cart])
  {
   session_register(cart, c_total, c_bad);
   $HTTP_SESSION_VARS[cart] = $cart = new cart;
   $HTTP_SESSION_VARS[c_total] = 0;
   $HTTP_SESSION_VARS[c_bad] = 0;
   if (ini_get ('register_globals'))
      $cart->add($HTTP_GET_VARS[id_p], 1);
      else $HTTP_SESSION_VARS[cart]->
        add($HTTP_GET_VARS[id_p], 1);
  }
  else $HTTP_SESSION_VARS[cart]->
       add($HTTP_GET_VARS[id_p], 1);
}

if ($HTTP_GET_VARS[id_p] > 0 AND
   is_numeric($HTTP_GET_VARS[id_p])) add_cart();
header('location: koszyk.php');
?>
Listing 6. Klasa cart, sposób inicjalizacji i dodania produktu do koszyka.
zainicjowanie koszyka (jeśli to konieczne), wprowadzenie nowego produktu do kosza (lub zwiększenie ilości danego produktu) oraz przekierowanie do skryptu koszyk.php. Zastosowanie pliku pośredniczącego (dokoszyka.php) dodawanie produktów do koszyka powoduje uodpornienie go na przeładowywanie strony (odśwież, reload) przez użytkownika, które mogłoby prowadzić do zwiększania się ilości danego produktu przy każdym odświeżeniu strony.

część 1 | część 2 >

sklepy_lst.zip - Wszystkie listingi zawarte w artykule (4 KB).

Artykul został pierwotnie opublikowany w magazynie Software 2.0 3/2002 - "Programowanie dla Internetu". Software 2.0
www.software.com.pl


Copyright (c) 2002-03 by Adam Major (Adam.Major[at]ivpro.net)