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


一、簡單的單條件查詢

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

/**簡單的篩選條件分類產品,單表查詢
 * @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;
}

 二、使用分頁

 由於Thinkphp的自帶Page分頁類有些不太好用,所以我進行了一點小改造,可以進行傳遞配置參數修改頁碼顯示的方式。這里的主要實現邏輯是:

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

2、$_GET['p']是Page類默認的辨別當前頁碼的參數。Page類尤其里面的 show() 函數是經過我改造的,可以傳遞定制化頁碼導航欄參數。不定制也可以,就是頁碼導航有點太長。

3、這里的 count() 在后面多表查詢的時候是有BUG的,后面再說。

/**
 * 執行分類和搜索中的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;
}

三、多表查詢功能概覽

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

其中的數據庫設計為:

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-標簽名

商品與品牌是多對一的關系,用字段做關聯;商品與標簽是多對多的關系,用表做關聯。在上面展示的分類和搜索中,黑色導航欄、性別以及以后可能擴展的篩選項為標簽聯表查詢,尺碼為庫存表聯表查詢。

四、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();
    }

五、兩表多次查詢

因為產品與標簽是多對多的關系,所以有一種需求是:查詢同時擁有兩個標簽一個產品,姑且設讀取列為*即全部列。

一開始想到的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;

六、產品控制器中的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',以便單表查詢時不必填寫這個無相緊要的參數。

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

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

    //搜索入口
    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~46、S~L…… $price = I('get.price'); // price: 小於99、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()

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM