使用Maypole进行快速Web应用开发:第2部分

3/5/2006来源:Perl教程人气:14709

当我们"上次":离开我们无畏的web开发者的时候,他已经成功地用11行代码建立了一个在线销售目录。然而现在,他必须接着将这变成一个带有购物车以及相关组件的销售站点。是时候该看些关于我们上星期谈及的可扩展性了;不幸的是,这意谓着我们必须再多写一些代码(事情总不是十全十美的对吧)。

我是谁?

为了把购物车加入站点,我们需要介绍一下当前用户的概念。这将会允许站点的访问者登录并且拥有他们自己的购物车。我们将把二个新的表加入数据库,一个表储存关于用户的详情,一个表代表购物车。我们的表看起来会是这样:

CREATETABLEuser(idintnotnullauto_incrementPRimarykey,first_namevarchar(64),last_namevarchar(64),emailvarchar(255),passWordvarchar(64),address1varchar(255),address2varchar(255),statevarchar(255),postal_codevarchar(64),countryvarchar(64));CREATETABLEcart_item(idintnotnullauto_incrementprimarykey,userint,itemint);

像以前一样,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方法给我们重载,而且在这里我们会拦截任何一个需要合法用户身份的请求,比如查看购物车,把物品加入订单等等。

subauthenticate{my($self,$r)=@_;unless($r->{table}eq"cart"or$r->{action}eq"buy"){returnOK;}#Elseweneedauser$r->get_user;if(!$r->{user}){$r->template("login");}returnOK;}

UserSessionCookie模块提供的一个get_user方法,它负责所有对cookie和登陆凭证进行设置的工作。我们唯一需要做的就是告诉它,我们想用用户的电子邮件地址和密码作为登录凭证,而不是一些随意的用户名。我们可以在我们的应用程序配置中对这些进行指定,如同UserSessionCookie文档中所述:

ISellIt->{config}->{auth}->{user_field}="email";

下一步,我们建立一个登录模板,用来显示一个表单给用户提交他们的凭证;在Maypole手册Request章节中就有一个,我们可以加以修改以适合我们的需要:

[INCLUDEheader]<h2>Youneedtologinbeforebuyinganything</h2><DIVclass="login">[IFlogin_error]<FONTCOLOR="#FF0000">[login_error]</FONT>[END]<FORMACTION="/[request.path]"METHOD="post">EmailAddress:<INPUTTYPE="text"NAME="email"><BR>Password:<INPUTTYPE="password"NAME="password"><BR><INPUTTYPE="submit"></FORM></DIV>

那么现在登录的问题解决了;如果一个用户提交了正确的凭证,get_user将会把用户的ISellIt::User对象作为$r->{user}放进Maypole请求对象中,然后继续处理用户的请求。

因为现在我们有了一个可用的用户对象,当然就可以在其他地方使用这个用户的信息:

[IFrequest.user]<DIVclass="messages">Welcomeback,[request.user.first_name]!</DIV>[END]

由于我们会在很多地方提及这个用户,所以我们把它作为一个额外的参数my传给模板。Maypole有一个很牛的hook方法additional_data,非常适合用来做这个。

subadditional_data{my$r=shift;$r->{template_args}{my}=$r->{user};}

我们给它取名为my,举例来说我们可以这样调用它:

<DIVclass="messages">Welcomeback,[my.first_name]!</DIV>

所以现在我们有一个用户。我们可以增加一个新的动作order,把一个物品加入用户的购物车:

packageISellIt::Product;suborder:Exported{my($self,$r,$product)=@_;$r->{user}->add_to_cart_items({item=>$product});$r->{template}="view";}

这会在cart_item表里添加一个条目,把物品和用户关联起来,然后让我们返回先前的页面来查看物品。

我们已经把我们的用户送回了先前的页面,但没有提示说我们刚才确实把一个物品加入了他的购物车;我们能通过把信息传进模板来给出这样的提示:

suborder:Exported{my($self,$r,$product)=@_;$r->{user}->add_to_cart_items({item=>$product});$r->{template}="view";$r->{template_args}{bought}=1;}

然后显示它:

[IFbought]<DIVclass="messages">We'vejustaddedthisitemtoyourshoppingcart.Tocompleteyourtransaction,please<AHREF="/user/view_cart">viewyourcart</A>andcheckout.</DIV>[END]

那么,现在我们得允许用户查看购物车。

显示购物车

这也被证明了是相当简单的(Maypole中的大多数事物都是)只需要调用用户类的一个动作。我们需要把用户购物车的物品加入我们的Maypole请求对象中:

packageISellIt::User;subview_cart:Exported{my($self,$r)=@_;$r->{objects}=[$r->{user}->cart_items];}

然后我们需要制作一个显示他们的user/view_cart模板:

[PROCESSheader]<h2>YourShoppingCart</h2><TABLE><TR><TH>Product</TH><TH>Price</TH></TR>[SETcount=0;FORitem=objects;SETcount=count 1;"<tr";'class="alternate"'IFcount2;">";]<TD>[item.product.name]</TD><TD>[item.product.price]</TD><TD><FORMACTION="/cart_item/delete/[item.id]"><INPUTTYPE="submit"VALUE="Removefromcart"></FORM></TD></tr>[END]</TABLE><AHREF="/user/checkout">Checkout!</A>

再次重申,这里的HTML代码写得并不好,但它也提供给了一些东西使得我们能把它交给设计人员进行很好地设计。现在该对购物车结帐了……

结帐

构建一个电子商务应用程序最困难的部份就是与付款和信用卡结算服务的交互。我们将使用"Business::OnlinePayment":http://search.cpan.org/perldoc?Business::OnlinePayment来处理这方面的事情,处理结算订单则是简单地发一封电子邮件。

真正的结算页面只需要收集信用卡和送货信息,所以实际上它不需要任何对象;事实上我们接下来唯一需要的对象是ISellIt::User,它已经由认证过程纳入请求对象中。无论如何我们的确需要显示物品总费用,所以,为了使事情变得更简单,我们会添加一个动作用Perl来计算它。我们为用户的总费用建立一个方法,以备后用:

packageISellIt::User;useList::Utilqw(sum);subbasket_cost{my$self=shift;summap{$_->item->price}$self->basket_items}

再定义checkout把总额加入我们的模板:

subcheckout:Exported{my($self,$r)=@_;$r->{template_args}{total_cost}=$r->{user}->basket_cost;}

现在我们写我们的user/checkout模板:

[PROCESSheader]<h2>Checkout</h2><p>Pleaseenteryourcreditcardanddeliverydetails.</p><formmethod="post"action="https://www.isellit.com/user/do_checkout"><P>Firstname:<inputname="first_name"value="[my.first_name]"><BR>Lastname:<inputname="last_name"value="[my.last_name]"></P><P>Streetaddress:<inputname="address"value="[my.address1]"><BR>City:<inputname="city"value="[my.address2]"><BR>State:<inputname="state"value="[my.state]">Zip:<inputname="zip"value="[my.postal_code]"></P><P>Cardtype:<selectname="type"><option>Visa</option><option>Mastercard</option>...</select>Cardnumber:<inputname="card_number">Expiration:<inputname="expiration"><BR>Total:$[total_price]</P><P>Pleaseclick<B>once</B>andwaitforthepaymenttobeauthorised....<inputtype="submit"value="order"></form>

当这些数据被提交给do_checkout动作时会发生什么呢?(你会注意到这是通过SSL连接的)。我们将会先检查用户是否已经输入地址详情,而且如果是的话,把它们存进数据库。也许这在浏览器能自动填写表单的今天显得没什么必要,不过仍不失为一项便利的措施。Maypole把POST过来的参数储存在params中:

subdo_checkout:Exported{my($self,$r)=@_;myparams={$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=newBusiness::OnlinePayment("TCLink");$tx->content(params,type=>"cc",login=>VENDOR_LOGIN,password=>VENDOR_PASSWORD,action=>'NormalAuthorization'amount=>$r->{user}->basket_total);

现在我们能提交账单,然后看有什么发生。如果有问题,我们会把一个消息加入模板,而且再一次把用户送回先前的页面:

$tx->submit;if(!$tx->is_success){$r->{template_args}{message}="Therewasaproblemauthorizingyourtransaction:".$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);

那么现在我们清空购物车,把用户送上路:

$_->deletefor$r->{user}->cart_items;$r->{template}="frontpage";}

完成!我们已带用户完成了登陆,把商品加入购物车,验证信用卡及结帐的过程。但是……稍等。起初我们怎样得到用户呢?

注册用户

我们必须找一个方法来让用户注册。实际上这并不怎么难,特别是因为我们能使用Maypole手册中Flox的例子。首先,我们将会把一个"注册"链接加入我们的登录模板:

<P>Newuser?<AHREF="/user/register">Signup!</A></P>

这页不需要载入任何对象,因为它只是用来显示一个注册表单;我们只要把我们的模板加入/user/register:

[INCLUDEheader]<P>WelcometobuyingwithiSellIt!</P><P>Tosetupyouraccount,weonlyneedafewdetailsfromyou:</P><FORMMETHOD="POST"ACTION="/user/do_register"><P>Yourname:<inputname="first_name"><inputname="last_name"></P><P>Youremailaddress:<inputname="email"></P><P>Pleasechooseapassword:<inputname="password"></P><inputtype="submit"name="Register"value="Register"></FORM>

像以前一样,我们需要向"Class::DBI::FromCGI"。解释这些字段的编辑属性(也就是这些字段对应的检查方式):

ISellIt::User->untaint_columns(printable=>[qw/first_namelast_namepassword/],email=>[qw/email/],);

现在我们能写我们的do_register事件了,使用FromCGI的形式:

subdo_register:Exported{my($self,$r)=@_;my$h=CGI::Untaint->new({$r->{params}});my$user=$self->create_from_cgi($h);

如果还有问题,我们把他们重新送回注册表单:

if(myerrors=$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的支持,我们现在有了一份详尽且附有一些案例分析(包括这个)的用户手册,以及一个活跃的用户和开发者社区。我希望你很快也会加入!