65.9K
CodeProject 正在变化。 阅读更多。
Home

轻松实现您自己的在线商店

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (23投票s)

2013年5月8日

CPOL

8分钟阅读

viewsIcon

89592

downloadIcon

4

使用 Scavix PHP Web 开发框架轻松构建在线商店。

介绍  

本文介绍如何使用 PHP 中的 Scavix Web 开发框架创建一个在线商店系统。所有代码都托管在 GitHub 存储库中,因此您可以在那里轻松浏览它们。您应该先阅读这篇基础文章,以了解 Scavix Web 开发框架的基础知识。

目标读者    

具有扎实的 PHP、JavaScript 和 SQL 知识的经验丰富的软件开发人员。

任务

“我们需要一个商店,不需要太复杂”

我们经常从客户那里听到这句话,而且我们确信您也知道。幸运的是,我们可以向您展示如何快速使用 Scavix WebFramework 设置一个基本的商店系统。此示例向您展示了基础知识,以便您可以稍后添加商店所需的所有附加功能。这让您可以自由地从非常基本的精简商店代码开始,然后根据您的需求扩展它,添加所有额外的逻辑。

在线商店的要求大多数时候都差不多

  • 产品列表 
  • 产品详情页 
  • 每个产品都有一个标题、副标题、描述、价格和图片。
  • 购物车
  • 简单的后台管理 
  • 不同的支付提供商

所有这些都可以非常快速轻松地完成。当然,还有更多功能,但这几乎是每个商店系统的核心。如果实现了这些基础,那么所有调整都相对容易。 

优秀的商店系统(市面上有大量非常好的商店系统)可以做到这一切,但需要为每个客户进行调整。这些调整可能需要花费很多天的时间,而实现一个轻量级、小型的专用商店可能花费的时间相同甚至更少。  此外,市面上功能齐全的在线商店系统具有大量通常不需要的功能,特别是当您商店的产品很少时。

好吧,我们在这里不讨论其他商店系统的优缺点,而是直接开始...  

数据库模式  

这是很常见的,所以我们将使用一个ERM来说明: 

如您所见,我们将从不包含用户注册开始,只让客户输入他们的地址详细信息。我们还将跳过管理表(用于管理员用户等),暂时只依赖硬编码的管理员凭据。  

当然,对于在线商店来说,这是不可行的,但如前所述,我们想专注于商店的基础知识。 

GitHub 上的示例代码包含一个函数,该函数将确保数据库结构存在,并在SQLite数据库中填充一些示例数据。请注意,上面的图片仅显示了数据库的基本概念,而不是实际结构,所以如果您在代码中发现图片中没有的字段,请不要责怪我们:)。

基本设置    

与使用 Scavix Web 开发框架工作一样,您将需要三个文件:index.phpconfig.php 和(实际上是可选的).htaccess 文件。

  • index.php 包含上述数据库设置代码,但除此之外没有其他您不知道的内容。
  • config.php 设置 Scavix Web 开发框架,
  • .htaccess 文件用于美观的 URL。 

所有这些都在这里详细描述:超快速 PHP 应用程序开发。 

结构  

我们将需要三个控制器:Products、Basket 和 Admin。

当然,如果您想拆分成其他逻辑单元,也可以这样做!

无论如何:会有产品列表、产品详情页、购物车页面和后台管理。从购物车页面,客户可以开始支付流程,这将需要更多页面来收集客户数据。  

 

布局 

我们不会花太多时间来为我们的示例商店创建一个漂亮的布局。只需要一个基础控制器类和几行 CSS 代码就足够了: 

//controller/shopbase.class.php 
<?php
use ScavixWDF\Base\HtmlPage;

class ShopBase extends HtmlPage { /* no code needed here */ }
?> 
// controller/shopbase.tpl.php
<div id="page">
    <div id="navigation">
        <a href="<?=buildQuery('Products')?>">Products</a>
        <a href="<?=buildQuery('Basket')?>">Basket</a>
        <a href="<?=buildQuery('Admin')?>">Administration (normally hidden)</a>
    </div>
    <div id="content">
        <? foreach( $content as $c ) echo $c; ?>    
    </div>
</div> 
// res/shopbase.css
#page { font: 14px normal Verdana,Arial,serif; }
#page > div { width: 960px; margin: auto; }
#navigation { padding-bottom: 10px; border-bottom: 1px solid gray; }
#navigation a
{
	font-size: 18px; 
	font-weight: bold; 
	margin-right: 15px;
}

.product_overview
{
	clear: both;
	margin-top: 25px;
	border-bottom: 1px solid gray;
	height: 75px;
}

.product_overview img
{
	width: 50px;
	float: left;
	margin-right: 10px;
}

.product_overview div
{
	white-space: nowrap;
	overflow: hidden;
}

.product_overview .title { font-weight: bold; }
.product_overview a { float: right; }

.product_basket
{
	clear: both;
}

.product_basket img
{
	width: 30px;
	float: left;
	margin-right: 10px;
}

.basket_total
{
	clear: both;
	text-align: right;
	font-size: 14px; 
	font-weight: bold;
} 

所以现在 ShopBase 只是一个中心基类,所有派生类都将继承相同的布局。当然,通过继承共享通用的 CSS 和程序逻辑总是一个好主意,所以这并不是白费力气。 

看到了那些丑陋的硬编码导航链接了吗?那将是以后要处理的另一项任务。 

可能只会显示给已验证用户链接,或者在购物车为空时隐藏购物车链接。但如前所述:我们不想失去焦点。   

产品   

我们将在此实现两个页面:产品列表和产品详情页。 

// controller/products.class.php
<?php
use ScavixWDF\Base\Template;
use ScavixWDF\JQueryUI\uiMessage;

class Products extends ShopBase
{
    /**
     * Lists all products.
     * @attribute[RequestParam('error','string',false)]
     */
    function Index($error)
    {
        // display error message if given
        if( $error )
            $this->content(uiMessage::Error($error));
        
        // loop thru the products...
        $ds = model_datasource('system');
        foreach( $ds->Query('products')->orderBy('title') as $prod )
        {
            //... and use a template to represent each
            $this->content( Template::Make('product_overview') )
                ->set('title',$prod->title)
                ->set('tagline',$prod->tagline)
                // see config.php where we set up products images folder as resource folder
                ->set('image',resFile($prod->image))
                ->set('link',buildQuery('Products','Details',array('id'=>$prod->id)))
                ;
        }
    }
    
    /**
     * Shows product details
     * @attribute[RequestParam('id','int')]
     */
    function Details($id)
    {
        // check if product really exists
        $ds = model_datasource('system');
        $prod = $ds->Query('products')->eq('id',$id)->current();
        if( !$prod )
            redirect('Products','Index',array('error'=>'Product not found'));
        
        // create a template with product details
        $this->content( Template::Make('product_details') )
            ->set('title',$prod->title)
            ->set('description',$prod->body)
            // see config.php where we set up products images folder as resource folder
            ->set('image',resFile($prod->image))
            ->set('link',buildQuery('Basket','Add',array('id'=>$prod->id)))
            ;
    }
} 

里面没有什么魔法,代码非常直接:'Index' 方法循环遍历数据库中的所有产品,并使用 'product_overview' 模板显示每个产品。如果您不熟悉这一点,请再次查看WDF 基础知识。  

// templates/product_overview.tpl.php
<div class="product_overview">
    <img src="<?=$image?>" alt=""/>
    <div class="title"><?=$title?></div>
    <div class="tagline"><?=$tagline?></div>
    <a href="<?=$link?>">Details...</a>
</div>  

'Details' 方法使用类似的方法,但不循环,而是加载单个产品。它还使用另一个模板文件。  

// templates/product_details.tpl.php
<div class="product_details">
    <img src="<?=$image?>" alt=""/>
    <div class="title"><?=$title?></div>
    <div class="description"><?=$description?></div>
    <a href="<?=$link?>">Add to basket</a>
    <a href="javascript:history.back()">back to listing</a>
</div>  

好吧……就是这样:我们已经实现了完整的​​产品部分。 

产品列表页: 

以及产品的详情页: 

 

购物车 

与市面上几乎所有其他商店一样,我们想要一个购物车。基本原理:客户将产品添加到购物车,然后可以更改购物车中每个产品的数量。将数量减少到零将从购物车中删除该产品。当然,这可以做得更好,但……您已经知道“保持焦点”这句话了:)。 

所以直接回到代码:  

// controller/basket.class.php
<?php
use ScavixWDF\Base\Template;
use ScavixWDF\JQueryUI\uiButton;
use ScavixWDF\JQueryUI\uiMessage;

class Basket extends ShopBase
{
    /**
     * Lists all items in the basket.
     * @attribute[RequestParam('error','string',false)]
     */
    function Index($error)
    {
        // display any given error message
        if( $error )
            $this->content(uiMessage::Error($error));
        
        // prepare basket variable
        if( !isset($_SESSION['basket']) )
            $_SESSION['basket'] = array();
        
        if( count($_SESSION['basket']) == 0 )
            $this->content(uiMessage::Hint('Basket is empty'));
        else
        {
            // list all items in the basket ...
            $ds = model_datasource('system');
            $price_total = 0;
            foreach( $_SESSION['basket'] as $id=>$amount )
            {
                $prod = $ds->Query('products')->eq('id',$id)->current();
                
                //... each using a template
                $this->content( Template::Make('product_basket') )
                    ->set('title',$prod->title)
                    ->set('amount',$amount)
                    ->set('price',$prod->price)
                    // see config.php where we set up
                    // products images folder as resource folder
                    ->set('image',resFile($prod->image))
                    ->set('add',buildQuery('Basket','Add',array('id'=>$prod->id)))
                    ->set('remove',buildQuery('Basket','Remove',array('id'=>$prod->id)))
                    ;
                $price_total += $amount * $prod->price;
            }
            // display total price and the button to go on
            $this->content("<div 
              class='basket_total'>Total price: $price_total</div>");
            $this->content( uiButton::Make("Buy now") )->onclick = 
              "location.href = '".buildQuery('Basket','BuyNow')."'";
        }
    }
    
    /**
     * Adds a product to the basket.
     * @attribute[RequestParam('id','int')]
     */
    function Add($id)
    {
        // check if the product exists
        $ds = model_datasource('system');
        $prod = $ds->Query('products')->eq('id',$id)->current();
        if( !$prod )
            redirect('Basket','Index',array('error'=>'Product not found'));

        // increase the counter for this product
        if( !isset($_SESSION['basket'][$id]) )
            $_SESSION['basket'][$id] = 0;
        $_SESSION['basket'][$id]++;
        redirect('Basket','Index');
    }
    
    /**
     * Removes an item from the basket.
     * @attribute[RequestParam('id','int')]
     */
    function Remove($id)
    {
        // check if the product exists
        $ds = model_datasource('system');
        $prod = $ds->Query('products')->eq('id',$id)->current();
        if( !$prod )
            redirect('Basket','Index',array('error'=>'Product not found'));
        
        // decrease the counter for this product
        if( isset($_SESSION['basket'][$id]) )
            $_SESSION['basket'][$id]--;
        // and unset if no more items left
        if( $_SESSION['basket'][$id] == 0 )
            unset($_SESSION['basket'][$id]);
        redirect('Basket','Index');
    }

    /* full code: https://github.com/ScavixSoftware/WebFramework/blob/master/web/sample_shop/controller/basket.class.php */
} 

同样,'Index' 方法提供了一个列表,这次的来源是一个 SESSION 变量,其中包含产品 ID 及其数量的键值对。 

'Add' 和 'Remove' 方法会增加和减少购物车中产品的数量。 

上面代码的其余部分是在检查产品是否存在以及变量是否存在。 

最后要提一下:我们再次使用一个特殊的模板来显示订单项列表: 

// templates/product_basket.tpl.php
<div class="product_basket">
    <img src="<?=$image?>" alt=""/>
    <span class="title"><?=$title?></span>
    <span class="amount">Amount: <?=$amount?></span>
    <span class="amount">Price: <?=$price?></span>
    <span class="amount">Total: <?=$amount * $price?></span>
    <a href="<?=$add?>">add one more</a>
    <a href="<?=$remove?>">remove one</a>
</div> 

购物车列表/编辑部分就到此为止。结果如下: 

结账

好吧,这实际上是实现商店系统中最有趣的部分:如何获得客户的钱。该示例提供了与PayPalGate2Shop(因为我们过去与他们合作过)的接口,并且可以轻松扩展以支持其他提供商。在开发阶段,还有一个测试支付提供商。 

订单支付的基本流程是:

  • 获取客户的地址数据 
  • 将其与购物车数据一起存储到数据库中
  • 为选定的支付提供商启动结账流程
  • 响应来自提供商的支付消息

复杂吗?不。

// controller/basket.class.php
<?php
use ScavixWDF\Base\Template;
use ScavixWDF\JQueryUI\uiButton;
use ScavixWDF\JQueryUI\uiMessage;

class Basket extends ShopBase
{
    /* full code: https://github.com/ScavixSoftware/WebFramework/blob/master/web/sample_shop/controller/basket.class.php*/
    
    /**
     * Entrypoint for the checkout process.
     * 
     * Requests customers address details and asks for payment processor.
     */
    function BuyNow()
    {
        // displays the chechout form, which has all inputs for address on it
        $this->content( Template::Make('checkout_form') );
    }
    
    /**
     * Persists current basket to the database and starts checkout process.
     * @attribute[RequestParam('fname','string')]
     * @attribute[RequestParam('lname','string')]
     * @attribute[RequestParam('street','string')]
     * @attribute[RequestParam('zip','string')]
     * @attribute[RequestParam('city','string')]
     * @attribute[RequestParam('email','string')]
     * @attribute[RequestParam('provider','string')]
     */
    function StartCheckout($fname,$lname,$street,$zip,$city,$email,$provider)
    {
        if( !$fname || !$lname || !$street || !$zip || !$city || !$email )
            redirect('Basket','Index',array('error'=>'Missing some data'));
        
        // create a new customer. note that we do not check for existance or stuff.
        // this should be part of a real shop system!
        $cust = new SampleCustomer();
        $cust->fname = $fname;
        $cust->lname = $lname;
        $cust->street = $street;
        $cust->zip = $zip;
        $cust->city = $city;
        $cust->email = $email;
        $cust->price_total = 0;
        $cust->Save();

        // create a new order and assign the customer (from above)
        $order = new SampleShopOrder();
        $order->customer_id = $cust->id;
        $order->created = 'now()';
        $order->Save();
        
        // now loop thru the basket-items and add them to the order...
        $ds = model_datasource('system');
        foreach( $_SESSION['basket'] as $id=>$amount )
        {
            //... by creating a dataset for each item
            $prod = $ds->Query('products')->eq('id',$id)->current();
            $item = new SampleShopOrderItem();
            $item->order_id = $order->id;
            $item->price = $prod->price;
            $item->amount = $amount;
            $item->title = $prod->title;
            $item->tagline = $prod->tagline;
            $item->body = $prod->body;
            $item->Save();
            
            $order->price_total += $amount * $prod->price;
        }
        // save the order again to persist the total amount
        $order->Save();
        $_SESSION['basket'] = array();
        
        // finally start the checkout process using the given payment provider
        log_debug("Handing control over to payment provider '$provider'");
        $p = new $provider();
        $p->StartCheckout($order,buildQuery('Basket','PostPayment'));
    }
    
    /**
     * This is the return URL for the payment provider.
     * Will be called when payment raches a final state, so control is handed over to our 
     * app again from the payment processor.
     */
    function PostPayment()
    {
        // we just display the $_REQUEST data for now.
        // in fact this is the point where some processing
        // should take place: send email to the team,
        // that prepares the items for shipping, send email(s) to customer,...
        log_debug("PostPayment",$_REQUEST);
        $this->content("<h1>Payment processed</h1>");
        $this->content("Provider returned this data:<br/>" + 
            "<pre>".render_var($_REQUEST)."</pre>");
    }
    
    /**
     * This is a special handler method for PayPal.
     * It will be called asynchronously from PayPal
     * backend so user will never see results of it.
     * Just here to update the database when payments
     * are ready or refunded or whatever.
     * See https://www.paypal.com/ipn for details
     * but in fact WebFramework will handle this for you.
     * Just needs this entry point for the callback.
     * @attribute[RequestParam('provider','string')]
     */
    function Notification($provider)
    {
        log_debug("Notification",$_REQUEST);
        $provider = new $provider();
        if( $provider->HandleIPN($_REQUEST) )
            die("OK");
        die("ERR");
    }
} 

这段代码创建了一个表单,要求提供客户的地址数据以及他们想使用的支付提供商: 

到目前为止,一切顺利。如果您点击“立即购买”(使用测试提供商,因为其他提供商缺少一些配置),您将看不到什么特别的,但订单将被创建。 

 

为了使其以这种简单的方式工作,SampleShopOrder必须实现 IShopOrder 接口。它提供了一些简单的方法,允许对支付流程进行通用处理: 

// model/sampleshoporder.class.php
<?php
use ScavixWDF\Model\Model;
use ScavixWDF\Payment\IShopOrder;
use ScavixWDF\Payment\ShopOrderAddress;

/**
 * Represents an order in the database.
 * 
 * In fact nothing more than implementations for the inherited Model 
 * and the implemented IShopOrder interface.
 * See https://github.com/ScavixSoftware/WebFramework/wiki/classes_modules_payment#wiki-1c67f96d00c3c22f1ab9002cd0e3acbb
 * More logic would go into the Set* methods to handle different order states.
 * For our sample we just set the states in the DB.
 */
class SampleShopOrder extends Model implements IShopOrder
{
	const UNKNOWN  = 0;
	const PENDING  = 10;
	const PAID     = 20;
	const FAILED   = 30;
	const REFUNDED = 40;
	
	/**
	 * Returns the table name.
	 * See https://github.com/ScavixSoftware/WebFramework/wiki/classes_essentials_model_model.class#gettablename
	 */
	public function GetTableName() { return 'orders'; }

	/**
	 * Gets the orders address.
	 * @return ShopOrderAddress The order address
	 */
	public function GetAddress()
	{
		$res = new ShopOrderAddress();
		$res->Firstname = $this->fname;
		$res->Lastname = $this->lname;
		$res->Address1 = $this->street;
		$res->Zip = $this->zip;
		$res->City = $this->city;
		$res->Email = $this->email;
		return $res;
	}

	/**
	 * Gets the currency code.
	 * @return string A valid currency code
	 */
	public function GetCurrency() { return 'EUR'; }

	/**
	 * Gets the invoice ID.
	 * @return mixed Invoice identifier
	 */
	public function GetInvoiceId() { return "I".$this->id; }

	/**
	 * Gets the order culture code.
	 * 
	 * See <CultureInfo>
	 * @return string Valid culture code
	 */
	public function GetLocale() { return 'en-US'; }

        /**
	 * Return the total price incl. VAT (if VAT applies for the given country). 
	 * @param float $price The price without VAT.
	 * @return float Price including VAT (if VAT applies for the country).
	 */
	public function GetTotalPrice($price = false)
	{
		if( $price !== false )
			return $price * ( (1+$this->GetVatPercent()) / 100 );
		return $this->price_total * ( (1+$this->GetVatPercent()) / 100 );
	}

        /**
	 * Return the total VAT (if VAT applies for the given country). 
	 * @return float VAT in order currency
	 */
	public function GetTotalVat() { return $this->price_total * ($this->GetVatPercent()/100); }

        /**
	 * Return the total VAT percent (if VAT applies for the given country). 
	 * @return float VAT percent
	 */
	public function GetVatPercent() { return 19; }

	/**
	 * Returns all items.
	 * 
	 * @return array A list of <IShopOrderItem> objects
	 */
	public function ListItems() { return SampleShopOrderItem::Make()->eq('order_id',$this->id)->orderBy('id'); }

	/**
	 * Sets the currency
	 * @param string $currency_code A valid currency code
	 * @return void
	 */
	public function SetCurrency($currency_code) { /* we stay with EUR */ }

	/**
	 * Creates an instance from an order id.
	 * @return IShopOrder The new/loaded order <Model>
	 */
	public static function FromOrderId($order_id)
	{
		return SampleShopOrder::Make()->eq('id',$order_id)->current();
	}

	/**
	 * Called when the order has failed.
	 * 
	 * This is a callback from the payment processor. Will be called when there was an error in the payment process.
	 * This can be synchronous (when cutsomer aborts in then initial payment ui) or asynchronous when something goes wrong
	 * later in the payment processors processes.
	 * @param int $payment_provider_type Provider type identifier (<PaymentProvider>::PROCESSOR_PAYPAL, <PaymentProvider>::PROCESSOR_GATE2SHOP, ...)
	 * @param mixed $transaction_id Transaction identifier (from the payment provider)
	 * @param string $statusmsg An optional status message
	 * @return void
	 */
	public function SetFailed($payment_provider_type, $transaction_id, $statusmsg = false)
	{
		$this->status = self::FAILED;
		$this->updated = $this->deleted = 'now()';
		$this->Save();
	}

	/**
	 * Called when the order has been paid.
	 * 
	 * This is a callback from the payment processor. Will be called when the customer has paid the order.
	 * @param int $payment_provider_type Provider type identifier (<PaymentProvider>::PROCESSOR_PAYPAL, <PaymentProvider>::PROCESSOR_GATE2SHOP, ...)
	 * @param mixed $transaction_id Transaction identifier (from the payment provider)
	 * @param string $statusmsg An optional status message
	 * @return void
	 */
	public function SetPaid($payment_provider_type, $transaction_id, $statusmsg = false)
	{
		$this->status = self::PAID;
		$this->updated = $this->completed = 'now()';
		$this->Save();
	}

	/**
	 * Called when the order has reached pending state.
	 * 
	 * This is a callback from the payment processor. Will be called when the customer has paid the order but the
	 * payment has not yet been finished/approved by the provider.
	 * @param int $payment_provider_type Provider type identifier (<PaymentProvider>::PROCESSOR_PAYPAL, <PaymentProvider>::PROCESSOR_GATE2SHOP, ...)
	 * @param mixed $transaction_id Transaction identifier (from the payment provider)
	 * @param string $statusmsg An optional status message
	 * @return void
	 */
	public function SetPending($payment_provider_type, $transaction_id, $statusmsg = false)
	{
		$this->status = self::PENDING;
		$this->updated = 'now()';
		$this->Save();
	}

	/**
	 * Called when the order has been refunded.
	 * 
	 * This is a callback from the payment processor. Will be called when the payment was refunded for any reason.
	 * This can be reasons from the provider and/or from the customer (when he cancels the payment later).
	 * @param int $payment_provider_type Provider type identifier (<PaymentProvider>::PROCESSOR_PAYPAL, <PaymentProvider>::PROCESSOR_GATE2SHOP, ...)
	 * @param mixed $transaction_id Transaction identifier (from the payment provider)
	 * @param string $statusmsg An optional status message
	 * @return void
	 */
	public function SetRefunded($payment_provider_type, $transaction_id, $statusmsg = false)
	{
		$this->status = self::REFUNDED;
		$this->updated = $this->deleted = 'now()';
		$this->Save();
	}

	/**
	 * Checks if VAT needs to be paid.
	 * @return boolean true or false
	 */
	public function DoAddVat() { return true; /* Let's assume normal VAT customers for now */ }
} 

您还需要创建一个订单项类,我们的类称为 SampleShopOrderItem,并实现 IShopOrderItem 接口。 

// model/sampleshoporderitem.class.php
<?php
use ScavixWDF\Model\Model;
use ScavixWDF\Payment\IShopOrderItem;

/**
 * Represents an order item in the database.
 * 
 * In fact nothing more than implementations for the inherited Model 
 * and the implemented IShopOrderItem interface.
 * See https://github.com/ScavixSoftware/WebFramework/wiki/classes_modules_payment#wiki-97745ff2e14aebb2225c7647a8a059bc
 */
class SampleShopOrderItem extends Model implements IShopOrderItem
{
	/**
	 * Returns the table name.
	 * See https://github.com/ScavixSoftware/WebFramework/wiki/classes_essentials_model_model.class#gettablename
	 */
	public function GetTableName() { return 'items'; }

	/**
	 * Gets the price per item converted into the requested currency.
	 * @param string $currency Currency code
	 * @return float The price per item converted into $currency
	 */
	public function GetAmount($currency) { return $this->price; }

	/**
	 * Gets the discount.
	 * @return float The discount
	 */
	public function GetDiscount() { return 0; }

	/**
	 * Gets the handling cost.
	 * @return float Cost for handling
	 */
	public function GetHandling() { return 0; }

	/**
	 * Gets the items name.
	 * @return string The item name
	 */
	public function GetName() { return $this->title; }

	/**
	 * Gets the quantity.
	 * @return float The quantity
	 */
	public function GetQuantity() { return $this->amount; }

	/**
	 * Gets the shipping cost.
	 * @return float Cost for shipping
	 */
	public function GetShipping() { return 0; }
}
 

最后,必须配置支付模块

// config.php 
<?php
// full code: https://github.com/ScavixSoftware/WebFramework/blob/master/web/sample_shop/config.php

// configure payment module with your IShopOrder class
$CONFIG["payment"]["order_model"] = 'SampleShopOrder';
// set up Gate2Shop if you want to use it
$CONFIG["payment"]["gate2shop"]["merchant_id"]      = '<your_merchant_id>';
$CONFIG["payment"]["gate2shop"]["merchant_site_id"] = '<your_merchant_site_id>';
$CONFIG["payment"]["gate2shop"]["secret_key"]       = '<your_secret_key>';
// set up PayPal if you want to use it
$CONFIG["payment"]["paypal"]["paypal_id"]      = '<your_paypal_id>';
$CONFIG["payment"]["paypal"]["notify_handler"] = array('Basket','Notification');
 

后台管理     

对于商店系统,您需要能够创建产品,并某种程度上能够访问您的客户数据和订单。  

一旦您进入后台管理页面,它会要求您输入凭据(如上所述,凭据是硬编码的):用户名使用“admin”,密码使用“admin”。    

// controller/admin.class.php
<?php
use ScavixWDF\Base\AjaxAction;
use ScavixWDF\Base\AjaxResponse;
use ScavixWDF\Base\Template;
use ScavixWDF\Controls\Form\Form;
use ScavixWDF\JQueryUI\Dialog\uiDialog;
use ScavixWDF\JQueryUI\uiButton;
use ScavixWDF\JQueryUI\uiDatabaseTable;
use ScavixWDF\JQueryUI\uiMessage;

class Admin extends ShopBase
{
        /**
	 * Checks if aa admin has logged in and redirects to login if not.
	 */
	private function _login()
	{
		// check only the fact that somebody logged in
		if( $_SESSION['logged_in'] ) 
			return true;
		
		// redirect to login. this terminates the script execution.
		redirect('Admin','Login');
	}
	
	/**
	 * @attribute[RequestParam('username','string',false)]
	 * @attribute[RequestParam('password','string',false)]
	 */
	function Login($username,$password)
	{
		// if credentials are given, try to log in
		if( $username && $password )
		{
			// see config.php for credentials
			if( $username==cfg_get('admin','username') && $password==cfg_get('admin','password') )
			{
				$_SESSION['logged_in'] = true; // check only the fact that somebody logged in
				redirect('Admin');
			}
			$this->content(uiMessage::Error("Unknown username/passsword"));
		}
		// putting it together as control here. other ways would be to create a new class 
		// derived from Control or a Template (anonymous or with an own class)
		$form = $this->content(new Form());
		$form->content("Username:");
		$form->AddText('username', '');
		$form->content("<br/>Password:");
		$form->AddPassword('password', '');
		$form->AddSubmit("Login");
	}

        /* full code: https://github.com/ScavixSoftware/WebFramework/blob/master/web/sample_shop/controller/admin.class.php*/
}  

Admin 类中的每个方法都会调用 `_login` 方法,如果没有管理员用户登录,则会重定向到“Admin/Login”。该方法使用纯 Control 语法构建登录表单,没有模板。 


在如此粗糙的开发状态下非常丑陋,但有效。   

现在我们从上面提到的第一个部分开始:产品管理。  

// controller/admin.class.php
<?php
use ScavixWDF\Base\AjaxAction;
use ScavixWDF\Base\AjaxResponse;
use ScavixWDF\Base\Template;
use ScavixWDF\Controls\Form\Form;
use ScavixWDF\JQueryUI\Dialog\uiDialog;
use ScavixWDF\JQueryUI\uiButton;
use ScavixWDF\JQueryUI\uiDatabaseTable;
use ScavixWDF\JQueryUI\uiMessage;

class Admin extends ShopBase
{
        /* full code: https://github.com/ScavixSoftware/WebFramework/blob/master/web/sample_shop/controller/admin.class.php */
     
        function Index()
	{
		$this->_login(); // require admin to be logged in
		
		// add products table and a button to create a new product
		$this->content("<h1>Products</h1>");
		$this->content(new uiDatabaseTable(model_datasource('system'),false,'products'))
			->AddPager(10)
			->AddRowAction('trash', 'Delete', $this, 'DelProduct');
		$this->content(uiButton::Make('Add product'))->onclick = AjaxAction::Post('Admin', 'AddProduct');
		
		// add orders table
		$this->content("<h1>Orders</h1>");
		$this->content(new uiDatabaseTable(model_datasource('system'),false,'orders'))
			->AddPager(10)
			->OrderBy = 'id DESC';
		
		// add customers table
		$this->content("<h1>Customers</h1>");
		$this->content(new uiDatabaseTable(model_datasource('system'),false,'customers'))
			->AddPager(10)
			->OrderBy = 'id DESC';
	}
	
	/**
	 * @attribute[RequestParam('title','string',false)]
	 * @attribute[RequestParam('tagline','string',false)]
	 * @attribute[RequestParam('body','text',false)]
	 * @attribute[RequestParam('price','double',false)]
	 */
	function AddProduct($title,$tagline,$body,$price)
	{
		$this->_login(); // require admin to be logged in
		
		// This is a quite simple condition: You MUST provide each of the variables
		if( $title && $tagline && $body && $price )
		{
			// store the uploaded image if present
			if( isset($_FILES['image']) && $_FILES['image']['name'] )
			{
				$i = 1; $image = __DIR__.'/../images/'.$_FILES['image']['name'];
				while( file_exists($image) )
					$image = __DIR__.'/../images/'.($i++).'_'.$_FILES['image']['name'];
				move_uploaded_file($_FILES['image']['tmp_name'], $image);
				$image = basename($image);
			}
			else 
				$image = '';
			
			// store the new product into the database
			$ds = model_datasource('system');
			$ds->ExecuteSql("INSERT INTO products(title,tagline,body,image,price)VALUES(?,?,?,?,?)",
				array($title,$tagline,$body,$image,$price));
			
			redirect('Admin');
		}
		// create a dialog and put a template on it.
		$dlg = new uiDialog('Add product',array('width'=>600,'height'=>450));
		$dlg->content( Template::Make('admin_product_add') );
		$dlg->AddButton('Add product', "$('#frm_add_product').submit()"); // frm_add_product is defined in the template
		$dlg->AddCloseButton("Cancel");
		return $dlg;
	}
	
	/**
	 * @attribute[RequestParam('table','string',false)]
	 * @attribute[RequestParam('action','string',false)]
	 * @attribute[RequestParam('model','array',false)]
	 * @attribute[RequestParam('row','string',false)]
	 */
	function DelProduct($table,$action,$model,$row)
	{
		$this->_login(); // require admin to be logged in
		
		// we use the ajax confirm features of the framework which require some translated string, so we set them up here
		// normally we would start the sysadmin and create some, but for this sample we ignore that.
		default_string('TITLE_DELPRODUCT','Delete Product');
		default_string('TXT_DELPRODUCT','Do you really want to remove this product? This cannot be undone!');
		if( !AjaxAction::IsConfirmed('DELPRODUCT') )
			return AjaxAction::Confirm('DELPRODUCT', 'Admin', 'DelProduct', array('model'=>$model));

		// load and delete the product dataset
		$ds = model_datasource('system');
		$prod = $ds->Query('products')->eq('id',$model['id'])->current();
		$prod->Delete();
		
		// delete the image too if present
		if( $prod->image )
		{
			$image = __DIR__.'/../images/'.$prod->image;
			if( file_exists($image) )
				unlink($image);
		}
		return AjaxResponse::Redirect('Admin');
	}
}

'Index' 方法创建一个数据库表和一个添加新产品的按钮。简单但有效。数据库表控件允许我们添加行操作,因此我们将其用于“删除”触发器,该触发器将在单击时调用“DelProduct”。  

点击“添加产品”按钮将显示一个对话框,其中包含一个用于输入所有产品数据的表单。然后(在对话框接受后),新产品将被添加到数据库,并且浏览器会被重定向以刷新产品列表。  

您猜怎么着:这就是产品后台管理基础部分的内容。当然,我们缺少“编辑”功能以及更多功能,但这展示了我们为实现完整的后台管理界面需要遵循的方向。  

顺着这个思路,我们只在这里再显示两个表:订单和客户。好吧……只是为了展示基本概念: 

接下来呢?

这对于一个示例来说已经足够了,但接下来该怎么做呢?好吧,有一些“标准”的事情要做: 

  • 在 PayPal 和/或 Gate2Shop 注册,并更新商店配置,以便它们可以使用。 
  • 实现更多代码以使支付流程可用(发送电子邮件等) 
  • 扩展后台管理界面以管理产品和订单。 
  • .... 

更改日志   

  • 2013/05/8:初始发布 
  • 2013/05/23:修复了损坏的链接,更新了代码片段以匹配最新版本。 
  • 2014/10/31:添加了命名空间代码。 
© . All rights reserved.