当我们 "上次": 离开我们无畏的 web 开发者的时候,他已经成功地用 11 行代码建立了一个在线销售目录。然而现在,他必须接着将这变成一个带有购物车以及相关组件的销售站点。是时候该看些关于我们上星期谈及的可扩展性了;不幸的是,这意谓着我们必须再多写一些代码(事情总不是十全十美的对吧)。
我是谁?
为了把购物车加入站点,我们需要介绍一下当前用户的概念。这将会允许站点的访问者登录并且拥有他们自己的购物车。我们将把二个新的表加入数据库,一个表储存关于用户的详情,一个表代表购物车。我们的表看起来会是这样:
CREATE TABLE user (
id int not null auto_increment primary key,
first_name varchar(64),
last_name varchar(64),
email varchar(255),
password varchar(64),
address1 varchar(255),
address2 varchar(255),
state varchar(255),
postal_code varchar(64),
country varchar(64)
);
CREATE TABLE cart_item (
id int not null auto_increment primary key,
user int,
item int
);
像以前一样,Maypole 会自动为表创建类。我们使用 Class::DBI 的自动关联能力来让 Maypole 处理这些新增的表:
ISellIt::User->has_many( "cart_items" => "ISellIt::BasketItem");
ISellIt::BasketItem->has_a( "user" => "ISellit::User" );
ISellIt::BasketItem->has_a( "item" => "ISellit::Product" );
我们现在需要一个方法把有关当前用户的信息告诉我们的应用程序。在 Maypole 文档中,关于 Maypole 的认证系统有一段很长的说明,但是引入当前用户概念最容易的方法之一是使用 Maypole::Authentication::UserSessionCookie 模块。
顾名思义,这个模块把每个用户和一个 session 相关联,并给用户的浏览器发送一个 cookie。同时也负责确认用户的登录凭证,默认方法是在一张数据库表中查找用户名和密码;这正是我们想要的!
Maypole 提供一个 authenticate 方法给我们重载,而且在这里我们会拦截任何一个需要合法用户身份的请求,比如查看购物车,把物品加入订单等等。
sub authenticate {
my ($self, $r) = @_;
unless ($r->{table} eq "cart" or $r->{action} eq "buy") {
return OK;
}
# Else we need a user
$r->get_user;
if (!$r->{user}) {
$r->template("login");
}
return OK;
}
UserSessionCookie 模块提供的一个 get_user 方法,它负责所有对 cookie 和登陆凭证 进行设置的工作。我们唯一需要做的就是告诉它,我们想用用户的电子邮件地址和密码作为登录凭证,而不是一些随意的用户名。我们可以在我们的应用程序配置中对这些进行指定,如同 UserSessionCookie 文档中所述:
ISellIt->{config}->{auth}->{user_field} = "email";
下一步,我们建立一个登录模板,用来显示一个表单给用户提交他们的凭证;在 Maypole 手册 Request 章节中就有一个,我们可以加以修改以适合我们的需要:
[% INCLUDE header %]
<h2> You need to log in before buying anything </h2>
<DIV class="login">
[% IF login_error %]
<FONT COLOR="#FF0000"> [% login_error %] </FONT>
[% END %]
<FORM ACTION="/[% request.path%]" METHOD="post">
Email Address:
<INPUT TYPE="text" NAME="email"> <BR>
Password: <INPUT TYPE="password" NAME="password"> <BR>
<INPUT TYPE="submit">
</FORM>
</DIV>
那么现在登录的问题解决了;如果一个用户提交了正确的凭证,get_user 将会把用户的 ISellIt::User 对象作为 $r->{user} 放进 Maypole 请求对象中, 然后继续处理用户的请求。
因为现在我们有了一个可用的用户对象,当然就可以在其他地方使用这个用户的信息:
[% IF request.user %]
<DIV class="messages">
Welcome back, [% request.user.first_name %]!
</DIV>
[% END %]
由于我们会在很多地方提及这个用户,所以我们把它作为一个额外的参数 my 传给模板。Maypole 有一个很牛的 hook 方法 additional_data,非常适合用来做这个。
sub additional_data {
my $r = shift;
$r->{template_args}{my} = $r->{user};
}
我们给它取名为 my ,举例来说我们可以这样调用它:
<DIV class="messages">
Welcome back, [% my.first_name %]!
</DIV>
所以现在我们有一个用户。我们可以增加一个新的动作 order,把一个物品加入用户的购物车:
package ISellIt::Product;
sub order :Exported {
my ($self, $r, $product) = @_;
$r->{user}->add_to_cart_items({ item => $product });
$r->{template} = "view";
}
这会在 cart_item 表里添加一个条目,把物品和用户关联起来,然后让我们返回先前的页面来查看物品。
我们已经把我们的用户送回了先前的页面,但没有提示说我们刚才确实把一个物品加入了他的购物车;我们能通过把信息传进模板来给出这样的提示:
sub order :Exported {
my ($self, $r, $product) = @_;
$r->{user}->add_to_cart_items({ item => $product });
$r->{template} = "view";
$r->{template_args}{bought} = 1;
}
然后显示它:
[% IF bought %]
<DIV class="messages">
We've just added this item to your shopping cart. To complete
your transaction, please <A HREF="/user/view_cart">view your
cart</A> and check out.
</DIV>
[% END %]
那么,现在我们得允许用户查看购物车。
显示购物车
这也被证明了是相当简单的(Maypole 中的大多数事物都是)只需要调用用户类的一个动作。我们需要把用户购物车的物品加入我们的 Maypole 请求对象中:
package ISellIt::User;
sub view_cart :Exported {
my ($self, $r) = @_;
$r->{objects} = [ $r->{user}->cart_items ];
}
然后我们需要制作一个显示他们的 user/view_cart 模板:
[% PROCESS header %]
<h2> Your Shopping Cart </h2>
<TABLE>
<TR> <TH> Product </TH> <TH> Price </TH> </TR>
[% SET count = 0;
FOR item = objects;
SET count = count + 1;
"<tr";
' class="alternate"' IF count % 2;
">";
%]
<TD> [% item.product.name %] </TD>
<TD> [% item.product.price %] </TD>
<TD>
<FORM ACTION="/cart_item/delete/[% item.id %]">
<INPUT TYPE="submit" VALUE="Remove from cart">
</FORM>
</TD>
</tr>
[% END %]
</TABLE>
<A HREF="/user/checkout"> Check out! </A>
再次重申,这里的 HTML 代码写得并不好,但它也提供给了一些东西使得我们能把它交给设计人员进行很好地设计。现在该对购物车结帐了……
结帐
构建一个电子商务应用程序最困难的部份就是与付款和信用卡结算服务的交互。我们将使用 "Business::OnlinePayment":http://search.cpan.org/perldoc?Business::OnlinePayment 来处理这方面的事情,处理结算订单则是简单地发一封电子邮件。
真正的结算页面只需要收集信用卡和送货信息,所以实际上它不需要任何对象;事实上我们接下来唯一需要的对象是 ISellIt::User ,它已经由认证过程纳入请求对象中。无论如何我们的确需要显示物品总费用,所以,为了使事情变得更简单,我们会添加一个动作用 Perl 来计算它。我们为用户的总费用建立一个方法,以备后用:
package ISellIt::User;
use List::Util qw(sum);
sub basket_cost {
my $self = shift;
sum map { $_->item->price }
$self->basket_items
}
再定义 checkout 把总额加入我们的模板:
sub checkout :Exported {
my ($self, $r) = @_;
$r->{template_args}{total_cost} = $r->{user}->basket_cost;
}
现在我们写我们的 user/checkout 模板:
[% PROCESS header %]
<h2> Check out </h2>
<p> Please enter your credit card and delivery details. </p>
<form method="post" action="https://www.isellit.com/user/do_checkout">
<P>
First name: <input name="first_name" value="[% my.first_name %]"><BR>
Last name: <input name="last_name" value="[% my.last_name %]"></P>
<P>
Street address: <input name="address" value="[% my.address1 %]"><BR>
City: <input name="city" value="[% my.address2 %]"><BR>
State: <input name="state" value="[% my.state %]">
Zip: <input name="zip" value="[% my.postal_code %]">
</P>
<P>
Card type: <select name="type">
<option>Visa</option>
<option>Mastercard</option>
...
</select>
Card number: <input name="card_number">
Expiration: <input name="expiration"> <BR>
Total: $ [% total_price %]
</P>
<P>
Please click <B>once</B> and wait for the payment to be
authorised.... <input type="submit" value="order">
</form>
当这些数据被提交给 do_checkout 动作时会发生什么呢?(你会注意到这是通过 SSL 连接的)。我们将会先检查用户是否已经输入地址详情,而且如果是的话,把它们存进数据库。也许这在浏览器能自动填写表单的今天显得没什么必要,不过仍不失为一项便利的措施。Maypole 把 POST 过来的参数储存在 params 中:
sub do_checkout :Exported {
my ($self, $r) = @_;
my %params = %{$r->{params}};
my $user = $r->{user};
$user->address1($params{address}) unless $user->address1;
$user->address2($params{city}) unless $user->address2;
$user->state($params{state}) unless $user->state;
$user->postal_code($params{zip}) unless $user->postal_code;
我们需要借助 Business::OnlinePayment 构造一个发送给外部支付网关的请求;太感谢了,我们接收到的表单参数将正好是 OnlinePayment 需要的格式,感谢这么体贴的表单设计。所有我们需要做的就是插入我们的帐户明细和总额:
my $tx = new Business::OnlinePayment("TCLink");
$tx->content(%params,
type => "cc",
login => VENDOR_LOGIN,
password => VENDOR_PASSWORD,
action => 'Normal Authorization'
amount => $r->{user}->basket_total
);
现在我们能提交账单,然后看有什么发生。如果有问题,我们会把一个消息加入模板,而且再一次把用户送回先前的页面:
$tx->submit;
if (!$tx->is_success) {
$r->{template_args}{message} =
"There was a problem authorizing your transaction: ".
$tx->error_message;
$r->{template} = "checkout";
return;
}
没有问题的话,我们就能得到支付给我们的钱;现在我们得把它告诉发货员,否则我们会很快失去客户:
fulfill_order(
address_details => $r->{params},
order_details => [ map { $_->item } $r->{user}->cart_items ],
cc_auth => $tx->authorization
);
那么现在我们清空购物车,把用户送上路:
$_->delete for $r->{user}->cart_items;
$r->{template} = "frontpage";
}
完成!我们已带用户完成了登陆,把商品加入购物车,验证信用卡及结帐的过程。但是……稍等。起初我们怎样得到用户呢?
注册用户
我们必须找一个方法来让用户注册。实际上这并不怎么难,特别是因为我们能使用 Maypole 手册中 Flox 的例子。首先,我们将会把一个 "注册" 链接加入我们的登录模板:
<P>New user? <A HREF="/user/register">Sign up!</A></P>
这页不需要载入任何对象,因为它只是用来显示一个注册表单;我们只要把我们的模板加入 /user/register :
[% INCLUDE header %]
<P>Welcome to buying with iSellIt!</P>
<P>To set up your account, we only need a few details from you:
</P>
<FORM METHOD="POST" ACTION="/user/do_register">
<P>Your name:
<input name="first_name">
<input name="last_name"> </P>
<P>Your email address: <input name="email"> </P>
<P>Please choose a password: <input name="password"> </P>
<input type="submit" name="Register" value="Register">
</FORM>
像以前一样,我们需要向 "Class::DBI::FromCGI" 。解释这些字段的编辑属性(也就是这些字段对应的检查方式):
ISellIt::User->untaint_columns(
printable => [qw/first_name last_name password/],
email => [qw/email/],
);
现在我们能写我们的 do_register 事件了,使用 FromCGI 的形式:
sub do_register :Exported {
my ($self, $r) = @_;
my $h = CGI::Untaint->new(%{$r->{params}});
my $user = $self->create_from_cgi($h);
如果还有问题,我们把他们重新送回注册表单:
if (my %errors = $obj->cgi_update_errors) {
$r->{template_args}{cgi_params} = $r->{params};
$r->{template_args}{errors} = \%errors;
$r->{template} = "register";
return;
}
否则,我们现在就有了一个用户;我们就需要发送 cookie 给该用户,使他们保持正常登陆的状态。这是 UserSessionCookie 再一次给我们方便的地方了:
$r->{user} = $user;
$r->login_user($user->id);
最后我们再一次把用户送上路:
$r->{template} = "frontpage";
}
至此:现在我们可以创建新的用户;提供一个找回密码功能就作为一个练习留给感兴趣的读者。
Maypole 摘要
现在我们已经完成了——在一个很短的时间里,用最少量的代码,我们已经创建了一个在线商店。我喜欢 Maypole 的原因之一就是你只需要专心编写处理商业逻辑流程的代码就行;所有其他的显示模板都可以“借鉴”一下别人的,再给专业人士去处理,而且余下的工作都被 Maypole 在幕后魔术般地处理了。
感谢 TPF 基金对 Maypole 的支持,我们现在有了一份详尽且附有一些案例分析(包括这个)的用户手册,以及一个活跃的用户和开发者社区。我希望你很快也会加入!
(http://www.fanqiang.com)