Strona główna
O mnie
Projekty
Publikacje
Linki
|
|
„klepy internetowe" Autor: Adam Major
|
Wersja do druku
|
Artykul został pierwotnie opublikowany w magazynie Software 2.0 3/2002 - "Programowanie dla Internetu". |
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:
- Nawigację klienta po sklepie
- Przechowywanie danych o produktach
- Składowanie zamówień
- 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". |
www.software.com.pl |
|