Сергей Супрунов
Что админу хорошо,
то пользователю – смерть.
Народная мудрость
Очень часто системному администратору, работающему в небольшой фирме,
попутно приходится разрабатывать программы для внутренних нужд компании.
Требования к подобному ПО, как правило, невысоки, но это с лихвой
компенсируется очень сжатыми сроками, отводимыми на разработку. Освоить Delphi
для того, чтобы на предприятии появилась программа-каталог пользователей
Интернета или программа, формирующая материальные отчеты, было бы просто
замечательно. Но времени на подобные вещи, как обычно, не хватает. И потому
приходится идти другим путём. В данной статье я хочу рассказать о своем опыте
использования веб-сервера для решения подобных задач.
Почему был выбран столь экзотический способ?
Во-первых, Perl я знаю несколько лучше, чем Delphi или C++ Builder. Во-вторых,
такой подход без лишних усилий позволяет создавать клиент-серверные приложения,
с которыми одновременно могут работать сотни пользователей, не заботясь о разработке
клиентских программ – с этой ролью отлично справится любой браузер. В-третьих,
налицо независимость от конкретных платформ и операционных систем – для сервера
достаточно, чтобы на нем мог работать Apache + Perl (вы можете использовать и
другую связку), от клиентов требуется лишь поддержка какого-нибудь графического
обозревателя. И наконец, времени на разработку и сопровождение ПО в данном
случае затрачивается заметно меньше, чем при «традиционных» способах.
Для наглядности рассмотрим в общих чертах процесс
разработки незатейливого приложения: каталога пользователей ADSL, в который
будут заноситься сведения об абонентах (фамилия, адрес, номер телефона),
параметры конфигурации (IP-адреса, интерфейсы, номера PVC), параметры линии
(длина, диаметр жилы, сопротивление шлейфа) и т. д.
В целях экономии места будет рассмотрена только
первая функция (работа со сведениями об абонентах). Очень многие детали
придется опустить. Так, не будут рассмотрены особенности Perl-модулей,
подключаемых к нашим сценариям, способы работы с базой данных и т. д. Читателю
понадобятся, по крайней мере, базовые знания Perl, HTML, PostgreSQL (или
какой-нибудь другой СУБД). Если материал статьи окажется вам интересен, я
постараюсь разложить все по полочкам в следующих статьях, оставляйте ваши
отзывы на форуме журнала «Системный администратор».
Подготовительные мероприятия
Итак, прежде всего нам нужно собрать сервер. Я остановил
свой выбор (поскольку все это уже есть и работает) на следующем ПО (правда,
кое-что из этого списка уже пора бы обновить):
n ОС: FreeBSD 5.2
n Веб-сервер: Russian Apache 1.3.29
n Язык программирования: Perl 5.6.1
n СУБД: PostgreSQL 7.4.2
Рассматривать установку и конфигурирование всего
этого я не буду – все довольно подробно описано и на страницах журнала, и на бескрайних
просторах Интернета. Выбор именно этого ПО – вопрос, скорее, личных
предпочтений, поскольку по каждому из пунктов можно привести массу как
положительных, так и отрицательных доводов. В конце концов с тем же успехом (с
поправкой на более высокие требования к ресурсам и вопросам безопасности) можно
использовать и связку «Windows2003 – IIS – ASP – MSSQL».
Структура базы данных и доступ
к СУБД
Начнем разработку с определения структуры БД. Нужно заметить, что это
итерационный процесс, то есть обычно при разработке сценариев выясняется, что
база данных должна быть несколько иной, потом корректировки вновь вносятся в
код сценариев, снова исправляется БД и так до тех пор, пока разработчик не
осознает, что эффект от дальнейшего улучшения уже не окупает затрат на
исправления. Но мы остановимся на первой итерации, тем более что наша задача –
показать сам принцип.
Итак, создадим БД с именем adsl,
владельцем которой будет пользователь adsluser с паролем password. В ней нам
потребуются следующие таблицы:
n sessions – информация сеансов пользователей
(см. далее):
n id char(32) – идентификатор сессии;
n a_session text – информация сессии;
n login char(12) – имя пользователя;
n password varchar – пароль пользователя.
n users – информация об абонентах ADSL:
n uid serial – уникальный идентификатор абонента;
n name varchar – фамилия, имя, отчество;
n address varchar – адрес проживания;
n phone char(7) – номер телефона.
n dslam– информация о ADSL-портах:
n uid serial – уникальный идентификатор порта;
n userid numeric – идентификатор подключенного на
порт абонента;
n num char(10) – номер порта (в виде nDSLAM/nBOARD/nPORT);
n vlan numeric(4) – номер VLAN, соответствующей
ADSL-порту;
n vpi numeric(3) – номер VPI;
n vci numeric(3) – номер VCI, присвоенный
клиенту;
n interface char(15) – имя интерфейса, на котором
будет вестись учет трафика;
n ipaddress inet – IP-адрес, сопоставленный с
данным портом.
n lines – характеристики линий связи:
n uid serial – уникальный идентификатор линии;
n portid numeric – идентификатор порта DSLAM, на
который подключена эта линия;
n length numeric(5) – длина линии в метрах;
n diameter numeric(2,1) – диаметр жилы в
миллиметрах;
n impedance numeric(4) – сопротивление шлейфа в
Омах.
И еще одна таблица для хранения
служебной информации:
n st_modules – список функциональных модулей:
n name char(20) – имя модуля;
n description varchar – описание модуля;
n ink varchar – ссылка на сценарий модуля;
n allow char(12)[] – массив, хранящий имена
пользователей, которым позволено работать с данным модулем;
n orderby numeric(2) – данное поле задает порядок
вывода модулей на экран.
В данном случае мы минимально задействуем
расширенные возможности PostgreSQL, что позволит почти ничего не менять при
использовании, например, MySQL.
Шаблон сайта – модульный подход
Поскольку переписывать все сначала при необходимости расширить
функциональность нашего приложения – занятие не очень интересное, применим
модульный подход. Пусть основной сценарий отвечает только за предоставление
доступа к имеющимся функциям, а каждая функция будет реализована отдельным скриптом.
Кроме того, часто используемые операции будем выносить в наш модуль My::Insite.
Выглядеть базовый сценарий будет примерно так:
#!/usr/bin/perl –w
#--------------------------------------------
adsl.cgi
use My::Insite;
# Подключаемся к БД и создаем
объект CGI для работы с HTTP
$dbh = My::Insite->DBConnect('adsl',
'adsluser', 'password');
$cgi = My::Insite->CGIStart();
# Считываем значение
HTTP-параметра «action»
($action = $cgi->param('action'))
or $action = '';
# Выполняем процедуру выхода
if($action eq 'logoff') { &doLogoff;
}
# Процедура авторизации
if($action eq 'logon') {
$savedLogin = $cgi->param('login');
$savedPassword = $cgi->param('password');
# ищем сессию для заявленного
логина
($sSessId, $sPassword) =
$dbh->selectrow_array('
SELECT id, password
FROM sessions WHERE login=?
',
undef, $savedLogin);
# если не нашли – повторный
запрос авторизации
if(!$sSessId) { &doLogon('Failed');
}
# если сессия есть, но пароль
не соответствует введенному, повторный запрос авторизации
if($sPassword ne $savedPassword)
{ &doLogon('Wrong'); }
# Если все нормально –
сохраняем идентификатор сессии в cookie
$cookie = $cgi->cookie(-name
=> 'sessid', -value => $sSessId);
print $cgi->header(-cookie
=> $cookie);
print 'Авторизация
выполнена успешно.';
print " <A href='adsl.cgi'>Продолжить...</A>";
exit;
}
# action не имеет значения,
пытаемся извлечь из cookie идентификатор сессии
$sessId = $cgi->cookie('sessid');
# Если безуспешно – уходим на
авторизацию
if(!$sessId) { &doLogon('First');
}
# Если sessId есть, пытаемся
получить пользователя этой сессии
($sLogin) = $dbh->selectrow_array('
SELECT login FROM sessions
WHERE id=?;
', undef, $sessId);
# Если удачно – открываем
сессию, иначе – на авторизацию
if($sLogin) {
$session = My::Insite->SessOpen($dbh,
$sessId);
} else { &doLogon('Fialed');
}
# Выбираем из БД и выводим на
экран список модулей
print $cgi->header;
$sth = $dbh->prepare('SELECT
* FROM st_modules ORDER BY orderby;');
$sth->execute;
print "<P align='right'>Вы
вошли под именем $sLogin | ";
print ' <A href="?action=logoff">Выход</A></P>';
while($rhash = $sth->fetchrow_hashref)
{
# Печатать будем только те
модули, для которых в поле allow есть имя вошедшего пользователя
if($$rhash{allow} =~ m($sLogin))
{
print
"<DT><A href='$$rhash{link}'> $$rhash{name}</A>";
print
"<DD>$$rhash{description}<BR>";
}
}
# Все закрываем (в принципе
это не обязательно – все и так закроется)
My::Insite->SessClose($session);
My::Insite->DBDisconnect($dbh);
exit;
#--------------------------------------------
подпрограммы
sub doLogon { # подпрограмма
авторизации
$status = shift @_;
if($status eq 'Wrong') {
$status =
Неправильный логин или пароль.';
} elsif($status eq 'Failed')
{
$status = Ошибка
подключения данного пользователя.';
} else {
$status = Введите логин
и пароль:';
}
print $cgi->header();
print <<__HTML__;
<CENTER><H3>$status</H3><FORM
method="POST">
<TABLE border="1"><TR><TD><TABLE>
<INPUT type="hidden"
name="action" value="logon">
<TR><TD>Login:<TD><INPUT
type="text" name="login" value="">
<TR><TD>Password:
<TD><INPUT type="password"
name="password" value="">
<TR><TD colspan="2"
align="center">
<INPUT type="submit"
value="Войти">
</TABLE></TABLE></FORM></CENTER>
__HTML__
exit;
}
sub doLogoff { #
подпрограмма закрытия сеанса
# Записываем cookie с
истекшим «сроком годности» (отрицательное значение параметра expire), что
уничтожит cookie в памяти
$cookie = $cgi->cookie(-name
=> 'sessid',
-value
=> '',
-expires
=> '-1d');
print $cgi->header(-cookie
=> $cookie);
print '<HEADER>';
print '<META http-equiv="refresh"
content="1;url=adsl.cgi">';
print '</HEADER>';
print 'До новых встреч!';
exit;
}
Задача данного сценария – выполнить авторизацию
пользователя и предоставить ему список доступных для работы модулей. Управление
поведением сценария осуществляется с помощью переменной «action», которая может
иметь одно из следующих значений: logoff (закрыть сеанс), logon (выполнить
процедуру авторизации, в ходе которой проверяется правильность пароля и
открывается сессия, соответствующая данному пользователю, о чем делается запись
в файлах cookie). Пустое значение данной переменной позволит вывести на экран
перечень доступных модулей.
Список модулей хранится в БД, в таблице st_modules,
откуда он выбирается и выводится на экран, причем отображаются только те
модули, для которых в поле allow содержится имя текущего пользователя. Больше
ничего от главного сценария не требуется. Для подключения к приложению
очередного модуля достаточно поместить в папку cgi-bin реализующий его сценарий
и добавить запись в таблицу st_modules. То есть в этой таблице будет что-то
похожее:
adsl=> select link,
name, allow from st_modules order by orderby;
link |
name | allow
---------------------+------------------------------------------+----------------------
adsl-users.cgi | Абоненты ADSL |
{"admin","operator"}
adsl-dslam.cgi |
Конфигурация DSLAM | {"admin"}
adsl-admin.cgi |
Модуль администратора | {"admin"}
(записей: 3)
Результат работы сценария adsl.cgi представлен на
рисунках 1 и 2.

Рисунок 1

Рисунок 2
Модуль My::Insite в моем случае будет размещаться
по такому адресу: /usr/local/lib/perl5/site_perl/5.6.1/My/Insite.pm. Узнать
пути, по которым Perl ищет подключаемые модули, позволяет специальная
переменная @INC:
#!/usr/bin/perl
#--------------------
testpath.pl
$, = "\n";
print @INC;
exit;
На моей машине результат был получен следующий:
$ ./testpath.pl
/usr/local/lib/perl5/site_perl/5.6.1/mach
/usr/local/lib/perl5/site_perl/5.6.1
/usr/local/lib/perl5/site_perl
/usr/local/lib/perl5/5.6.1/BSDPAN
/usr/local/lib/perl5/5.6.1/mach
/usr/local/lib/perl5/5.6.1
Код модуля My::Insite представлен ниже:
package My::Insite;
use CGI;
use DBI;
use Apache::Session::Postgres;
sub CGIStart {
return CGI->new;
}
sub DBConnect {
my($obj, $dbName, $dbUser,
$dbPwd) = @_;
return DBI->connect('dbi:Pg:dbname='.$dbName,
$dbUser, $dbPwd);
}
sub DBDisconnect {
my($obj, $dbh) = @_;
$dbh->disconnect;
return(1);
}
sub SessOpen {
my($obj, $dbh, $sessId) =
@_;
tie %session, 'Apache::Session::Postgres',
$sessId,
{Handle
=> $dbh, LockHandle => $dbh};
return(bless(\%session, $obj));
}
sub SessClose {
my($obj, $session) = @_;
untie(%$session);
return(1);
}
return(1);
Как видите, сюда вынесены функции подключения к
БД, работы с сессиями и т. д.
Может показаться, что в некоторых функциях нет
смысла. Например, зачем создавать CGIStart, которая только и делает, что
вызывает функцию new() модуля CGI? Не проще ли вызывать эту функцию самому и не
захламлять модуль?
А теперь представьте, что вы решили вместо модуля
CGI перейти на более функциональный. Что проще – переписывать все имеющиеся
сценарии или изменить одну функцию в My::Insite?
Думаю, в этом модуле все понятно без комментариев.
Если что непонятно – всегда под рукой man DBI, man CGI, man Apache::Session.
Доступ на сайт и Apache::Session
Если вы доверяете всем сотрудникам или собираетесь ограничивать доступ к
сайту «низкоуровневыми» средствами вроде брандмауэра для ограниченного круга
лиц с равными правами, то этот пункт можно пропустить. В общем же случае
желательно организовать проверку «подлинности» пользователя и соответствующим
образом ограничивать его права в нашей программе. В серьезных случаях можно
дополнительно организовать SSL-шифрование, но сейчас обойдемся без этого, чтобы
не отвлекаться от основной цели.
Пароли для простоты хранить и передавать будем в
явном виде, признак правильного входа в приложение, а заодно и некоторые
персональные настройки будем хранить, используя механизм сессий. В Perl это
выглядит несколько сложнее, чем в PHP, зато проще сделать именно то, что нужно.
Для работы понадобится модуль Apache::Session. Если на вашей системе такого
нет, для FreeBSD его, как и большинство других модулей, можно установить из
коллекции портов:
# cd /usr/ports/www/p5-Apache-Session
# make install
Более универсальный путь, пригодный практически
для всех систем – использование архива CPAN. Этот метод описан на страницах
руководства man perlmodinstall.
Так как база данных у нас есть, целесообразно для
хранения сессионной информации использовать именно ее. Поэтому будем
использовать подмодуль Apache::Session:: Postgres. Поскольку число
пользователей нашего приложения ограничено и все они известны, то имеет смысл
для каждого из них заранее создать сессию, в которой будут храниться все
пользовательские данные, и при авторизации подключать именно ее. Такой подход
позволит не беспокоиться об удалении старых сессий и о хранении идентификатора
сессии между сеансами. Вручную создавать новую сессию не очень удобно, поэтому
будем использовать такой небольшой сценарий:
#!/usr/bin/perl –w
#------------------------------------------
adsl-adduser.pl
use DBI;
use Apache::Session::Postgres;
$login = $ARGV[0]; #
первый аргумент – имя
$password = $ARGV[1]; #
второй – пароль
# Запрашиваем все, что не
передано в аргументах
if(!$login) {
print 'Enter login: ';
chomp($login = <>);
}
if(!$password) {
print 'Enter password: ';
chomp($password =
<>);
}
# Создаем новую сессию и сразу
закрываем
$dbh = DBI->connect('dbi:Pg:dbname=adsl',
'adsluser', 'password');
tie %session, 'Apache::Session::Postgres',
undef,
{Handle =>
$dbh, LockHandle => $dbh};
untie %session;
# В запись в таблице sessions,
соответствующей нашему сеансу, добавляем имя пользователя и пароль, введенные
выше
$pre = $dbh->prepare('
update sessions
set login
= ?, password = ?
where login
is null;
');
$pre->execute($login, $password);
$dbh->disconnect;
exit;
При желании этот модуль можно сделать CGI-скриптом
и организовать доступ к нему через модуль администрирования, подключаемый к
нашему приложению, как и все остальные.
Ну и раз информация сессий будет необходима нам в
каждом модуле, то процедуры работы с ней вынесены в наш модуль My::Insite. Как
все это будет работать – смотрите в листингах, приведенных в статье.
Взаимодействие с БД
Для работы с базой данных будем использовать Perl-модуль DBI с драйвером DBD::Pg.
Данный модуль и нужный драйвер можно установить как из портов, так и из CPAN.
Функции открытия и закрытия соединения вынесены в модуль My::Insite, остальное
смотрите в коде конкретных модулей.
Модуль обработки информации об
абоненте
Вот мы и добрались до первого «рабочего» модуля. В его рамках нам нужно
решить следующие задачи: вывод на экран списка абонентов, ввод нового абонента,
удаление абонента, изменение данных.
Код модуля следующий:
#!/usr/bin/perl –w
#------------------------------------
adsl-users.cgi
use My::Insite;
$dbh = My::Insite->DBConnect('adsl',
'adsluser', 'password');
$cgi = My::Insite->CGIStart();
$action = $cgi->param('action');
# Пытаемся считать из cookie
идентификатор сессии, если безуспешно – отправляем на авторизацию
$sessId = $cgi->cookie('sessid');
if(!$sessId) {
&toLogon;
}
# Открываем сессию, или на авторизацию
в случае ошибки
($sLogin) = $dbh->selectrow_array('
SELECT login FROM sessions
WHERE id=?;
', undef,
$sessId);
if($sLogin) {
$session = My::Insite->SessOpen($dbh,
$sessId);
} else {
&toLogon;
}
# Проверяем, можно ли данному
пользователю работать с этим модулем
($allow) = $dbh->selectrow_array('
SELECT allow FROM st_modules
WHERE link=?;',
undef, 'adsl-users.cgi');
if($allow !~ m($sLogin)) {
&toLogon;
}
print $cgi->header;
# Разбираем возможные
действия
if ($action eq '' )
{ &showUsers; }
elsif($action eq 'user'
) { &userForm; }
elsif($action eq 'changeuser')
{ &changeUser; }
else { print 'Не могу
выполнить: '.$action; }
My::Insite->SessClose($session);
My::Insite->DBDisconnect($dbh);
exit;
#-------------------------------------
subroutines
sub showUsers { #
подпрограмма вывода списка абонентов
$sth = $dbh->prepare('SELECT
uid, name, address, phone
FROM
users ORDER BY name;');
$sth->execute;
print
'<TABLE><TR><TD><H3>Абоненты</H3>';
print '<TD align="right"><A
href="adsl.cgi">Главная</A>';
print '<TR><TD colspan="2">';
print
'<TABLE><TR><TD align="right">';
print '[ <A href="?action=user&type=add">
Добавить
нового абонента</A> ]';
print
'<TR><TD><TABLE border=1><TR bgcolor=#AAAAFF>
<TH>Абонент
<TH>Адрес
<TH>Телефон
<TH>Действие;
while(@res = $sth->fetchrow_array)
{
$oper = ($res[6] eq 'I'?'inlager':'outlager');
print
"<TR><TD>$res[1]<TD>$res[2]<TD>$res[3]<TD>
[ <A href='?action=user&type=update&uid=$res[0]
'>Изменить</A> ] ::
[ <A href='?action=user&type=delete&uid=$res[0]
'>Удалить</A> ]";
}
print
'</TABLE><TR><TD align="right">';
print '[ <A href="?action=user&type=add">Добавить
нового абонента</A> ]';
print
'</TABLE></TABLE>';
return;
}
sub userForm { # выводит
форму для манипуляций с данными
$uid = $cgi->param('uid');
$type = $cgi->param('type');
if($type eq 'add') { $submitName
= 'Добавить'; }
elsif($type eq 'delete')
{ $submitName = 'Удалить'; }
elsif($type eq 'update')
{ $submitName = 'Изменить'; }
else {
print 'Ошибка
операции: '.$type;
exit;
}
$header = $submitName.'
абонента:';
if($type ne 'add') {
($name, $address, $phone)
= $dbh->selectrow_array('
SELECT name, address,
phone FROM users WHERE uid = ?;
', undef, $uid);
} else {
$name = $address = $phone
= '';
}
if($type eq 'delete') {
$in1 =
"<B>$name</B>";
$in2 =
"<B>$address</B>";
$in3 =
"<B>$phone</B>";
} else {
$in1 =
"<INPUT type='text' name='name' value='$name' size='35'>";
$in2 = "<INPUT type='text'
name='address' value='$address' size='35'>";
$in3 = "<INPUT type='text'
name='phone' value='$phone' size='7'>";
}
print <<__HTML__;
<CENTER><H3>$header</H3>
<FORM method="GET">
<TABLE border="1"><TR><TD><TABLE>
<INPUT type="hidden"
name="action" value="changeuser">
<INPUT type="hidden"
name="type" value="$type">
<INPUT type="hidden"
name="uid" value="$uid">
<TR><TD>Абонент:
<TD>$in1
<TR><TD>Адрес:
<TD>$in2
<TR><TD>Телефон:
<TD>$in3
<TR><TD colspan="2"><HR>
<TR><TD>[ <A href="adsl-users.cgi?action=">Отмена</A>
]
<TD align="right"><INPUT
type="submit" value="$submitName">
</TABLE></TABLE></FORM></CENTER>
__HTML__
exit;
}
sub changeUser { # запись
изменений в БД
$type = $cgi->param('type');
$uid = $cgi->param('uid');
$name = $cgi->param('name');
$address = $cgi->param('address');
$phone = $cgi->param('phone');
if($type eq 'add') {
$res = $dbh->do('
INSERT INTO users(name,
address, phone)
VALUES(?, ?,
?);',
undef, $name, $address,
$phone);
} elsif($type eq 'delete')
{
$res = $dbh->do('DELETE
FROM users WHERE uid=?;', undef, $uid);
} elsif($type eq 'update')
{
$res = $dbh->do('
UPDATE users SET name=?,
address=?, phone=? WHERE uid=?;
', undef, $name, $address,
$phone, $uid);
} else { print 'Ошибка
операции: '.$type; }
if($res) { print
'Операция выполнена успешно. '};
print '<BR><A href="?action=">Продолжить...</A>';
exit;
}
sub toLogon {
print $cgi->header;
print '<META http-equiv="refresh"
content="1;url=adsl.cgi?account=logon">';
print 'Ошибка входа.
Перенаправление...';
exit;
}
В данном случае для управления поведением
сценария используется еще одна переменная – type. Если action определяет, на
какую подпрограмму следует передавать управление, то type содержит информацию о
том, что именно следует делать в данной подпрограмме.
Сгенерированный приложением список абонентов
имеет вид, представленный на рисунке 3. Рисунок 4 демонстрирует форму для
изменения данных.

Рисунок 3

Рисунок 4
Прочие модули
Поскольку другие модули ничего нового и
интересного в себе не содержат, отличаясь от приведенного лишь именами и
количеством полей, а также некоторыми интерфейсными особенностями, то и тратить
время на их рассмотрение не будем. Добавив соответствующую запись в таблицу st_modules,
вы сделаете новый модуль доступным для работы.
Что можно изменить?
Как известно, нет предела совершенству. Рассмотренный здесь пример был очень
сильно урезан и упрощен, чтобы за деталями не потерялась суть и чтобы уложиться
в рамки журнальной статьи. Однако, разрабатывая реальное приложение, имеет
смысл сделать некоторые улучшения.
Например, «шаблонность» нашего приложения
оставляет желать лучшего. Каждый добавляемый модуль в принципе имеет очень
схожую структуру и функциональность. То есть можно разработать один
модуль-шаблон и настраивать его под конкретные таблицы и поля автоматически в
процессе обращения к конкретной функции.
Можно сделать более гибкой систему разграничения
доступа, помимо пользователей введя понятие групп пользователей, а также
разграничивая права пользователей в пределах одного модуля (полный доступ,
только чтение).
Механизм сессий используется очень слабо.
Например, его можно использовать для передачи таких параметров как
идентификатор пользователя (uid), вместо того чтобы делать это с помощью
скрытых полей формы. Не совсем удобной выглядит необходимость в каждом модуле
задавать логин и пароль для подключения к БД. Выносить это в модуль My::Insite
неправильно (иначе будут сложности с использованием данного модуля в других
приложениях для подключения к другим базам), а вот сделать что-то типа
конфигурационного файла и брать нужные данные оттуда было бы намного лучше,
поскольку в случае смены имени или пароля корректировка потребуется только в
одном месте. В существенном улучшении нуждается проверка корректности вводимых
данных, контроль ошибок, и т. д. В реальной жизни этим, конечно же,
пренебрегать нельзя.
И вообще, можно сделать более удобный и красивый
дизайн, добавить страничкам «динамизм» с помощью JavaScript (например,
всплывающие подсказки, предупреждения и т. п.) и много еще чего хорошего и
полезного.
Заключение
Ну что ж. Надеюсь, полученный результат хотя бы частично соответствует нашим
ожиданиям, несмотря на множество недоработок, оставленных «за бортом». Мы
получили гибкое, легко модифицируемое приложение, соответствующее большинству
наших требований. В будущем его без труда можно расширить, добавив, например,
модуль для работы с жалобами абонентов, для сбора статистики по потребленному
трафику и оплатам и т. д. Единственное, чего мне в данный момент не хватает,
это красивых отчетов, которые не стыдно было бы распечатать, сохранить в файл,
отправить по электронной почте. Эта задача тоже решается довольно просто. Но об
этом – в следующей статье.