Часть 2. Построение электронного магазина
В части I мы рассмотрели основные принципы электронной коммерции, познакомились с инструментарием Microsoft, разобрали основные концепции проектирования баз данных и системного проектирования, а также построили несложное приложение.
База данных, спроектированная в главе 3, будет использоваться в главах этой части. Мы возьмем ее за основу и начнем строить на ее основе электронный магазин. В главе 6 рассматривается основной механизм перемещения по сайту, домашняя страница и общая инфраструктура Web-сайта.
В главе 7 создаются базовые средства для построения корзины. В главе 8 мы перейдем к следующей стадии - процессу оформления заказов. Наконец, в главе 9 рассматривается организация обратной связи с покупателем на примере получения информации о состоянии заказов.

Глава 6. Построение пользовательского интерфейса
При конструировании пользовательского интерфейса необходимо учитывать некоторые фундаментальные принципы построения электронных магазинов. Подход, которым вы будете руководствоваться по отношению к этим принципам, заметно повлияет на впечатления, которые останутся у пользователя после посещения магазина. В этой главе мы займемся построением базовой функциональности электронного магазина, в том числе средствами перемещения, общей структурой сайта и данными о товарах.

Проектирование основных функциональных возможностей магазина
Наш магазин будет состоять из нескольких страниц ASP, объединенных при помощи приложения Visual Basic. Страницы работают с базой данных, структура которой была определена в главе 3. В табл. 6.1 перечислены страницы нашего магазина с краткими описаниями их функций. Также для каждой страницы указывается номер главы, в которой анализируется ее программный код.
Таблица 6.1. Web-страницы электронного магазина ECStore


Страница

Описание

Глава

Footer.asp

Нижний колонтитул, выводимый в конце каждой отображаемой страницы сайта. Содержит теги, завершающие область основного содержимого

Глава 6

Header.asp

Верхний колонтитул, выводимый в начале каждой отображаемой страницы сайта. Определяет основную структуру сайта

Глава 6

AddItem.asp

Включение новой позиции в корзину при выборе товара пользователем

Глава 7

Basket.asp

Вывод текущего содержимого корзины

Глава 7

Confirmed.asp

Выводит подтверждающее сообщение и благодарит пользователя за оформление заказа

Глава 8

Default.asp

Домашняя страница сайта

Глава 6

DeleteItem.asp

Удаление позиции из корзины

Глава 7

Dept.asp

Вывод списка всех разделов магазина

Глава 6

EmailPassword.asp

Отправка по электронной почте пароля, соответствующего заданному профилю

Глава 9

EmptyBasket.asp

Удаление всего содержимого корзины

Глава 7

Global.asa

Файл уровня приложения, выполняемый при запуске нового приложения или сеанса

Глава 6

OrderHistoryDisplay.asp

Вывод истории заказов покупателя

Глава 9

OrderReceipt.asp

Вывод экранного отчета для полученного заказа

Глава 8

OrderStatus.asp

Страница, на которой вводится имя и пароль для получения доступа к истории заказов покупателя

Глава 9

Payment.asp

Ввод данных для выписки счета

Глава 8

Product.asp

Вывод информации о заданном товаре

Глава 6

Products.asp

Вывод всех товаров заданного раздела

Глава 6

Profile.asp

Страница для ввода имени и пароля при чтении и редактировании покупателем своего профиля

Глава 9

ProfileDisplay.asp

Вывод профиля заданного покупателя

Глава 9

Search.asp

Поиск товаров в базе данных

Глава 6

Shipping.asp

Ввод данных для доставки заказа

Глава 8

UpdateBasket.asp

Обновление корзины новым количеством товаров

Глава 7

UpdateProfile.asp

Обновление профиля покупателя

Глава 9

ValidatePayment.asp

Проверка платежных реквизитов, введенных покупателем

Глава 8

ValidateShipping.asp

Проверка данных доставки, введенных покупателем

Глава 8

ПРИМЕЧАНИЕ
В части 3 "Управление магазином" мы создадим инструменты для управления данными магазина. В части 4 "Реклама" будут реализованы дополнительные средства для поддержки различных рекламных функций.

Именно эти базовые страницы образуют среду, в которой происходит непосредственное приобретение товаров на сайте. При создании страниц, обеспечивающих перемещение по сайту и выполнение прочих функций, мы ориентируемся на вполне определенную последовательность действий покупателя.
Архитектура сайта
При входе в магазин покупатели обычно действуют по одному из нескольких типовых сценариев. В идеальном варианте они просматривают ассортимент магазина, перебирая разделы и товары. Кроме того, они могут искать конкретный товар.
Мы надеемся, что в ходе просмотра покупатель помещает отобранные товары в корзину. Затем покупатель выполняет с корзиной некоторые операции (изменяет количество единиц товара, удаляет отдельные позиции и т. д.) и переходит к оформлению заказа. В процессе оформления мы получаем ключевые данные покупателя, в том числе данные для выписки счета и доставки. После обработки данных покупатель может просмотреть историю заказов в своем профиле.
Конечно, при проектировании средств перемещения в системе необходимо предусмотреть возможность перехода в любое допустимое состояние. Однако мы не можем разрешить покупателю перейти к вводу платежных реквизитов, если в его корзине нет ни одного товара. В этом случае необходимо вывести соответствующее сообщение для пользователя.
Наконец, в течение всего процесса закупки очень важно обеспечить сохранение состояния. Мы должны знать текущий идентификатор покупателя, чтобы следить за содержимым его корзины. Кроме того, необходимо иметь возможность временного сохранения данных, введенных на различных формах. Например, если после перехода на страницу оплаты покупатель захотел включить в корзину еще один товар, не следует заставлять его заново вводить всю информацию для доставки.
Для отслеживания различных данных мы будем использовать сеансовые переменные.
Создание проекта
Как было показано в главе 5, работа начинается с создания нового проекта на Web-сервере. В качестве примера мы будем использовать электронный магазин, торгующий компакт-дисками. В этом магазине будут продаваться довольно необычные диски, не встречающиеся даже у самых крупных Интернет-продавцов.
Сценарии SQL, находящиеся на прилагаемом компакт-диске, заполняют таблицы разделов и товаров примерами данных. Кроме того, на диске присутствует вся графика, использованная в оформлении нашего магазина. Если вы захотите увидеть магазин в работе, зайдите по адресу www.activepubs.com.
Что касается программирования, ASP-код нашего магазина будет относительно простым. Как вы вскоре убедитесь, последовательное и хорошо продуманное распределение кода по страницам решает многие проблемы, возникающие при построении магазина. Имена переменных также выбираются по простым и понятным правилам.
Построение проекта начинается со страницы global.asа, приведенной в листинге 6.1. Функции этого файла вызываются при каждом запуске приложения (запуске/перезапуске Web-сайта) или создании нового сеанса пользователя. Файл содержит специализированные функции, инициируемые в определенной ситуации. Например, функция Application_OnStart выполняется при запуске приложения Web-сайта. Обычно она используется для начальной инициализации параметров. Также существует парная функция Application_OnEnd. Хотя в нашем примере эти функции не используются, они часто применяются для сохранения глобальных данных (например, статистических счетчиков), служебных переменных сервера и т. д.
В нашем магазине используется функция Session_OnStart. Вспомните: как было сказано выше, мы должны отслеживать идентификатор покупателя. В момент посещения идентификатор еще не существует, но в дальнейшем мы собираемся проверить наличие cookie с профилем на компьютере покупателя. Чтобы обеспечить правильную инициализацию этой ключевой переменной, мы обнуляем idShopper. В дальнейшем этой переменной либо присваивается новый идентификатор покупателя, либо существующее значение загружается из профиля, хранящегося на компьютере клиента. У функции Session_OnStart существует парная функция Sess1on_OnEnd, которая в нашем примере не используется.
Листинг 6.1. Global.asa
<SCRIPT LANGUAGE=VBScript RUNAT=Server>
' ****************************************************
' Global.ASA - выполняется при создании нового сеанса для нового покупателя.
' ****************************************************
' Процедура, выполняемая в начале сеанса.
Sub Session_OnStart
' Присвоить идентификатору покупателя нулевое значение.
session("idShopper") = 0
End Sub
</SCRIPT>
Нам понадобится еще один пустячок - имя источника данных (DSN) ODBC для подключения к базе данных. В данном примере используется файловый DSN
WildWillieCDs. Остается лишь заполнить базу примерами данных, после чего можно переходить к программированию.
ПРИМЕЧАНИЕ
Для подключения также можно воспользоваться механизмом OLE DB. В процессе разработки и тестирования допускается применение имени SA и соответствующего пароля - это упрощает процесс разработки, но не забудьте установить защиту в окончательной версии.

Загрузка данных
Средства управления магазином еще не готовы, однако мы должны занести в таблицы примерные данные. На прилагаемом компакт-диске находятся сценарии SQL для заполнения таблиц. Давайте рассмотрим процесс вставки данных - это поможет вам лучше разобраться в том, как происходит заполнение таблиц.
Сначала мы заполняем таблицу Department данными о разделах. Запись содержит название раздела, описание и ссылку на графическое изображение. Пример вставки записи приведен в листинге 6.2.
Листинг 6.2. Заполнение таблицы разделов
insert into department(chrDeptName, txtDeptDesc, chrDeptImage) values('Funky Wacky Music','The craziest music you have ever seen. Is it even music?','funk.gif')
Затем мы загружаем информацию о товарах и указываем, к какому разделу относится тот или иной товар. Команда SQL, приведенная в листинге 6.3, создает описание товара- в таблицу Products включается компакт-диск Joe Bob's Thimble Sounds.
СОВЕТ
Не забудьте проверить апострофы в строках и удвоить их перед вставкой в базу данных. Методика замены продемонстрирована в главе 5.

Следующая команда SQL в листинге 6.3 связывает новый товар с одним из разделов. В нашем примере первый товар, вставленный в таблицу, связывается с первым разделом посредством создания записи в таблице DepartmentProducts.
ПРИМЕЧАНИЕ
Сначала запустите сценарий LoadDepts.sql, и только потом - сценарий LoadProducts.sql. Последний предполагает, что записи разделов уже были занесены в таблицу и следуют в определенном порядке.

Листинг 6.3. Заполнение таблицы товаров
insert into products(chrProductName, txtDescription, chrProductImage, intPrice, intActive)
values('Joe Bob''s Thimble Sounds', 'Great thimble music that you will love!', 'thimble.gif', 1000, 1)
insert departmentproducts(idDepartment, idProduct) values(1,1)
Некоторые товары обладают атрибутами, которые также необходимо загрузить. Например, в ассортименте нашего магазина имеются футболки разных цветов и размеров, выбираемых покупателем. Две команды SQL, приведенные в листинге 6.4, включают в таблицу категорий два типа атрибутов: размер (Size) и цвет (Color).
Листинг 6.4. Создание атрибутов
insert into attributecategory(chrCategoryName) values('Size')
insert into attributecategory(chrCategoryName) values('Color')
Значения атрибутов, входящих в эти категории, заносятся в таблицу Attribute. Например, категория Size в нашем примере состоит из атрибутов Small, Medium, Large и X-Large. Команды SQL, приведенные в листинге 6.5, создают атрибуты категорий Size и Color.
Листинг 6.5. Создание категорий атрибутов
insert into attribute(chrAttributeName, idAttributeCategory) values('Small', 1)
insert into attribute(chrAttributeName, idAttributeCategory) values('Medium', 1)
insert into attribute(chrAttributeName, idAttributeCategory) values('Large', 1)
insert into attribute(chrAttributeName, idAttributeCategory) values('X-Large', 1)
insert into attribute(chrAttributeName, idAttributeCategory) values('Red', 2)
insert into attribute(chrAttributeName, idAttributeCategory) values('Blue', 2)
insert into attribute(chrAttributeName, idAttributeCategory) values('Green', 2)
insert into attribute(chrAttributeName, idAttributeCategory) values('White', 2)
Остается связать товары с различными атрибутами. В нашем примере магазин предлагает два типа футболок. Построение комбинаций осуществляется командами, приведенными в листинге 6.6.
Листинг 6.6. Назначение атрибутов
insert into productattribute(idAttribute, idProduct) values(1, 9)
insert into productattribute(idAttribute, idProduct) values(2, 9)
insert into productattribute(idAttribute, idProduct) values(3, 9)
insert into productattribute(idAttribute, idProduct) values(4, 9)
Создание структуры страниц
Хорошо спроектированный магазин должен обладать логически согласованными средствами перемещения между страницами. Ключевыми элементами механизма перемещения являются ссылки на страницы списка разделов, корзины, оформления заказов и поиска товаров.
Кроме того, мы создадим ссылки для получения информации о состоянии заказа и операций с профилем покупателя.

Также обратите внимание на элементы, расположенные в нижней части окна. Мы хотим объединить страницы сайта в структуру, которой будет удобно управлять. Например, когда-нибудь в будущем на сайте может появиться ссылка для просмотра специальных предложений. Было бы нежелательно изменять все страницы магазина для того, чтобы вставить новую ссылку. Вместо этого мы выделяем верхний и нижний колонтитул во включаемые файлы, создающие общую структуру страницы.
Страницы Header и Footer
Страница Header.asp (см. листинг 6.7) определяет общую структуру отображаемых страниц. Кроме того, в ней реализованы логические элементы для настройки данных покупателя. Сначала эта страница проверяет, равен ли идентификатор покупателя (сеансовая переменная idShopper) нулю.
В этом случае необходимо решить две задачи. Во-первых, мы проверяем, имеется ли на компьютере пользователя cookie с предыдущим идентификатором покупателя. Для этого мы получаем значение cookie с именем WWCD. Если оно равно пустой строке, значит, необходимо создать новую запись покупателя. В противном случае идентификатор читается из cookie.
Листинг 6.7. Header.asp
<!-- Header.asp - файл включается в начало каждой страницы магазина. В нем определяется общий макет страницы и ссылки для перемещения между страницами.-->
<%
'Проверить, равен ли идентификатор покупателя 0. В этом случае мы должны создать идентификатор для дальнейшего отслеживания покупателя.
if session("idShopper") = "0" then
' Проверить наличие cookie с идентификатором покупателя.
if Request.Cookies("WWCD") = "" then
Чтобы создать запись покупателя, мы открываем подключение к базе данных и выполняем хранимую процедуру sp_InsertShopper, которая возвращает новый идентификатор покупателя. Полученное значение присваивается сеансовой переменной, и страница продолжает работу. Обратите внимание: идентификатор покупателя не записывается в cookie. Эта возможность будет предложена покупателю в процессе оформления заказа.

Листинг 6.8. Header.asp (продолжение)
'Создать объект подключения к базе данных
set dbShopper = server.createobject("adodb.connection")
'Создать набор записей
set rsShopper = server.CreateObject("adodb.recordset")
'Открыть подключение, используя файловый DSN ODBC
dbShopper.open("filedsn=WildWillieCDs")
'Построить команду вызова хранимой процедуры для создания нового покупателя, поскольку cookie не существует.
sql = "execute sp_InsertShopper"
'Выполнить команду SQL
set rsShopper = dbShopper.Execute(sql)

'Сохранить идентификатор в сеансовой переменной.
session("idShopper") = rsShopper("idShopper")

else
Если cookie существует, мы читаем из него идентификатор покупателя. Впрочем, это еще не все - мы также должны загрузить последнюю открытую корзину (если она существует), чтобы покупатель продолжил свою работу с того места, на котором она прекратилась. Идентификатор покупателя сохраняется в сеансовой переменной. После этого мы выполняем хранимую процедуру sp_RetrieveLastBasket, которая возвращает последнюю корзину. Если корзина была успешно возвращена, ее идентификатор сохраняется в сеансовой переменной idBasket
Листинг 6.9. Header.asp (продолжение)
'Прочитать идентификатор покупателя из cookie
session("idShopper") = Request.Cookies("WWCD")

'Создать объект подключения к базе данных
set dbShopperBasket = server.createobject("adodb.connection")
'Создать набор записей
set rsShopperBasket = server.CreateObject("adodb.recordset")
'Открыть подключение, используя файловый DSN ODBC
dbShopperBasket.open("filedsn=WildWillieCDs")
'Получить последнюю корзину, использовавшуюся покупателем.
'Возвращаются только те корзины, обработка которых не была завершена
sql = "execute sp_RetrieveLastBasket " & session("idShopper")
'Выполнить команду SQL
set rsShopperBasket = dbShopperBasket.Execute(sql)

'Проверить, была ли возвращена корзина.
if rsShopperBasket.EOF <> true then

'Присвоить идентификатор корзины сеансовой переменной
session("idBasket") = rsShopperBasket("idBasket")

end if
'Установить признак того, что профиль НЕ БЫЛ прочитан.
session("ProfileRetrieve") = "0"
end if
end if
%>
После получения информации о покупателе и корзине можно переходить к форматированию верхнего колонтитула. Мы создаем несколько таблиц, содержащих ключевые секции страницы.
Первая секция представляет собой верхнюю строку, в которой выводится логотип с компакт-дисками и название страницы. За ней следует таблица, организующая перемещение по сайту. В первом столбце первой строки расположены ссылки для перемещения по основным страницам сайта. Во втором столбце выводится основное содержимое страниц. Закрывающие теги второго столбца, строки и таблицы находятся в файле Footer.asр.
В этой странице использованы две хранимые процедуры. Первая, sp_InsertShopper, создает новую запись в таблице покупателей. В столбце счетчика при этом генерируется новое значение. Мы обращаемся к нему при помощи системной переменной @@identity, содержащей последнюю сгенерированную величину.
Листинг 6.11. Хранимая процедура spJnsertShopper
/* Сохранение записи нового покупателя в базе данных. */
CREATE PROCEDURE sp_InsertShopper AS
/* Сохранить запись покупателя в базе, присвоив полям имени и фамилии пустые строки */
insert into shopper(chrusername, chrpassword) values('', '')
/* Вернуть значение столбца-счетчика, то есть идентификатор покупателя */
select idShopper = @@identity
Вторая хранимая процедура, sp_RetrieveLastBasket, возвращает последнюю активную корзину, использовавшуюся покупателем. Процедура возвращает только те корзины, которые не входят в оформленные заказы. Чтобы последняя корзина находилась в начале набора записей, ключевое слово DESC сортирует результаты выборки по убыванию.
Листинг 6.12. Хранимая процедура sp_RetrieveLastBasket
/* Загрузка из базы данных последней корзины, использованной покупателем. */
CREATE PROCEDURE sp_RetrieveLastBasket
/* При вызове процедуре передается идентификатор покупателя */
@idShopper int
AS
/* Выборка всех корзин покупателя, на которые не был оформлен заказ. Данные сортируются по убыванию, чтобы последняя запись стояла на первом месте. */
select * from basket
where idShopper = @idShopper and
intOrderPlaced = 0 and intTotal = 0
order by dtCreated DESC
Наряду cHeader.asp, мы должны включить Footer.asp в конец страницы. Эта страница закрывает теги, открытые в Header.asp. В нашем примере она также дает возможность вывести сообщение об авторских правах, адрес электронной почты службы поддержки и т. д. Страница Footer.asp приведена в листинге 6.13.
Листинг 6.13. Footer.asp
<!-- Footer.asp - страница включается в конец каждой страницы магазина.
Она завершает описание структуры страницы. -->
<!-- Закрыть столбец содержимого, открытый в header -->
</td>
<!-- Закрыть строку таблицы -->
</tr>
<!-- Начать новую строку для вывода нижнего колонтитула -->
<tr>
<!-- Начать новый столбец, распространяющийся поперек четырех столбцов -->
<td colspan="4" width="680">
<HR>
<!-- Вывести адрес электронной почты службы поддержки -->
Need help? Email
<a href="mailto:support@wildwillieinc.com">
support@wildwillieinc.com</a>
<BR><BR>
<!-- Вывести информацию об авторских правах -->
<font size="2">&copy;Copyright 1999 Wild Willie
Productions, Inc.</font>
</td>
</tr>
Обратите внимание: эти две страницы должны включаться в начало и конец всех страниц, выводящих содержимое для пользователя. При включении только одной страницы произойдет ошибка, поскольку в них содержатся парные открывающие и закрывающие теги.

Построение домашней страницы
Домашняя страница представляет собой точку входа в магазин для покупателей. Как и положено, она содержит верхний и нижний колонтитул. Сразу же после открывающих тегов страницы мы включаем страницу Header.asp командой ASP #1nclude.
Затем мы выводим на странице основное содержимое, которое в нашем случае представляет собой простое приветственное сообщение. Страница закрывается включением файла Footer.asр. Код домашней страницы приведен в листинге 6.14.
Листинг 6.14. Default.asp
<%@ Language=VBScript %>
<HTML>
<!-- Default.asp - Домашняя страница магазина, на которой выводится
приветственное сообщение -->
<!-- #include file="include/header.asp" -->
<!-- Текст приветствия -->
Welcome to <font color="blue"><B>Wild Willie's CRAZY CD</b></font> store! We have some
of the wildest CDs that not even the biggest of the CD stores have. <br><br>

Select departments on the left to start your shopping experience!
<!-- #include file="include/footer.asp" -->
</BODY>
</HTML>
В примерах, находящихся на прилагаемом компакт-диске, имеются правильные ссылки на графические изображения, поэтому все должно выглядеть как нужно. А мы переходим к рассмотрению интерактивных функциональных возможностей сайта.

Просмотр разделов и товаров
Центральное место в этой главе занимают средства для просмотра разделов и товаров. Разобравшись с базовой структурой страниц и загрузкой записей в базы данных, можно переходить к основной теме.
С позиций маркетинга красиво оформленная витрина является одним из важнейших факторов привлечения покупателей. Представленные ниже базовые средства образуют фундамент, на котором можно построить любое внешнее оформление - гораздо более эффектное, чем в нашем магазине.
Разделы
На странице Dept.asp выводится список разделов магазина. В нашем примере для каждого раздела выводится название и графическое изображение. Программный код страницы разделов приведен в листинге 6.15.
Страница начинается со стандартной структуры с включением файла заголовка.
Листинг 6.15. Dept.asp
<%@ Language=VBScript %>
<HTML>
<!--
Dept.asp - вывод списка разделов электронного магазина.
-->
<!-- #include file="include/header.asp" -->

<b>Select from a department below:</b><BR><BR>
Затем мы создаем подключение к базе данных и читаем все записи из базы данных разделов при помощи хранимой процедуры sp_retrieveDepts. Затем начинается цикл, в котором мы последовательно перебираем разделы и выводим их данные.
Листинг 6.16. Dept.asp (продолжение)
<%
' Create an ADO database connection
set dbDepts = server.createobject("adodb.connection")
' Create a record set
set rsDepts = server.CreateObject("adodb.recordset")
' Open the connection using our ODBC file DSN
dbDepts.open("filedsn=WildWillieCDs")
' Call the stored procedure to retrieve
' the departments in the store.
sql = "execute sp_RetrieveDepts"
' Execute the SQL statement
set rsDepts = dbdepts.Execute(sql)
' We will use a flag to rotate images
' from left to right
Flag = 0
Чтобы страница выглядела более привлекательно, мы будем поочередно выводить графику раздела слева и справа от текста. При каждом проходе цикла из базы данных читается название раздела, ссылка на графическое изображение и идентификатор раздела.
Название раздела и графическое изображение связываются со страницей Products.asp, на которой выводится список товаров раздела. Мы проверяем состояние переменной-флага и выбираем способ построения ссылки в зависимости от результата проверки.
Страница закрывается включением файла Footer.asp и завершающими тегами.
Листинг 6.17. Dept.asp (продолжение)
' Loop through the departments
do until rsDepts.EOF
' Retrieve the field values to display the
' name, image and link to the ID of the
' department
chrDeptName = rsDepts("chrDeptName")
chrDeptImage = rsDepts("chrDeptImage")
idDepartment = rsDepts("idDepartment")
' Check the flag
If Flag = 0 then
' Flip the flag
Flag = 1
%>
<!-- Display the image and the name of the department
In this case the image is on the left and the
name on the right.
-->
<a href="products.asp?idDept=<%=idDepartment%>">
<img src="images/<%=chrDeptImage%>" align="middle" border=0>
<%=chrDeptName%></a><BR><BR>

<% else %>
<!-- Display the image and the name of the department
In this case the image is on the right and the
name on the left.
-->
<a href="products.asp?idDept=<%=idDepartment%>">
<%=chrDeptName%>
<img src="images/<%=chrDeptImage%>" align="middle" border=0>
</a><BR><BR>
<%
' Reset the flag
Flag = 0
end if
' Move to the next row.
rsDepts.MoveNext
loop
%>
<!-- #include file="include/footer.asp" -->
</BODY>
</HTML>
В странице Dept.asp используется хранимая процедура sp_RetrieveDepts, возвращающая список разделов. В нашем примере она состоит из простой команды SELECT.
Листинг 6.18. Хранимая процедура sp_RetrieveDepts
/* Загрузка информации обо всех разделах магазина из базы данных */
CREATE PROCEDURE sp_RetrieveDepts AS
/* Выборка всех записей разделов */
На этом построение первой интерактивной страницы нашего магазина завершается. С каждым разделом связана страница, на которой выводится список товаров данного раздела.

СОВЕТ
При большом количестве разделов список лучше построить не в один столбец: а в несколько. В этом случае вам придется реализовать дополнительную логику перехода между столбцами списка.

На следующей странице, Products.asp, отображается список товаров раздела, выбранного на странице Dept.asp. Идентификатор раздела передается в URL при вызове страницы Products.asp (см. листинг 6.19).
Работа страницы начинается с создания набора записей, используемого при запросах к базе данных. Хранимая процедура sp_RetrieveDept возвращает данные конкретного раздела. При ее вызове передается идентификатор раздела, переданный в URL.
Листинг 6.19. Products.asp
<%@ Language=VBScript %>
<HTML>
<!-- Products.asp - вывод товаров конкретного раздела. -->
<!-- #include file="include/header.asp" -->
<%
' Create an ADO database connection
set dbDepartment = server.createobject("adodb.connection")
' Create the record set
set rsDepartment = server.CreateObject("adodb.recordset")
' Open the connection using our ODBC file DSN
dbDepartment.open("filedsn=WildWillieCDs")
' Build the SQL statement. We are calling the
' stored procedure to retrieve the department
' information and passing in the ID of the
' department
sql = "execute sp_RetrieveDept " & request("idDept")
После создания команды SQL мы читаем данные раздела и выводим в заголовке страницы графическое изображение, название и описание раздела. Эта информация поможет покупателю лучше разобраться в том, куда он перешел.
Идентификатор раздела сохраняется в сеансовой переменной для дальнейшего использования. Когда покупатель перейдет к странице корзины, мы сможем построить ссылку для возвращения к разделу, в котором он работал. Скажем, если покупатель интересуется джазом, он сможет быстро вернуться к разделу "Джаз" и продолжить покупки.
Листинг 6.20. Products.asp (продолжение)
'Прочитать данные раздела
set rsDepartment = dbDepartment.Execute(sql)
'Получить значения полей
txtDescription = rsDepartment("txtDeptDesc")
chrDeptImage = rsDepartment("chrDeptImage")
chrDeptName = rsDepartment("chrDeptName")
' Store the ID of the deparment being referenced in
' the LastIDDept session variable. This will allow us
' to build a link on the basket back to the department
' for further shopping.
session("LastIDDept") = request("idDept")
%>
<!-- Display the department image and name -->
<CENTER>
<img src="images/<%=chrDeptImage%>" align="middle">
<FONT size="4"><B><%=chrDeptName%></b></font><BR><BR>
</CENTER>
<!-- Display the description -->
<%=txtDescription%> Select a product:<BR><BR>
Переходим к получению списка товаров раздела. Мы снова создаем подключение к базе данных и используем хранимую процедуру для получения списка товаров. Идентификатор раздела передается хранимой процедуре в качестве параметра.
Листинг 6.21. Products.asp (продолжение)
<%
' Создать объект подключения к базе данных
set dbProducts = server.createobject("adodb.connection")
'Создать набор записей
set rsProducts = server.CreateObject("adodb.recordset")
'Открыть подключение, используя файловый DSN ODBC
dbProducts.open("filedsn=WildWillieCDs")
'Построить команду SQL для получения списка всех товаров раздела.
'Идентификатор раздела передается процедуре в качестве параметра.
sql = "execute sp_RetrieveDeptProducts " & request("idDept")
'Выполнить команду SQL и получить набор записей
set rsProducts = dbProducts.Execute(sql)
Как и на странице разделов, мы будем выводить информацию о товарах, чередуя порядок следования графики и текста. В процессе перебора товаров позиция последнего изображения отмечается при помощи переменной-флага.
Для каждого товара выводится его название и графическое изображение. Как и на странице разделов, мы используем их для построения ссылки на страницу с описанием конкретного товара и включаем в URL идентификатор товара. Страница завершается включением стандартного файла Footer.asp и парой завершающих тегов.
Листинг 6.22. Products.asp (продолжение)
' We are going to rotate the images from left
' to right.
Flag = 0
' Loop through the products record set
do until rsProducts.EOF
' Retrieve the product information to be displayed.
chrProductName = rsProducts("chrProductName")
chrProductImage = rsProducts("chrProductImage")
idProduct = rsProducts("idProduct")
' Check the display flag. We will rotate the
' product images from left to right.
If flag = 0 then
' Set the flag
flag = 1
%>
<!-- Build the link to the product information. -->
<a href="product.asp?idProduct=<%=idProduct%>">
<img src="images/products/sm_<%=chrProductImage%>"
align="middle" border="0">
<%=chrProductName%></a><BR><BR>
<% else %>
<!-- Build the link to the product information. -->
<a href="product.asp?idProduct=<%=idProduct%>">
<%=chrProductName%>
<img src="images/products/sm_<%=chrProductImage%>"
align="middle" border="0"></a><BR><BR>
<%
' Reset the flag
Flag = 0
end if
' Move to the next row
rsproducts.movenext
loop
%>
<!-- #include file="include/footer.asp" -->
</BODY>
</HTML>
На этой странице используются две хранимые процедуры. Первая процедура, sp_RetrieveDept, читает данные раздела согласно переданному идентификатору.
Листинг 6.23. Хранимая процедура sp_RetrieveDept
/* Загрузка информации об одном разделе */
CREATE PROCEDURE sp_RetrieveDept
/* Pass in the ID of the department */
@idDepartment int
AS
/* Select all of the data on the
department */
select * from department
where idDepartment = @idDepartment
Вторая хранимая процедура возвращает список товаров, относящихся к заданному разделу. Для получения списка необходимо связать таблицы Department, DcpartmentProducts и Products. Мы возвращаем все товары, ассоциируемые с заданным идентификатором раздела в таблице DepartmentProducts.
Листинг 6.24. Хранимая процедура sp_RetrieveDeptProducts
/* Загрузка списка товаров, относящихся к заданному разделу */
CREATE PROCEDURE sp_RetrieveDeptProducts
/* При вызове процедуре передается идентификатор раздела */
@idDept int
AS
/* Выборка товаров заданного раздела из таблицы products */
select * from products, departmentproducts
where products.idproduct = departmentproducts.idproduct and
departmentproducts.iddepartment = @idDept
Теперь при щелчке на любом разделе на странице Dept.asp загружается страница Products.asp со списком товаров. Ссылка Departments в списке слева позволяет быстро просмотреть любой раздел.
В приведенном примере раздел состоит из двух товаров: Joe Bob's Thimble Sounds и The Sounds of Silence (for real). Обратите внимание на информацию о разделе, отображаемую наверху справа непосредственно под заголовком страницы.
Если щелкнуть на ссылке Department, можно выбрать новый раздел - например, Crying Westerns. Сохраняется чередование графики и текста на странице.
Товары
На странице Product.asp (см. листинг 6.25) выводятся сведения о товаре, выбранном на странице Products.asp. Именно здесь покупатель получает более подробную информацию и принимает решение о покупке.
Страница начинается стандартно, с включения файла Header.asp и создания объекта подключения к базе данных для чтения информации о товаре.

Листинг 6.25. Product.asp
<%@ Language=VBScript %>
<HTML>
<!-- Product.asp - вывод информации о товаре. -->
<!-- #include file="include/header.asp" -->
<%
' Create an ADO database connection
set dbProduct = server.createobject("adodb.connection")
' Create a record set
set rsProduct = server.CreateObject("adodb.recordset")
' Open the connection using our ODBC file DSN
dbProduct.open("filedsn=WildWillieCDs")
Команда SQL использует хранимую процедуру sp_RetrieveProduct для чтения данных товара, идентификатор которого передается в URL страницы. Из полученных данных извлекаются основные сведения о товаре - описание, графическое изображение, название, цена и идентификатор товара.
Листинг 6.26. Product.asp (продолжение)
' При вызове хранимой процедуры для получения данных товара
' передается идентификатор товара
sql = "execute sp_RetrieveProduct " & request("idProduct")
' Выполнить команду SQL
set rsProduct = dbProduct.Execute(sql)
' Получить основные сведения о товаре
txtDescription = rsProduct("txtDescription")
chrProductImage = rsProduct("chrProductImage")
chrProductName = rsProduct("chrProductName")
intPrice = rsProduct("intPrice")
idProduct = rsProduct("idProduct")
%>
Тег FORM создает форму, данные которой передаются странице AddItem.asp. На этой странице товар включается в корзину и отслеживается вплоть до окончательного оформления заказа.
Затем мы создаем таблицу для отображения сведений о товаре. Графическое изображение находится в левом столбце, а название, описание и цена товара - в правом столбце. В текстовом поле пользователь вводит количество приобретаемых единиц товара.
Обратите внимание на скрытые поля. Они предназначены для получения быстрого доступа к данным корзины со страницы AddItem.asp.
Листинг 6.27. Product.asp (продолжение)
<!-- The additem.asp page will be called to
add the product to the basket -->
<form method="post" action="additem.asp">
<!-- The table will provide the layout
structure for the product. -->
<table border="0" cellpadding="3" cellspacing="3">
<!-- Row to display the product image, name and
description. -->
<TR>
<!-- Display the image -->
<td><img src="images/products/<%=chrProductImage%>"></td>

<!-- Show the product name and description -->
<td valign="top">
<CENTER><b><font size="5"><%=chrProductName%></font></b></center>
<BR><BR>
<%=txtDescription%><BR><BR>
</td>
</TR>
<!-- Show the product price. An input quantity box is
created. Also, several hidden variables will hold
key data for adding the product to the database. -->
<TR>
<TD align="center"><B>Price:
<%=formatcurrency(intPrice/100, 2)%></b>
</td>

<TD align="center">
<B>Quantity:
<input type="text" value="1" name="quantity" size="2"></b>
<input type="hidden" value="<%=idProduct%>" name="idProduct">
<input type="hidden" value="<%=chrProductName%>" name="ProductName">
<input type="hidden" value="<%=intPrice%>" name="ProductPrice">
</td>
</TR>
На следующем шаге необходимо проверить наличие у товара атрибутов. Вероятно, вы помните, что для футболок мы определили атрибуты "размер" и "цвет". Разумеется, структура нашей базы данных поддерживает множество других разновидностей атрибутов.
Загрузка атрибутов осуществляется при помощи хранимой процедуры sp_Attributes. При вызове процедуре передается идентификатор товара. Затем мы проверяем, вернула ли процедура какие-либо атрибуты.
Листинг 6.28. Product.asp (продолжение)
<%
' Создать объект подключения к базе данных
set dbAttributes = server.createobject("adodb.connection")
' Создать набор записей
set rsAttributes = server.CreateObject("adodb.recordset")
' Открыть подключение, используя файловый DSN ODBC
dbAttributes.open("filedsn=WildWillieCDs")
' Execute the stored procedure to retrieve the attributes
' for the products.
sql = "execute sp_Attributes " & request("idProduct")
' Execute the SQL statement
set rsAttributes = dbProduct.Execute(sql)
' Loop through and display the attributes for the product.
if not rsAttributes.EOF then
%>
<TR>
<!-- Color column -->
<TD>
Сначала обрабатывается атрибут "цвет". В форме создается список, содержащий все значения данного атрибута.
ПРИМЕЧАНИЕ
В различных областях бизнеса используются разные единицы учета запасов. Иногда основной единицей является базовый идентификатор товара, а атрибуты всего лишь уточняют данные заказа. Именно такой подход к атрибутам использован в нашем магазине. Однако во многих магазинах в качестве единицы учета запасов используется комбинация идентификатора товара, всех атрибутов и т. д. При хранении атрибутов и данных товара вообще следует принимать во внимание некоторые обстоятельства. Так, при частом изменении ассортимента крайне важно, чтобы информация о ценах, атрибутах и т. д. сохранялась в заказе. Мы не хотим, чтобы цена товара (цвет и т. д.) неожиданно изменилась после оформления заказа, и клиенту пришлось рассчитываться по новой цене. Стандартное решение, использованное в нашем примере, заключается в фиксировании данных товара в момент покупки. При больших объемах товара на складе необходимость в этом может отпасть.

В процессе перебора всех атрибутов товара мы следим за моментом перехода от атрибута "цвет" к атрибуту "размер". Когда это произойдет, мы перейдем к заполнению следующего списка и начнем заполнять его значениями атрибута "размер".
Список размеров заполняется по тем же правилам, что и список цветов. На этом вывод сведений о товаре завершается, и пользователь может сделать свой выбор.
Листинг 6.29. Product.asp (продолжение)
Color:
<!-- Список вариантов цвета -->
<SELECT name="color">

<%

' В цикле перебирать атрибуты.
do until rsAttributes.EOF

' Проверить, не перешли ли мы к другому атрибуту в списке.
if rsAttributes("chrCategoryName") <> "Color" then

' Выйти из цикла
exit do

end if
%>

<!-- Построить очередную строку списка цветов. Атрибут value равен идентификатору цвета-->
<option value="<%=rsAttributes("chrAttributeName")%>">
<%=rsAttributes("chrAttributeName")%>

<%

' Перейти к следующей записи
rsAttributes.MoveNext

loop

%>

</select>
</TD>

<!-- Size column -->
<TD>
Size:
<!-- Start the size select box -->
<SELECT name="size">

<%

' Loop through the size attributes
do until rsAttributes.EOF

%>

<!-- Display the options -->
<option value="<%=rsAttributes("chrAttributeName")%>">
<%=rsAttributes("chrAttributeName")%>

<%

' Move to the next row
rsAttributes.MoveNext
loop

%>

</select>
</TD>
</TR>
<%
end if
%>
Страница завершается кнопкой отправки данных, закрывающим тегом формы, включением файла Footer.asp и тегами конца страницы.
Листинг 6.30. Product.asp (продолжение)
<!-- Вывести кнопку отправки данных -->
<TR>
<td colspan="2" align="center">
<input type="submit" value="Order" name="Submit">
</td>
</tr>
</table>
</form>
<!-- #include file="include/footer.asp" -->
</BODY>
</HTML>
На странице используются две хранимые процедуры. Первая, sp_RetrieveProduct, просто возвращает сведения о товаре с переданным идентификатором.
Листинг 6.31. Хранимая процедура sp_RetrieveProduct
/* Загрузка данных товара */
CREATE PROCEDURE sp_RetrieveProduct
/* При вызове процедуре передается идентификатор товара */
@idProduct int
AS
/* Выборка сведений о товаре */
select * from products
where idProduct = @idProduct
Хранимая процедура sp_Attributes возвращает все атрибуты заданного товара. Для этого мы объединяем четыре таблицы Products, ProductAttribute, Attribute и AttributeCategory. В таблице AttributeCategory хранятся названия категорий Size и Color. Таблица AttributeName определяет названия цветов и размеров, входящих в категории. Таблица ProductAttribute содержит набор комбинаций, связывающих товары с атрибутами.
Данные, полученные при вызове хранимой процедуры, упорядочиваются по названию категории. Это сделано для того, чтобы мы могли последовательно перебирать все атрибуты и завершать построение списка при переходе к следующей категории.
Листинг 6.32. Хранимая процедура sp_Attributes
/* Загрузка атрибутов заданного товара из базы данных */
CREATE PROCEDURE sp_Attributes
/* При вызове процедуре передается идентификатор товара */
@idProduct int
AS
/* Выборка атрибутов товара. */
select products.idproduct,
attribute.idattribute,
attribute.chrattributename,
attributecategory.chrcategoryname,
productattribute.idproductattribute
from products, productattribute, attribute, attributecategory
where
products.idproduct = @idProduct and
productattribute.idproduct = @idProduct and
productattribute.idattribute = attribute.idattribute and
attribute.idattributecategory = attributecategory.idattributecategory
order by chrcategoryname
На этом наша работа над страницей товаров завершается. Страница в первую очередь предназначена для вывода данных товара. Настоящее волшебство начинается в тот момент, когда пользователь решает включить товар в корзину.
 Графическое изображение расположено слева, а текстовая информация - справа. Цена выводится непосредственно под графикой. Чтобы заказать этот товар, покупатель щелкает на кнопке Order, что приводит к загрузке страницы AddItem.asp.  
Товар с атрибутами - симпатичная и неповторимая футболка стоимостью $20. К радости покупателя имеется четыре варианта расцветки и четыре размера. Пользователь выбирает нужное сочетание цвета и размера и включает товар в корзину.
Теперь покупатель располагает базовыми возможностями для перемещения между товарами разделов. Давайте посмотрим, как происходит поиск товаров на сайте.
Поиск
Средства поиска на Web-сайте необходимы для того, чтобы пользователи могли находить товары по своим специфическим критериям. В нашем примере реализованы два основных варианта поиска. Первый - стандартный поиск по ключевым словам. Поиск будет осуществляться в полях имени и описания товара средствами синтаксиса SQL.
Во втором варианте поиска ищутся товары, относящиеся к определенному интервалу цены. Например, можно провести поиск товаров стоимостью от $10 до $20, в название или описание которых входит слово "jazz".
Начало страницы выглядит вполне стандартно (см. листинг 6.33). Впрочем, форма для передачи критериев поиска устроена несколько необычно. Вместо того чтобы отправлять их другой странице для обработки результатов, она отправляет их самой себе, то есть Search.asр. Проверка переданных данных осуществляется в процессе работы страницы.
Листинг 6.33. Search.asp
<%@ Language=VBScript %>
<HTML>
<!--
Search.asp - Provides searching capabilities for finding
products.
-->
<!-- #include file="include/header.asp" -->

<BR>
<!-- Build the search form. Note we post
to this page.
-->
<form method="post" action="search.asp">
Вывод начинается с таблицы, содержащей критерии поиска. Она выводится даже при получении результатов поиска, чтобы после просмотра результатов последнего поиска пользователь мог легко перейти к новому поиску. Обратите внимание - по умолчанию поля заполняются предыдущими данными.
Листинг 6.34. Search.asp (продолжение)
<!-- Таблица для вывода критериев поиска -->
<table border="0">
<!-- Поле для ввода искомого текста -->
<tr>
<td align="right"><b>Enter your search text:</b></td>
<!-- Input text box -->
<td align="right"><input type="text"
value="<%=request("search")%>" name="Search">
</td>
</tr>

<!-- Поиск товаров с ценой в заданном интервале -->
<tr><td><b>Price Range:</b></td>
<td align="right">Low:
<input type="text" value="<%=request("low")%>" name="Low"></td>
</tr>
<!-- Верхняя граница цены -->
<tr><td></td>
<td align="right">High: <input type="text"
value="<%=request("high")%>" name="High"></td>
</tr>
<!-- Промежуток -->
<tr><td colspan="2">&nbsp;</td></tr>
<!-- Кнопка отправки данных -->
<tr><td colspan="2" align="center">
<input type="submit" value="Submit" name="Submit">
</td></tr>

</table>
</form>
Следующий фрагмент проверяет, был ли отправлен странице запрос на проведение поиска. Мы проверяем состояние трех переменных - search (текст), low (нижняя граница цены) и high (верхняя граница цены). Если проверка дает положительный результат, страница открывает подключение к базе данных для проведения поиска.
Листинг 6.35. Search.asp (продолжение)
<%

' Проверить, был ли отправлен странице запрос на проведение поиска.
if request("search") <> "" or _
request("low") <> "" or _
request("high") <> "" then

' Создать объект подключения к базе данных
set dbSearch = server.createobject("adodb.connection")
' Создать объект набора данных
set rsSearch = server.CreateObject("adodb.recordset")
' Открыть подключение, используя файловый DSN ODBC
dbSearch.open("filedsn=WildWillieCDs")
Сначала мы проверяем, содержит ли переменная low какое-либо значение и является ли оно числовой величиной. Если данные отсутствуют, нижняя граница цены устанавливается равной 0. Если переменная содержит число, мы читаем его и умножаем на 100.
Умножение выполняется для сравнения с ценами в базе данных, хранящимися в виде целых чисел.
Листинг 6.36. Search.asp (продолжение)
' Проверить наличие нижней границы и числовой тип ее значения.
If request("Low") = "" or _
isnumeric(request("low")) = false then
' Default to 0
Low = 0
else
' Присвоить переменной введенное число. Поскольку цены хранятся
' в базе в виде целых чисел, значение умножается на 100
Low = request("Low") * 100
end if
Аналогичная проверка выполняется и для верхней границы (переменная high). Если значение отсутствует, переменной присваивается очень большая величина в долларах. Если значение задано, оно, как и в случае с нижней границей, умножается на 100.
Листинг 6.37. Search.asp (продолжение)
' Проверить наличие верхней границы и числовой тип ее значения.
if request("High") = "" or _
isnumeric(request("High")) = false then

' Присвоить очень большое число
High = 99999999

else

' Присвоить значение, умноженное на 100
High = Request("High") * 100

end if
При поиске товаров используется хранимая процедура sp_SearchProducts. Искомый текст, а также нижняя и верхняя граница цены включаются в запрос. Команда SQL применяется к базе данных и возвращает набор данных результата.
Листинг 6.38. Search.asp (продолжение)
'Построить запрос SQL для отбора товаров по заданным критериям.
' Искомый текст и границы интервала цены передаются
' в качестве параметров
sql = "execute sp_SearchProducts '" & _
request("search") & "', " & Low & ", " & High
' Выполнить команду SQL
set rsSearch = dbSearch.Execute(sql)

%>

<!-- Начало списка -->
<UL>
Хранимая процедура sp_SearchProducts получает три параметра: искомый текст, нижнюю границу и верхнюю границу цены. Поиск текста в названии и описании товара в запросе SQL выполняется при помощи конструкции LIKE. Наконец, цена товара сравнивается с нижней и верхней границей интервала.
СОВЕТ
Поиск на Web-сайтах - весьма интересная тема. Обычно на сайтах реализуется два типа поиска. Первый, неструктурированный поиск, поддерживается большинством поисковых систем. Такие инструменты, как Index Server, просматривают файлы содержимого сайта и индексируют ключевые слова в специальной базе данных. Существует специальный язык для запросов к этой базе. Второй тип, поиск в базе данных, обычно реализуется на таких языках, как SQL. Примером является поиск товаров в электронном магазине.

На коммерческом Web-сайте, особенно с большим ассортиментом, поисковая страница может оказаться едва ли не самой посещаемой. Нередко в ход идут нестандартные возможности - например, отслеживание ключевых слов, по которым проводят поиск покупатели, и включение в базу данных специальных полей, повышающих вероятность успешного поиска. Иногда фантазия разработчика доходит до поисковых механизмов с выдачей рекомендаций, как на ряде крупных Web-магазинов (например, Amazon, CD Now, EBWord и других).
В электронных магазинах средствам поиска необходимо уделить особое внимание. Хотя традиционный способ просмотра товаров по разделам весьма важен, не упускайте из виду специфику поиска. Хороший поисковый механизм должен помочь потенциальному покупателю найти именно тот товар, который его интересует.
Листинг 6.39. Хранимая процедура sp_SearchProducts
/* Поиск товаров по переданным параметрам. */
CREATE PROCEDURE sp_SearchProducts
/* При вызове процедуре передается искомый текст, нижняя и верхняя граница цены */
@SearchText varchar(255),
@Low int,
@High int
AS

/* Select products from the data base where the
product name or description contain the search
text. And where the price falls in the given
parameters. The products are ordered by the
product name. */
select * from products
where (chrProductName like '%' + @SearchText+ '%' or
txtDescription like '%' + @SearchText + '%') and
(intPrice >= @low and intPrice <= @High)
order by chrProductName
Поисковая страница нашего сайта. Обратите внимание на три текстовых поля. Для примера введите в первом поле текст jazz, в поле нижней границы - 10, а в поле верхней границы - 20.
Рис. 6.9. Поисковая страница
Результаты поиска. В нашем примере они состоят из одного товара, Alley Jazz. Покупатель щелкает на ссылке и переходит к найденному товару.
Итоги
Страницы, описанные в этой главе, образуют основную инфраструктуру для представления сайта покупателю. Обычно именно на этих страницах происходит большинство всех маркетинговых событий на Web-сайтах. В части 4 "Реклама" основное внимание уделяется продвижению товаров на Web-сайте.
Рис. 6.10. Результаты поиска
Следует обратить внимание на некоторые ключевые концепции, использованные при построении этих страниц. Первая из них - применение включаемых файлов, определяющих общую структуру страницы. Используя эти файлы, мы реализуем структурированный подход к пользовательскому интерфейсу, обеспечивающий его простую модификацию и масштабирование.
Вторая концепция заключается в структурном подходе к построению базы данных. При помощи хранимых процедур мы реализуем всю основную структуру данных разделов и товаров, включая атрибуты товаров и т. д.
В результате у нас появилась платформа, на которой можно построить вторую половину магазина с поддержкой корзины и оформления заказов. В следующей главе мы перейдем к следующей фазе процесса электронной коммерции, а именно к операциям с корзиной.

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г. Яндекс.Метрика