產品列表頁分類篩選、排序的算法實現(PHP)

1、簡單的單條件查詢

工做都是從簡單的開始,先從最簡單的單表查詢開始,這個通常用在首頁以及一些比較獨立的頁面,只須要查找幾個符合條件的產品展現出來便可,可使用分頁或者不使用分頁。下面這個是產品控制器 ProductController 中的一個函數,用於簡單的查詢,好比199元專區就可使用 getTypeSimPro('price=199');php

/**簡單的篩選條件分類產品,單表查詢
 * @param string $sql 單表查詢的SQL
 * @param int $countPerPage=16 每頁商品數
 * @param string $orderBy='salseF DESC' 排序 默認銷量閾值
 * @return array $res 產品二維數組
 */
function getTypeSimPro($sql, $countPerPage = 16, $orderBy='salesF DESC'){
    //$sql = "SELECT ProductId,name,mainPic,priceN,priceVIP,isNew,isHot,sales FROM product WHERE ".$sql;
    $productM = M('product');    // 實例化Data數據對象       
    $where = $sql ? 'onSale=1 AND '.$sql : 'onSale=1';

    $tempSQL = $productM->field('ProductId,name,mainPic,priceN,priceVIP,isNew,isHot,sales,salesF')
                         ->where( $where )                        
                        ->order( $orderBy );
                        
//    $res = $this->executeTempSQL($tempSQL, $countPerPage);
    $res = $tempSQL->select();    //演示不使用分頁,直接返回結果集
    return $res;
}

 2、使用分頁

 因爲Thinkphp的自帶Page分頁類有些不太好用,因此我進行了一點小改造,能夠進行傳遞配置參數修改頁碼顯示的方式。這裏的主要實現邏輯是:html

一、利用同一個臨時數據庫對象 $tempSQL ,使計數和查詢結果的條件保持一致,注意這裏使用了對象克隆,由於TP中,一個Model執行完操做後會被初始化成原始的Model對象,參見 TP手冊連貫操做說明>>前端

二、$_GET['p']是Page類默認的辨別當前頁碼的參數。Page類尤爲裏面的 show() 函數是通過我改造的,能夠傳遞定製化頁碼導航欄參數。不定製也能夠,就是頁碼導航有點太長。sql

三、這裏的 count() 在後面多表查詢的時候是有BUG的,後面再說。thinkphp

/**
 * 執行分類和搜索中的SQL對象
 * @param TP.Model $tempSQL Thinkphp的Model對象
 * @param int $countPerPage=16 每頁的產品數
 * @return array $res['nowP']當前頁數  $res['totalP']總產品數  $res['links']分頁欄HTML     $res['productList']產品二維數組
 * */
protected function executeTempSQL( $tempSQL, $countPerPage=16 ){
    $tempSQL2 = clone $tempSQL;        //對象複製,不然調用一次後第二次會被初始化成原始的M對象
//    print_r($tempSQL);
    $count = $tempSQL->count(); // 查詢知足要求的總記錄數,這裏在多表查詢時必定要以產品編號爲限制條件
//  var_dump($count);
//    var_dump($tempSQL);

    $nowPage = isset($_GET['p'])?$_GET['p']:1;  //當前頁        
    import('ORG.Util.Page');// 導入分頁類
    $Page = new \Think\Page($count,$countPerPage);        // 實例化分頁類 傳入總記錄數,每頁數
    $list = $tempSQL2->page($nowPage.','.$Page->listRows)->select();        //查詢結果集        
//    var_dump($list);
        
    //分頁導航的定製
    $showConfig = array(
        'first'  => '首頁',
        'prev'   => '上一頁',
        'next'   => '下一頁',    
//        'last'   => '尾頁',    //這個不行
        'rollPage' => 5,        //最多顯示5頁導航
    );
    $links = $Page->show( $showConfig );            // 分頁顯示輸出
    //var_dump($links);
    //var_dump($list);
    $res['nowP'] = $nowPage;
    $res['totalP'] = $count;
    $res['links'] = $links;            //分頁輸出
    $res['productList'] = $list;    //數據集
    return $res;
}

3、多表查詢功能概覽

先來一張截圖,要達到的篩選功能大概是這個樣子的。數據庫

其中的數據庫設計爲:數組

product表:ProductId-產品ID、name-產品名、sort1-一級分類、sort2-二級分類、sort_brand-品牌分類、price-價格、onSale-上下架……等等
reserve表:ProductId-產品ID、color-顏色、size-尺碼、reserve-庫存
brand表:id-品牌ID、name-品牌名

tagpro表:Id-自增沒實際用途、tagId-標籤ID、ProductId-產品ID
tag表:Id-標籤ID、tag_name-標籤名數據庫設計

商品與品牌是多對一的關係,用字段作關聯;商品與標籤是多對多的關係,用表作關聯。在上面展現的分類和搜索中,黑色導航欄、性別以及之後可能擴展的篩選項爲標籤聯表查詢,尺碼爲庫存表聯表查詢。ide

4、SearchController控制器

定義了一個Search控制器,裏面有下面幾個方法:函數

function index() 方法是根據上面頁面中的篩選選項拼裝相應的SQL語句的,提交到ProductController去篩選出相關的產品;

function getCutURL($getKey, $CtrlName=CONTROLLER_NAME) 是爲了給頁面生成一系列切除了指定get值的URL地址的;

function pageCheck() 若是改變了篩選條件,則去除頁碼參數,回到從第一頁開始;

在個人項目規劃中IndexController負責頁面的顯示,因此IndexController中的 search() 方法則負責搜索頁面的展現,代碼以下

    function search(){        
        $searchC = A('search');
        $res = $searchC->index(); 
        $URLArr['type2URL'] = $searchC->getCutURL('type2');            //取消選擇分類的URL
        $URLArr['brandURL'] = $searchC->getCutURL('brand');            //取消選擇品牌的URL
        $URLArr['peopleURL'] = $searchC->getCutURL('people');        //取消選擇性別人羣的URL
        $URLArr['sizeURL'] = $searchC->getCutURL('size');            //取消選擇尺碼的URL
        $URLArr['priceURL'] = $searchC->getCutURL('price');            //取消選擇價格區間的URL        
        $URLArr['keyword'] = $searchC->getCutURL('keyword');        //取消搜索關鍵字的URL    
        $URLArr['orderbyURL'] = $searchC->getCutURL('orderby');        //orderby按鈕的URL前部分
        
        $this->assign('URLArr', $URLArr)        
            ->assign ( 'productArr', $res['productList'] )
            ->assign( 'totalNum', $res['totalP'] )
            ->assign( 'pageinfo', $res['links'])
            ->display();
    }

5、兩表屢次查詢

由於產品與標籤是多對多的關係,因此有一種需求是:查詢同時擁有兩個標籤一個產品,姑且設讀取列爲*即所有列。

一開始想到的SQL語句是這個樣子的

SELECT * FROM product p INNER JOIN tagpro ON tagpro.ProductId = p.ProductId INNER JOIN tagpro ON tagpro.ProductId = p.ProductId WHERE onSale=1 AND tagpro.tagId=46 AND tagpro.tagId=40;

然而這條語句並無執行成功,而是報錯 Not unique table/alias: 'tagpro' ,意思是說兩次INNER JOIN的表是同一個表/表別名,因此不行。因此我就試着把一個INNER JOIN刪掉,而後再看是能夠執行了,可是倒是沒有查到任何結果。到這裏,我差點就要罵SQL不夠智能了,明明是該產品在tagpro表中有tagId等於46也有tagId等於40,爲何你要理解成了 tagId同時等於46和40呢?

SELECT * FROM product p INNER JOIN tagpro ON tagpro.ProductId = p.ProductId WHERE onSale=1 AND tagpro.tagId=46 AND tagpro.tagId=40;

罵也沒用,只能再想辦法,相信這種事情不少人都遇到,SQL確定有辦法解決這種問題,後來嘗試了一下這種寫法,把同一個表分別用了兩個不一樣的別名,而後終於能查出來了。 ≧▽≦

SELECT * FROM product p INNER JOIN tagpro a ON a.ProductId = p.ProductId INNER JOIN tagpro b ON b.ProductId = p.ProductId WHERE onSale=1 AND a.tagId=46 AND b.tagId=40;

6、產品控制器中的SQL查詢函數

前面說了,Search控制器中的index()方法負責拼接SQL語句,提交到 Product控制器中進行產品的查詢,如今在Product控制器中新建一個 getSearchPro() 方法,參考原來簡單查詢中的作法,另外加入JOIN的處理。這裏其實就是把 where拼接起來, 把 join 拼接起來。原始的where和join的生成在Search控制器的index()中。

    /**根據篩選條件查找分類產品,多表查詢     //默認每頁16    //排序爲銷售閾值
     * @param string $sql 單表查詢的SQL
     * @param int $countPerPage=16 每頁商品數
     * @param string $orderBy='salseF DESC' 銷量閾值
     * @param array $joinConfig=NULL 多表查詢時 
     *         $joinConfig['joinTable']爲聯合表名二維數組,每一列遍歷爲 $joinTableL
     *                 $joinTableL[name]爲真實表名  $joinTableL[asname]爲as表名
     *         $joinConfig['where']爲附加查詢條件as表名的字段=條件  $joinTableL[asname].size=$size;
     * @return array $res['nowP']當前頁數  $res['totalP']總產品數  $res['links']分頁欄HTML $res['productList']產品二維數組
     */    
    function getSearchPro($sql, $countPerPage = 16, $orderBy='salesF DESC', $joinConfig=NULL){        
        $productM = M('product')->alias('p');    // 實例化Data數據對象
        $where = $sql ? 'p.onSale=1 AND '.$sql : 'p.onSale=1';

        $joinTableArr = $joinConfig['joinTable'];    //要JOIN的表名
        $joinWhereArr = $joinConfig['where'];        //要篩選的格外條件
        foreach( $joinWhereArr as $joinLine ){
            $where .= ' AND '.$joinLine;
        }
        
        $tempSQL = $productM->distinct(true)
                        ->field('p.ProductId,p.name,p.mainPic,p.priceN,p.priceVIP,p.isNew,p.isHot,p.sales,p.salesF')
                        ->where( $where )
                        ->order( $orderBy );
        //處理JOIN
        foreach( $joinTableArr as $joinTableL){
            $tempSQL = $tempSQL->join("$joinTableL[name] AS $joinTableL[asname] ON $joinTableL[asname].ProductId = p.ProductId");
        }
    
        $res = $this->executeTempSQL($tempSQL, $countPerPage, 'p.ProductId');
        return $res;
    }    

注意最後的 $res = $this->executeTempSQL($tempSQL, $countPerPage, 'p.ProductId'); 最後比以前的函數多了一個參數,由於前文提到的 executeTempSQL()方法中的 $count = $tempSQL->count();  改成了  $count = $tempSQL->count('DISTINCT '.$countCond); 不然在多表查詢時計數會出現count的數量比實際查到的結果條數多的狀況。 這裏的executeTempSQL()後面新增的參數爲 $countCond,默認值爲'ProductId',以便單表查詢時沒必要填寫這個無相緊要的參數。

7、Search控制器,篩選項轉換成SQL拼接

index()函數:生成查詢的SQL語句段。邏輯是:
一、根據 get 的參數,分別依次進行篩選/排序處理;
二、只在product表中產生where條件的,以一次查詢加 簡單where SQL拼接的方式處理;
三、多表聯合並在其它表有 where條件的,以 join 數組的形式提交給產品控制器統一拼接處理;
四、這個是目前現行的方案,之後還要再優化的;

    //搜索入口
    function index( $defaultTag=NULL ){
//若是改變了篩選條件,則去除頁碼參數 $this->pageCheck(); //********處理篩選********************************** $type2 = I('get.type2'); // type2: 籃球鞋、跑步鞋…… $brand = I('get.brand'); // brand: 阿迪、匹克、李寧…… $people = I('get.people'); // people: 男、女、中性…… $size = I('get.size'); // size: 35~4六、S~L…… $price = I('get.price'); // price: 小於9九、100~199…… $tag = I('get.tag'); // tagsId: 限時、熱銷、性價比…… $keyword = trim( I('get.keyword') );// keyword: 搜索關鍵字…… $orderby = I('get.orderby'); $joinTableNameIndex = 0; //join所用的表替代名稱後綴,爲了兩個表屢次聯合查詢而設計全部的join表as tb0、tb1... //例如 SELECT * FROM product p INNER JOIN tagpro tb0 ON tb0.ProductId = p.ProductId INNER JOIN tagpro tb1 ON tb1.ProductId = p.ProductId WHERE onSale=1 AND tb0.tagId=46 AND tb1.tagId=40; $sql = ''; //二級分類: /type2/籃球鞋 if( $type2 ){ $type2Id = M()->query("SELECT Id FROM sort WHERE sortName='$type2'"); $type2Id = $type2Id[0]['Id']; $thesql = "p.sort_secType='$type2Id'"; $sql = $sql ? $sql.' AND '.$thesql : $thesql; } //品牌篩選: /brand/李寧 if( $brand ){ $brandId = M()->query("SELECT Id FROM sort WHERE sortName='$brand'"); $brandId = $brandId[0]['Id']; $thesql = "(p.sort_brand='$brandId' OR p.name LIKE '%$brand%')"; //這裏除了品牌分類匹配外還根據商品名稱模糊匹配 $sql = $sql ? $sql.' AND '.$thesql : $thesql; } //性別: /people/男性 /people/女性 /people/中性 if( $people ){ //人羣標籤ID $tagsPeopleId = M('tags')->where("tag_name='$people'")->field('Id')->select(); $tagsPeopleId = $tagsPeopleId[0]['Id']; $join['joinTable'][] = array('name'=>'tagpro', 'asname'=>'tb'.$joinTableNameIndex ); $join['where'][] = 'tb'.$joinTableNameIndex.'.tagId='.$tagsPeopleId ; $joinTableNameIndex++; // $sql = $sql ? $sql.' AND '.$thesql : $thesql; } //營銷標籤(與人羣標籤同樣處理邏輯) if( $tag ){ $tagsId = M('tags')->where("tag_name='$tag'")->field('Id')->select(); $tagsId = $tagsId[0]['Id']; $join['joinTable'][] = array('name'=>'tagpro', 'asname'=>'tb'.$joinTableNameIndex ); $join['where'][] = 'tb'.$joinTableNameIndex.'.tagId='.$tagsId ; $joinTableNameIndex++; // $sql = $sql ? $sql.' AND '.$thesql : $thesql; } //尺碼篩選: /size/35 if( $size ){ $join['joinTable'][] = array('name'=>'reserve', 'asname'=>'tb'.$joinTableNameIndex ); $join['where'][] = 'tb'.$joinTableNameIndex.'.size='.$size ; $joinTableNameIndex++; // $join['joinTable'][] = 'reserve'; // $join['where'][] = 'reserve.size='.$size ; // $sql = $sql ? $sql.' AND '.$thesql : $thesql; } //價格篩選: /price/100~199 /price/大於1999 /price/小於99 if( $price ){ // var_dump( $price ); if( preg_match('/~/', $price) ){ // echo '介於'; $priceArr = explode('~', $price); $thesql = "p.priceVIP BETWEEN '$priceArr[0]' AND '$priceArr[1]'"; }elseif( preg_match('/大於/', $price) ){ // echo '大於'; $priceArr = explode('大於', $price); $thesql = "p.priceVIP>'$priceArr[1]'"; }elseif( preg_match('/小於/', $price) ){ // echo '小於'; $priceArr = explode('小於', $price); $thesql = "p.priceVIP<'$priceArr[1]'"; } $sql = $sql ? $sql.' AND '.$thesql : $thesql; } //搜索關鍵字 if( $keyword ){ $thesql = "(p.ProductId LIKE '%$keyword%' OR p.name LIKE '%$keyword%')"; $sql = $sql ? $sql.' AND '.$thesql : $thesql; } //排序 if( $orderby ){ if( $orderby == 'default'){ $orderbySQL = NULL; }elseif( $orderby == 'priceVIPa' ){ $orderbySQL = 'priceVIP ASC'; }elseif( $orderby == 'priceVIPd' ){ $orderbySQL = 'priceVIP DESC'; }else{ $orderbySQL = $orderby.' DESC'; } } //******************************************* $productC = A('DataProduct'); $res = $productC->getSearchPro($sql,20,$orderbySQL,$join); // var_dump($res); return $res; }

Search 控制器下的另外兩個函數以下:

    /**
     * 若是改變了篩選條件,則去除頁碼參數,回到從第一頁開始
      * 實現原理:若是存在p參數且不是最後一個參數時,則認爲是修改了篩選條件
     * 這裏有一點BUG,多項選擇再翻頁時、取消一個選項並不會回到第一頁(由於p參數仍是在最後)
     */
    function pageCheck(){
//        var_dump( $_SERVER['HTTP_REFERER'] );
//        var_dump( $_GET );
        $getKeyArr = array_keys( $_GET ); //var_dump($getKeyArr);
        $pKeyIndex = array_search('p', $getKeyArr);
//        var_dump($pKeyIndex);
        $arrL = sizeof($getKeyArr); //var_dump($getKeyArr);
        
        if( $pKeyIndex!==FALSE && $pKeyIndex+1 < $arrL ){    //p參數若是不是在最後則爲更改了篩選條件
            $cutPurl = $this->getCutURL('p');
            redirect( $cutPurl );
        }
        
    }
    
    /**
     * 得到切除了指定get值的URL
     * @param string $getKey 要去除的get鍵
     * @param string $CtrlName 控制器名,默認爲頁面URL中的控制器名
     * @return string 不含http://域名 的URL,可直接用於前端輸出
     * */
    function getCutURL($getKey, $CtrlName=CONTROLLER_NAME){
        $getStr ='';
        $getArr = I('get.');
        unset($getArr[$getKey]);
        foreach( $getArr as $getKey=>$getVal){
            $getStr .= '/'.$getKey.'/'.$getVal;
        }
//        var_dump($getStr);
            	$thisURL = explode('.html', U("Index/search") );
		$thisURL = $thisURL[0];
        return $thisURL.$getStr;        
    }
pageCheck() 和 getCutURL()
相關文章
相關標籤/搜索