Browse Source

feat:版本

super-yimizi 2 months ago
parent
commit
667290de60
90 changed files with 10945 additions and 234 deletions
  1. 1 1
      addons/shop/controller/api/Base.php
  2. 1 1
      addons/shop/controller/api/Cart.php
  3. 2 10
      addons/shop/model/Carts.php
  4. 39 0
      addons/shop/validate/Cart.php
  5. 1 1
      application/api/controller/Base.php
  6. 25 13
      application/api/controller/Cart.php
  7. 39 0
      application/api/validate/Cart.php
  8. 29 0
      application/api/validate/Goods.php
  9. 175 0
      application/common/library/Alter.php
  10. 210 0
      application/common/library/Bootstrap.php
  11. 161 0
      application/common/library/FulltextSearch.php
  12. 180 0
      application/common/library/HashMap.php
  13. 40 0
      application/common/library/IntCode.php
  14. 76 0
      application/common/library/Jssdk.php
  15. 262 0
      application/common/library/KdApiExpOrder.php
  16. 18 0
      application/common/library/OrderException.php
  17. 296 0
      application/common/library/SensitiveHelper.php
  18. 186 0
      application/common/library/Service.php
  19. 44 0
      application/common/library/Theme.php
  20. 114 0
      application/common/library/VicDict.php
  21. 270 0
      application/common/library/VicWord.php
  22. 139 0
      application/common/library/Wechat/Service.php
  23. 27 0
      application/common/library/aip/AipContentCensor.php
  24. 74 0
      application/common/library/aip/AipImageCensor.php
  25. 456 0
      application/common/library/aip/AipNlp.php
  26. 1155 0
      application/common/library/aip/AipOcr.php
  27. 408 0
      application/common/library/aip/lib/AipBase.php
  28. 227 0
      application/common/library/aip/lib/AipHttpClient.php
  29. 181 0
      application/common/library/aip/lib/AipHttpUtil.php
  30. 181 0
      application/common/library/aip/lib/AipSampleSigner.php
  31. 19 0
      application/common/library/aip/lib/AipSignOption.php
  32. 18 0
      application/common/library/coupon/Discount.php
  33. 19 0
      application/common/library/coupon/FullReduction.php
  34. 18 0
      application/common/library/coupon/ToCalculate.php
  35. 9 0
      application/common/library/coupon/calculateInterface.php
  36. 420 0
      application/common/library/hashids/Hashids.php
  37. 24 0
      application/common/library/hashids/HashidsException.php
  38. 57 0
      application/common/library/hashids/HashidsInterface.php
  39. 20 0
      application/common/library/hashids/LICENSE
  40. 123 0
      application/common/library/hashids/Math/Bc.php
  41. 123 0
      application/common/library/hashids/Math/Gmp.php
  42. 99 0
      application/common/library/hashids/Math/MathInterface.php
  43. 49 0
      application/common/library/hashids/composer.json
  44. 19 0
      application/common/library/message/Email.php
  45. 102 0
      application/common/library/message/Mini.php
  46. 19 0
      application/common/library/message/Mobile.php
  47. 95 0
      application/common/library/message/Mp.php
  48. 36 0
      application/common/library/message/Service.php
  49. 98 0
      application/common/model/Address.php
  50. 108 88
      application/common/model/Area.php
  51. 47 0
      application/common/model/Attribute.php
  52. 61 0
      application/common/model/AttributeValue.php
  53. 191 0
      application/common/model/Block.php
  54. 31 0
      application/common/model/Brand.php
  55. 47 0
      application/common/model/Card.php
  56. 106 0
      application/common/model/Carts.php
  57. 346 28
      application/common/model/Category.php
  58. 118 0
      application/common/model/Collect.php
  59. 117 0
      application/common/model/Comment.php
  60. 446 0
      application/common/model/Coupon.php
  61. 89 0
      application/common/model/CouponCondition.php
  62. 32 0
      application/common/model/ElectronicsOrder.php
  63. 126 0
      application/common/model/Exchange.php
  64. 89 0
      application/common/model/ExchangeOrder.php
  65. 64 0
      application/common/model/Freight.php
  66. 188 0
      application/common/model/FreightItems.php
  67. 245 0
      application/common/model/Goods.php
  68. 65 0
      application/common/model/GoodsAttr.php
  69. 55 0
      application/common/model/Guarantee.php
  70. 187 0
      application/common/model/Menu.php
  71. 2 1
      application/common/model/MoneyLog.php
  72. 41 0
      application/common/model/Navigation.php
  73. 615 0
      application/common/model/Order.php
  74. 33 0
      application/common/model/OrderAction.php
  75. 67 0
      application/common/model/OrderAftersales.php
  76. 25 0
      application/common/model/OrderElectronics.php
  77. 171 0
      application/common/model/OrderGoods.php
  78. 23 0
      application/common/model/OrderRegion.php
  79. 199 0
      application/common/model/Page.php
  80. 73 0
      application/common/model/SearchLog.php
  81. 29 0
      application/common/model/Shipper.php
  82. 48 0
      application/common/model/Sku.php
  83. 56 0
      application/common/model/SkuSpec.php
  84. 24 0
      application/common/model/Spec.php
  85. 24 0
      application/common/model/SpecValue.php
  86. 25 0
      application/common/model/SubscribeLog.php
  87. 193 0
      application/common/model/TemplateMsg.php
  88. 6 87
      application/common/model/User.php
  89. 141 0
      application/common/model/UserCoupon.php
  90. 8 4
      application/common/model/Version.php

+ 1 - 1
addons/shop/controller/api/Base.php

@@ -1,6 +1,6 @@
 <?php
 <?php
 
 
-namespace addons\shop\controller\api;
+namespace app\api\controller;
 
 
 use app\common\controller\Api;
 use app\common\controller\Api;
 use app\common\library\Auth;
 use app\common\library\Auth;

+ 1 - 1
addons/shop/controller/api/Cart.php

@@ -1,6 +1,6 @@
 <?php
 <?php
 
 
-namespace addons\shop\controller\api;
+namespace app\api\controller;
 
 
 use addons\shop\model\Carts;
 use addons\shop\model\Carts;
 use addons\shop\model\Sku;
 use addons\shop\model\Sku;

+ 2 - 10
addons/shop/model/Carts.php

@@ -64,32 +64,24 @@ class Carts extends Model
      * @param string $goods_sku_id 商品SKUID
      * @param string $goods_sku_id 商品SKUID
      * @param int    $nums         数量
      * @param int    $nums         数量
      * @param int    $user_id      会员ID
      * @param int    $user_id      会员ID
-     * @param int    $sceneval     1是加入购物车 2是立即购买
      * @return mixed
      * @return mixed
      */
      */
-    public static function push($goods_id, $goods_sku_id, $nums = 1, $user_id = 0, $sceneval = 1)
+    public static function push($goods_id, $goods_sku_id, $nums = 1, $user_id = 0)
     {
     {
 
 
         $row = (new self)->where('goods_id', $goods_id)
         $row = (new self)->where('goods_id', $goods_id)
             ->where('goods_sku_id', $goods_sku_id)
             ->where('goods_sku_id', $goods_sku_id)
             ->where('user_id', $user_id)
             ->where('user_id', $user_id)
-            ->where('sceneval', $sceneval)
             ->find();
             ->find();
         //已存在,数量加
         //已存在,数量加
         if ($row) {
         if ($row) {
-            if ($sceneval == 2) {
-                $row->nums = $nums;
-                $row->save();
-            } else {
-                $row->setInc('nums', $nums);
-            }
+            $row->setInc('nums', $nums);
         } else {
         } else {
             $row = (new self);
             $row = (new self);
             $row->save([
             $row->save([
                 'goods_id'     => $goods_id,
                 'goods_id'     => $goods_id,
                 'goods_sku_id' => $goods_sku_id,
                 'goods_sku_id' => $goods_sku_id,
                 'user_id'      => $user_id,
                 'user_id'      => $user_id,
-                'sceneval'     => $sceneval,
                 'nums'         => $nums
                 'nums'         => $nums
             ]);
             ]);
         }
         }

+ 39 - 0
addons/shop/validate/Cart.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace addons\shop\validate;
+
+use think\Validate;
+
+class Cart extends Validate
+{
+    /**
+     * 验证规则
+     */
+    protected $rule = [
+        'goods_id'     => 'require|integer|gt:0',
+        'goods_sku_id' => 'integer|egt:0',
+        'nums'         => 'require|integer|gt:0',
+    ];
+
+    /**
+     * 提示消息
+     */
+    protected $message = [
+        'goods_id.require'  => '商品ID不能为空',
+        'goods_id.integer'  => '商品ID必须是整数',
+        'goods_id.gt'       => '商品ID必须大于0',
+        'goods_sku_id.integer' => '商品规格ID必须是整数',
+        'goods_sku_id.egt'  => '商品规格ID必须大于或等于0',
+        'nums.require'      => '商品数量不能为空',
+        'nums.integer'      => '商品数量必须是整数',
+        'nums.gt'           => '商品数量必须大于0',
+    ];
+
+    /**
+     * 验证场景
+     */
+    protected $scene = [
+        'add'       => ['goods_id', 'goods_sku_id', 'nums'],
+        'set_nums'  => ['nums'],
+    ];
+} 

+ 1 - 1
application/api/controller/Base.php

@@ -1,6 +1,6 @@
 <?php
 <?php
 
 
-namespace addons\shop\controller\api;
+namespace app\api\controller;
 
 
 use app\common\controller\Api;
 use app\common\controller\Api;
 use app\common\library\Auth;
 use app\common\library\Auth;

+ 25 - 13
application/api/controller/Cart.php

@@ -1,10 +1,10 @@
 <?php
 <?php
 
 
-namespace addons\shop\controller\api;
+namespace app\api\controller;
 
 
-use addons\shop\model\Carts;
-use addons\shop\model\Sku;
-use addons\shop\model\Goods;
+use app\common\model\Carts;
+use app\common\model\Sku;
+use app\common\model\Goods;
 
 
 /**
 /**
  * 购物车接口
  * 购物车接口
@@ -20,10 +20,20 @@ class Cart extends Base
         $goods_id = $this->request->post('goods_id'); //单规格使用
         $goods_id = $this->request->post('goods_id'); //单规格使用
         $goods_sku_id = $this->request->post('goods_sku_id'); //多规格使用
         $goods_sku_id = $this->request->post('goods_sku_id'); //多规格使用
         $nums = $this->request->post('nums/d', 1);
         $nums = $this->request->post('nums/d', 1);
-        $sceneval = $this->request->post('sceneval/d', 1);
+        // 增加验证器
+        $validate = new \app\api\validate\Cart();
+        $validate->scene('add')->check([
+            'goods_id' => $goods_id,
+            'goods_sku_id' => $goods_sku_id,
+            'nums' => $nums,
+        ]);
+        if ($validate->getError()) {
+            $this->error($validate->getError());
+        }
         $goods = Goods::where('id', $goods_id)->where('status', 'normal')->find();
         $goods = Goods::where('id', $goods_id)->where('status', 'normal')->find();
+        
         if (empty($goods)) {
         if (empty($goods)) {
-            $this->error('商品已下架');
+            $this->error('商品不存在');
         }
         }
         if ($goods['spectype'] && !$goods_sku_id) {
         if ($goods['spectype'] && !$goods_sku_id) {
             $this->error("请选择规格");
             $this->error("请选择规格");
@@ -53,7 +63,7 @@ class Cart extends Base
             }
             }
         }
         }
         //去添加购物车
         //去添加购物车
-        $cart_id = Carts::push($goods_id, $goods_sku_id, $nums, $this->auth->id, $sceneval);
+        $cart_id = Carts::push($goods_id, $goods_sku_id, $nums, $this->auth->id);
         $this->success('添加成功', $cart_id);
         $this->success('添加成功', $cart_id);
     }
     }
 
 
@@ -81,9 +91,12 @@ class Cart extends Base
             $this->error('参数错误');
             $this->error('参数错误');
         }
         }
         $nums = $this->request->post('nums/d');
         $nums = $this->request->post('nums/d');
-        if ($nums <= 0) {
-            $this->error('数量必须大于0');
-        }
+        // 使用验证器验证数量
+        $validate = new \addons\shop\validate\Cart();
+        $validate->scene('set_nums')->check([
+            'nums' => $nums,
+        ]);
+
         $row = Carts::with(['Goods', 'Sku'])->where('id', $id)->where('user_id', $this->auth->id)->find();
         $row = Carts::with(['Goods', 'Sku'])->where('id', $id)->where('user_id', $this->auth->id)->find();
         if (!$row) {
         if (!$row) {
             $this->error('未找到记录');
             $this->error('未找到记录');
@@ -112,8 +125,7 @@ class Cart extends Base
     public function index()
     public function index()
     {
     {
         $ids = $this->request->param('ids');
         $ids = $this->request->param('ids');
-        $sceneval = $this->request->param('sceneval/d', 1);
-        $list = Carts::getGoodsList($ids, $this->auth->id, $sceneval);
+        $list = Carts::getGoodsList($ids, $this->auth->id);
         //没有商品过滤数据
         //没有商品过滤数据
         foreach ($list as $key => $item) {
         foreach ($list as $key => $item) {
             if (empty($item->goods)) {
             if (empty($item->goods)) {
@@ -130,7 +142,7 @@ class Cart extends Base
     //获取购物车的数量
     //获取购物车的数量
     public function cart_nums()
     public function cart_nums()
     {
     {
-        $total = Carts::where('user_id', $this->auth->id)->where('sceneval', 1)->count();
+        $total = Carts::where('user_id', $this->auth->id)->count();
         $this->success('', $total);
         $this->success('', $total);
     }
     }
 }
 }

+ 39 - 0
application/api/validate/Cart.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace app\api\validate;
+
+use think\Validate;
+
+class Cart extends Validate
+{
+    /**
+     * 验证规则
+     */
+    protected $rule = [
+        'goods_id'     => 'require|integer|gt:0',
+        'goods_sku_id' => 'integer|egt:0',
+        'nums'         => 'require|integer|gt:0',
+    ];
+
+    /**
+     * 提示消息
+     */
+    protected $message = [
+        'goods_id.require'  => '商品ID不能为空',
+        'goods_id.integer'  => '商品ID必须是整数',
+        'goods_id.gt'       => '商品ID必须大于0',
+        'goods_sku_id.integer' => '商品规格ID必须是整数',
+        'goods_sku_id.egt'  => '商品规格ID必须大于或等于0',
+        'nums.require'      => '商品数量不能为空',
+        'nums.integer'      => '商品数量必须是整数',
+        'nums.gt'           => '商品数量必须大于0',
+    ];
+
+    /**
+     * 验证场景
+     */
+    protected $scene = [
+        'add'       => ['goods_id', 'goods_sku_id', 'nums'],
+        'set_nums'  => ['nums'],
+    ];
+} 

+ 29 - 0
application/api/validate/Goods.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace app\api\validate;
+
+use think\Validate;
+
+class Goods extends Validate
+{
+    /**
+     * 验证规则
+     */
+    protected $rule = [
+        'orderby' => 'in:weigh,sales,price,views,comments',
+    ];
+
+    /**
+     * 提示消息
+     */
+    protected $message = [
+        'orderby.in' => '排序字段只能是权重、销量、价格、浏览数或评论数',
+    ];
+
+    /**
+     * 验证场景
+     */
+    protected $scene = [
+        'lists' => ['orderby'],
+    ];
+} 

+ 175 - 0
application/common/library/Alter.php

@@ -0,0 +1,175 @@
+<?php
+
+namespace app\common\library;
+
+class Alter
+{
+    protected static $instance = null;
+    protected $options = [];
+    protected $data = [
+        'table'   => '',
+        'oldname' => '',
+        'name'    => '',
+        'type'    => 'VARCHAR',
+        'length'  => '255',
+        'content' => '',
+        'comment' => '',
+        'after'   => '',
+    ];
+
+    public function __construct($options = [])
+    {
+        $this->options = array_merge($this->options, $options);
+    }
+
+    public static function instance($options = [])
+    {
+        if (is_null(self::$instance)) {
+            self::$instance = new static($options);
+        }
+
+        return self::$instance;
+    }
+
+    public function setTable($table)
+    {
+        $this->data['table'] = db()->name($table)->getTable();
+        return $this;
+    }
+
+    public function setType($type)
+    {
+        switch ($type) {
+            case 'checkbox':
+            case 'selects':
+                $this->data['type'] = 'SET';
+                break;
+            case 'radio':
+            case 'select':
+                $this->data['type'] = 'ENUM';
+                break;
+            case 'number':
+                $this->data['type'] = 'INT';
+                break;
+            case 'date':
+            case 'datetime':
+            case 'time':
+                $this->data['type'] = strtoupper($type);
+                break;
+            case 'editor':
+                $this->data['type'] = 'TEXT';
+                break;
+            default:
+                $this->data['type'] = 'VARCHAR';
+                break;
+        }
+        return $this;
+    }
+
+    public function setOldname($oldname)
+    {
+        $this->data['oldname'] = $oldname;
+        return $this;
+    }
+
+    public function setName($name)
+    {
+        $this->data['name'] = $name;
+        return $this;
+    }
+
+    public function setLength($length)
+    {
+        $this->data['length'] = $length;
+        return $this;
+    }
+
+    public function setContent($content)
+    {
+        $this->data['content'] = $content;
+        return $this;
+    }
+
+    public function setComment($comment)
+    {
+        $this->data['comment'] = $comment;
+        return $this;
+    }
+
+    public function setDefaultvalue($defaultvalue)
+    {
+        $this->data['defaultvalue'] = $defaultvalue;
+        return $this;
+    }
+
+    public function setDecimals($decimals)
+    {
+        $this->data['decimals'] = $decimals;
+        return $this;
+    }
+
+    protected function process()
+    {
+        if ($this->data['type'] == 'INT') {
+            if ($this->data['decimals'] > 0) {
+                $this->data['type'] = 'DECIMAL';
+                $this->data['length'] = "({$this->data['length']},{$this->data['decimals']})";
+            } else {
+                $this->data['length'] = "({$this->data['length']})";
+            }
+            $this->data['defaultvalue'] = $this->data['defaultvalue'] == '' ? 'NULL' : $this->data['defaultvalue'];
+        } elseif (in_array($this->data['type'], ['SET', 'ENUM'])) {
+            $content = \app\common\model\Config::decode($this->data['content']);
+            $this->data['length'] = "('" . implode("','", array_keys($content)) . "')";
+            $this->data['defaultvalue'] = in_array($this->data['defaultvalue'], array_keys($content)) ? $this->data['defaultvalue'] : ($this->data['type'] == 'ENUM' ? key($content) : '');
+        } elseif (in_array($this->data['type'], ['DATE', 'TIME', 'DATETIME'])) {
+            $this->data['length'] = '';
+            $this->data['defaultvalue'] = "NULL";
+        } elseif (in_array($this->data['type'], ['TEXT'])) {
+            $this->data['length'] = "(0)";
+            $this->data['defaultvalue'] = 'NULL';
+        } else {
+            $this->data['length'] = "({$this->data['length']})";
+        }
+        $this->data['defaultvalue'] = strtoupper($this->data['defaultvalue']) === 'NULL' ? "NULL" : "'{$this->data['defaultvalue']}'";
+    }
+
+    /**
+     * 获取添加字段的SQL
+     * @return string
+     */
+    public function getAddSql()
+    {
+        $this->process();
+
+        $sql = "ALTER TABLE `{$this->data['table']}` "
+            . "ADD `{$this->data['name']}` {$this->data['type']} {$this->data['length']} "
+            . "DEFAULT {$this->data['defaultvalue']} "
+            . "COMMENT '{$this->data['comment']}' "
+            . ($this->data['after'] ? "AFTER `{$this->data['after']}`" : '');
+        return $sql;
+    }
+
+    public function getModifySql()
+    {
+        $this->process();
+
+        $sql = "ALTER TABLE `{$this->data['table']}` "
+            . ($this->data['oldname'] ? 'CHANGE' : 'MODIFY') . " COLUMN " . ($this->data['oldname'] ? "`{$this->data['oldname']}`" : '') . " `{$this->data['name']}` {$this->data['type']} {$this->data['length']} "
+            . "DEFAULT {$this->data['defaultvalue']} "
+            . "COMMENT '{$this->data['comment']}' "
+            . ($this->data['after'] ? "AFTER `{$this->data['after']}`" : '');
+        return $sql;
+    }
+
+    /**
+     * 获取删除字段的SQL
+     * @return string
+     */
+    public function getDropSql()
+    {
+        $sql = "ALTER TABLE `{$this->data['table']}` "
+            . "DROP `{$this->data['name']}`";
+        return $sql;
+    }
+}

+ 210 - 0
application/common/library/Bootstrap.php

@@ -0,0 +1,210 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: zhangyajun <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace app\common\library;
+
+use think\Paginator;
+
+class Bootstrap extends Paginator
+{
+
+    /**
+     * 上一页按钮
+     * @param string $text
+     * @return string
+     */
+    protected function getPreviousButton($text = "&laquo;")
+    {
+        if ($this->currentPage() <= 1) {
+            return $this->getDisabledTextWrapper($text);
+        }
+
+        $url = $this->url(
+            $this->currentPage() - 1
+        );
+
+        return $this->getPageLinkWrapper($url, $text);
+    }
+
+    /**
+     * 下一页按钮
+     * @param string $text
+     * @return string
+     */
+    protected function getNextButton($text = '&raquo;')
+    {
+        if (!$this->hasMore) {
+            return $this->getDisabledTextWrapper($text);
+        }
+
+        $url = $this->url($this->currentPage() + 1);
+
+        return $this->getPageLinkWrapper($url, $text);
+    }
+
+    /**
+     * 页码按钮
+     * @return string
+     */
+    protected function getLinks()
+    {
+        if ($this->simple) {
+            return '';
+        }
+
+        $block = [
+            'first'  => null,
+            'slider' => null,
+            'last'   => null
+        ];
+
+        $side = 3;
+        $window = $side * 2;
+
+        if ($this->lastPage < $window + 6) {
+            $block['first'] = $this->getUrlRange(1, $this->lastPage);
+        } elseif ($this->currentPage <= $window) {
+            $block['first'] = $this->getUrlRange(1, $window + 2);
+            $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage);
+        } elseif ($this->currentPage > ($this->lastPage - $window)) {
+            $block['first'] = $this->getUrlRange(1, 2);
+            $block['last'] = $this->getUrlRange($this->lastPage - ($window + 2), $this->lastPage);
+        } else {
+            $block['first'] = $this->getUrlRange(1, 2);
+            $block['slider'] = $this->getUrlRange($this->currentPage - $side, $this->currentPage + $side);
+            $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage);
+        }
+
+        $html = '';
+
+        if (is_array($block['first'])) {
+            $html .= $this->getUrlLinks($block['first']);
+        }
+
+        if (is_array($block['slider'])) {
+            $html .= $this->getDots();
+            $html .= $this->getUrlLinks($block['slider']);
+        }
+
+        if (is_array($block['last'])) {
+            $html .= $this->getDots();
+            $html .= $this->getUrlLinks($block['last']);
+        }
+
+        return $html;
+    }
+
+    /**
+     * 渲染分页html
+     * @return mixed
+     */
+    public function render($params = null)
+    {
+        if (is_array($params)) {
+            if (isset($params['type'])) {
+                $this->simple = $params['type'] === 'simple';
+            }
+        }
+        if ($this->hasPages()) {
+            if ($this->simple) {
+                return sprintf(
+                    '<ul class="pager">%s %s</ul>',
+                    $this->getPreviousButton(),
+                    $this->getNextButton()
+                );
+            } else {
+                return sprintf(
+                    '<ul class="pagination">%s %s %s</ul>',
+                    $this->getPreviousButton(),
+                    $this->getLinks(),
+                    $this->getNextButton()
+                );
+            }
+        }
+    }
+
+    /**
+     * 生成一个可点击的按钮
+     *
+     * @param  string $url
+     * @param  int    $page
+     * @return string
+     */
+    protected function getAvailablePageWrapper($url, $page)
+    {
+        return '<li><a href="' . htmlentities($url) . '">' . $page . '</a></li>';
+    }
+
+    /**
+     * 生成一个禁用的按钮
+     *
+     * @param  string $text
+     * @return string
+     */
+    protected function getDisabledTextWrapper($text)
+    {
+        return '<li class="disabled"><span>' . $text . '</span></li>';
+    }
+
+    /**
+     * 生成一个激活的按钮
+     *
+     * @param  string $text
+     * @return string
+     */
+    protected function getActivePageWrapper($text)
+    {
+        return '<li class="active"><span>' . $text . '</span></li>';
+    }
+
+    /**
+     * 生成省略号按钮
+     *
+     * @return string
+     */
+    protected function getDots()
+    {
+        return $this->getDisabledTextWrapper('...');
+    }
+
+    /**
+     * 批量生成页码按钮.
+     *
+     * @param  array $urls
+     * @return string
+     */
+    protected function getUrlLinks(array $urls)
+    {
+        $html = '';
+
+        foreach ($urls as $page => $url) {
+            $html .= $this->getPageLinkWrapper($url, $page);
+        }
+
+        return $html;
+    }
+
+    /**
+     * 生成普通页码按钮
+     *
+     * @param  string $url
+     * @param  int    $page
+     * @return string
+     */
+    protected function getPageLinkWrapper($url, $page)
+    {
+        if ($page == $this->currentPage()) {
+            return $this->getActivePageWrapper($page);
+        }
+
+        return $this->getAvailablePageWrapper($url, $page);
+    }
+}

+ 161 - 0
application/common/library/FulltextSearch.php

@@ -0,0 +1,161 @@
+<?php
+
+namespace app\common\library;
+
+use addons\xunsearch\library\Xunsearch;
+use think\Config;
+use think\Exception;
+use think\View;
+
+class FulltextSearch
+{
+
+    public static function config()
+    {
+        $data = [
+            [
+                'name'   => self::getProjectName(),
+                'title'  => get_addon_config('shop')['sitename'],
+                'fields' => [
+                    ['name' => 'pid', 'type' => 'id', 'title' => '主键'],
+                    ['name' => 'id', 'type' => 'numeric', 'title' => 'ID'],
+                    ['name' => 'title', 'type' => 'title', 'title' => '标题'],
+                    ['name' => 'content', 'type' => 'body', 'title' => '内容',],
+                    ['name' => 'type', 'type' => 'string', 'title' => '类型', 'index' => 'self'],
+                    ['name' => 'category_id', 'type' => 'numeric', 'title' => '分类ID', 'index' => 'self',],
+                    ['name' => 'user_id', 'type' => 'numeric', 'title' => '会员ID', 'index' => 'self',],
+                    ['name' => 'url', 'type' => 'string', 'title' => '链接',],
+                    ['name' => 'views', 'type' => 'numeric', 'title' => '浏览次数',],
+                    ['name' => 'comments', 'type' => 'numeric', 'title' => '评论次数',],
+                    ['name' => 'createtime', 'type' => 'date', 'title' => '发布时间',],
+                ]
+            ]
+        ];
+        return $data;
+    }
+
+    /**
+     * 重置搜索索引数据库
+     */
+    public static function reset()
+    {
+        \addons\shop\model\Goods::where('status', 'normal')->chunk(100, function ($list) {
+            foreach ($list as $item) {
+                self::add($item);
+            }
+        });
+        return true;
+    }
+
+    /**
+     * 添加索引
+     * @param $row
+     */
+    public static function add($row)
+    {
+        self::update($row, true);
+    }
+
+    /**
+     * 更新索引
+     * @param      $row
+     * @param bool $add
+     */
+    public static function update($row, $add = false)
+    {
+        if (!get_addon_info('xunsearch')) {
+            return;
+        }
+        if (is_numeric($row)) {
+            $row = \addons\shop\model\Goods::get($row);
+            if (!$row) {
+                return;
+            }
+        }
+        if (isset($row['status']) && $row['status'] != 'normal') {
+            self::del($row);
+            return;
+        }
+        $data = [];
+        if ($row instanceof \addons\shop\model\Goods || $row instanceof \app\admin\model\shop\Goods) {
+            $data['id'] = isset($row['id']) ? $row['id'] : 0;
+            $data['title'] = isset($row['title']) ? $row['title'] : '';
+            $data['category_id'] = isset($row['category_id']) ? $row['category_id'] : 0;
+            $data['user_id'] = isset($row['user_id']) ? $row['user_id'] : 0;
+            $data['content'] = isset($row['content']) ? $row['content'] : '';
+            $data['comments'] = isset($row['comments']) ? $row['comments'] : 0;
+            $data['createtime'] = isset($row['createtime']) ? $row['createtime'] : 0;
+            $data['views'] = isset($row['views']) ? $row['views'] : 0;
+            $data['type'] = 'goods';
+            $data['url'] = $row->fullurl;
+        }
+        if ($data) {
+            $data['pid'] = substr($data['type'], 0, 1) . $data['id'];
+            Xunsearch::instance(self::getProjectName())->update($data, $add);
+        }
+    }
+
+    /**
+     * 删除
+     * @param $row
+     */
+    public static function del($row)
+    {
+        if (!get_addon_info('xunsearch')) {
+            return;
+        }
+        $pid = "g" . (is_numeric($row) ? $row : ($row && isset($row['id']) ? $row['id'] : 0));
+        if ($pid) {
+            Xunsearch::instance(self::getProjectName())->del($pid);
+        }
+    }
+
+    /**
+     * 获取搜索结果
+     * @return array
+     */
+    public static function search($q, $page = 1, $pagesize = 20, $order = '', $fulltext = true, $fuzzy = false, $synonyms = false)
+    {
+        if (!get_addon_info('xunsearch')) {
+            return [];
+        }
+        return Xunsearch::instance(self::getProjectName())->search($q, $page, $pagesize, $order, $fulltext, $fuzzy, $synonyms);
+    }
+
+    /**
+     * 获取建议搜索关键字
+     * @param string $q     关键字
+     * @param int    $limit 返回条数
+     */
+    public static function suggestion($q, $limit = 10)
+    {
+        if (!get_addon_info('xunsearch')) {
+            return [];
+        }
+        return Xunsearch::instance(self::getProjectName())->suggestion($q, $limit);
+    }
+
+    /**
+     * 获取搜索热门关键字
+     * @return array
+     * @throws \XSException
+     */
+    public static function hot()
+    {
+        if (!get_addon_info('xunsearch')) {
+            return [];
+        }
+        return Xunsearch::instance(self::getProjectName())->getXS()->search->getHotQuery();
+    }
+
+    /**
+     * 获取项目名称
+     * @return mixed|string
+     */
+    public static function getProjectName()
+    {
+        $config = get_addon_config('shop');
+        return $config['xunsearchprojectname'] ?? 'shop';
+    }
+
+}

+ 180 - 0
application/common/library/HashMap.php

@@ -0,0 +1,180 @@
+<?php
+
+/**
+ * php构建哈希表类.
+ * User: wanghui
+ * Date: 17/3/9
+ * Time: 上午9:10
+ **/
+
+namespace app\common\library;
+
+class HashMap
+{
+    /**
+     * 哈希表变量
+     *
+     * @var array|null
+     */
+    protected $hashTable = array();
+
+    public function __construct()
+    {
+    }
+
+    /**
+     * 向HashMap中添加一个键值对
+     *
+     * @param $key
+     * @param $value
+     * @return mixed|null
+     */
+    public function put($key, $value)
+    {
+        if (!array_key_exists($key, $this->hashTable)) {
+            $this->hashTable[$key] = $value;
+            return null;
+        }
+        $_temp = $this->hashTable[$key];
+        $this->hashTable[$key] = $value;
+        return $_temp;
+    }
+
+    /**
+     * 根据key获取对应的value
+     *
+     * @param $key
+     * @return mixed|null
+     */
+    public function get($key)
+    {
+        if (array_key_exists($key, $this->hashTable)) {
+            return $this->hashTable[$key];
+        }
+        return null;
+    }
+
+    /**
+     * 删除指定key的键值对
+     *
+     * @param $key
+     * @return mixed|null
+     */
+    public function remove($key)
+    {
+        $temp_table = array();
+        if (array_key_exists($key, $this->hashTable)) {
+            $tempValue = $this->hashTable[$key];
+            while ($curValue = current($this->hashTable)) {
+                if (!(key($this->hashTable) == $key)) {
+                    $temp_table[key($this->hashTable)] = $curValue;
+                }
+                next($this->hashTable);
+            }
+            $this->hashTable = null;
+            $this->hashTable = $temp_table;
+            return $tempValue;
+        }
+        return null;
+    }
+
+    /**
+     * 获取HashMap的所有键值
+     *
+     * @return array
+     */
+    public function keys()
+    {
+        return array_keys($this->hashTable);
+    }
+
+    /**
+     * 获取HashMap的所有value值
+     *
+     * @return array
+     */
+    public function values()
+    {
+        return array_values($this->hashTable);
+    }
+
+    /**
+     * 将一个HashMap的值全部put到当前HashMap中
+     *
+     * @param $map
+     */
+    public function putAll($map)
+    {
+        if (!$map->isEmpty() && $map->size() > 0) {
+            $keys = $map->keys();
+            foreach ($keys as $key) {
+                $this->put($key, $map->get($key));
+            }
+        }
+
+        return;
+    }
+
+    /**
+     * 移除HashMap中所有元素
+     *
+     * @return bool
+     */
+    public function removeAll()
+    {
+        $this->hashTable = null;
+        return true;
+    }
+
+    /**
+     * 判断HashMap中是否包含指定的值
+     *
+     * @param $value
+     * @return bool
+     */
+    public function containsValue($value)
+    {
+        while ($curValue = current($this->H_table)) {
+            if ($curValue == $value) {
+                return true;
+            }
+            next($this->hashTable);
+        }
+        return false;
+    }
+
+    /**
+     * 判断HashMap中是否包含指定的键key
+     *
+     * @param $key
+     * @return bool
+     */
+    public function containsKey($key)
+    {
+        if (array_key_exists($key, $this->hashTable)) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 获取HashMap中元素个数
+     *
+     * @return int
+     */
+    public function size()
+    {
+        return count($this->hashTable);
+    }
+
+    /**
+     * 判断HashMap是否为空
+     *
+     * @return bool
+     */
+    public function isEmpty()
+    {
+        return (count($this->hashTable) == 0);
+    }
+}

+ 40 - 0
application/common/library/IntCode.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace app\common\library;
+
+use Hashids\Hashids;
+
+class IntCode
+{
+    private static $hasids = null;
+
+    /**
+     * 初始化
+     * @access public
+     * @return Hashids
+     */
+    public static function hashids()
+    {
+        if (is_null(self::$hasids)) {
+            $config = get_addon_config('shop');
+            $key = $config['coupon_key'];
+            $key = $key ? $key : config('token.key');
+            self::$hasids = new Hashids($key, 4);
+        }
+        return self::$hasids;
+    }
+
+    public static function encode($int)
+    {
+        return self::hashids()->encode($int);
+    }
+
+    public static function decode($str)
+    {
+        $data = self::hashids()->decode($str);
+        if (isset($data[0])) {
+            return $data[0];
+        }
+        return null;
+    }
+}

+ 76 - 0
application/common/library/Jssdk.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace app\common\library;
+
+use fast\Http;
+use fast\Random;
+use think\Cache;
+
+class Jssdk
+{
+    private $appId;
+    private $appSecret;
+
+    public function __construct()
+    {
+        $config = get_addon_config('third');
+
+        $this->appId = $config['wechat']['app_id'];
+        $this->appSecret = $config['wechat']['app_secret'];
+    }
+
+    public function getSignedPackage($url)
+    {
+        $jsapiTicket = $this->getJsApiTicket();
+        $timestamp = time();
+        $nonceStr = Random::alnum(16);
+        // 这里参数的顺序要按照 key 值 ASCII 码升序排序
+        $string = "jsapi_ticket={$jsapiTicket}&noncestr={$nonceStr}&timestamp={$timestamp}&url={$url}";
+        $signature = sha1($string);
+        $signPackage = array(
+            "appId"     => $this->appId,
+            "nonceStr"  => $nonceStr,
+            "timestamp" => $timestamp,
+            "url"       => $url,
+            "signature" => $signature,
+            "rawString" => $string,
+            "jsticket" => $jsapiTicket,
+        );
+        return $signPackage;
+    }
+
+    private function getJsApiTicket()
+    {
+        $ticket = Cache::get("wechat_jsapi_ticket");
+        if (!$ticket) {
+            $accessToken = $this->getAccessToken();
+            // 如果是企业号用以下 URL 获取 ticket
+            // $url = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token={$accessToken}";
+            $url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token={$accessToken}";
+            $ret = Http::get($url);
+            $json = (array)json_decode($ret, true);
+            $ticket = $json['ticket'] ?? '';
+            if ($ticket) {
+                Cache::set('wechat_jsapi_ticket', $ticket, 7200);
+            }
+        }
+        return $ticket;
+    }
+
+    private function getAccessToken()
+    {
+        $token = Cache::get("wechat_access_token");
+        if (!$token) {
+            // 如果是企业号用以下URL获取access_token
+            // $url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={$this->appId}&corpsecret={$this->appSecret}";
+            $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$this->appId}&secret={$this->appSecret}";
+            $ret = Http::get($url);
+            $json = (array)json_decode($ret, true);
+            $token = $json['access_token'] ?? '';
+            if ($token) {
+                Cache::set('wechat_access_token', $token, 7200);
+            }
+        }
+        return $token;
+    }
+}

+ 262 - 0
application/common/library/KdApiExpOrder.php

@@ -0,0 +1,262 @@
+<?php
+
+namespace app\common\library;
+
+use app\common\model\Order;
+use app\common\model\ElectronicsOrder;
+use app\common\model\OrderElectronics;
+use fast\Http;
+
+/**
+ * 电子面单
+ * @ DateTime 2021-06-11
+ * @ 
+ */
+class KdApiExpOrder
+{
+
+    protected static $reqUrl = '';
+
+    /**
+     * @ DateTime 2021-06-11
+     * @ 
+     * @electronics [type] $electronics
+     * @electronics [type] $sender
+     * @return void
+     */
+    public static function create($order_id, $electronics_id)
+    {
+        $order = Order::with(['OrderGoods'])
+            ->field('o.*,p.name province_name,c.name city_name,a.name area_name')
+            ->alias('o')
+            ->join('shop_address d', 'o.address_id=d.id', 'LEFT')
+            ->join('shop_area p', 'd.province_id=p.id', 'LEFT')
+            ->join('shop_area c', 'd.city_id=c.id', 'LEFT')
+            ->join('shop_area a', 'd.area_id=a.id', 'LEFT')
+            ->where('o.id', $order_id)
+            ->find();
+
+        if ($order->orderstate == 1) {
+            throw new \Exception('订单已取消,不支持生成电子面单');
+        }
+
+        if ($order->orderstate == 2) {
+            throw new \Exception('订单已失效,不支持生成电子面单');
+        }
+
+        if ($order->orderstate == 3) {
+            throw new \Exception('订单已完成,不支持生成电子面单');
+        }
+
+        if (!$order->paystate) {
+            throw new \Exception('订单未支付,不支持生成电子面单');
+        }
+
+        if ($order->shippingstate) {
+            throw new \Exception('订单已发货,不支持生成电子面单');
+        }
+
+        $electronics = ElectronicsOrder::with(['Shipper'])->where('id', $electronics_id)->find();
+
+        if (empty($order)) {
+            throw new \Exception('未找到订单');
+        }
+
+        if (empty($electronics)) {
+            throw new \Exception("电子面单模板不存在");
+        }
+
+        if (empty($electronics->shipper)) {
+            throw new \Exception("快递公司不存在");
+        }
+
+        if (empty($order->order_goods)) {
+            throw new \Exception("订单商品不存在");
+        }
+
+        $Commodity = [];
+        $quantity = 0;
+        $Weight = 0;
+
+        foreach ($order->order_goods as $item) {
+            $Commodity[] =  [
+                'GoodsName' => $item['title'],
+                'GoodsCode' => $item['goods_id'] . '_' . $item['goods_sku_id'],
+                'Goodsquantity' => $item['nums'],
+                'GoodsPrice' => $item['price'],
+                'GoodsWeight' => $item['weight'],
+                // 'GoodsVol' => 0,
+                'GoodsDesc' => $item['attrdata']
+            ];
+            $quantity = bcadd($quantity, $item['nums']);
+            $Weight = bcadd($Weight, $item['weight']);
+        }
+
+        // 组装应用级参数
+        $requestData = [
+            'MemberID'  => $order->user_id,
+            'OrderCode' => $order->order_sn,
+            'ShipperCode' => $electronics->shipper->shipper_code,
+            'LogisticCode'  => $electronics->logistic_code,
+            'CustomerName' => $electronics->customer_name,
+            'CustomerPwd' => $electronics->customer_pwd,
+            // 'ThrOrderCode' => '1234567890',
+            'SendSite'  => $electronics->send_site,
+            'PayType'  => $electronics->paytype,
+            'MonthCode'  => $electronics->month_code,
+            'IsReturnSignBill'  => $electronics->is_return_sign_bill,
+            'OperateRequire'  => $electronics->operate_require,
+            'ExpType'  => $electronics->exp_type,
+            'Cost'  => $order->shippingfee,
+            'OtherCost'  => 0,
+            'Sender'  => [
+                'Company' => $electronics->company,
+                'Name' => $electronics->name,
+                'Tel' => $electronics->tel,
+                'Mobile' => $electronics->mobile,
+                'PostCode' => $electronics->post_code,
+                'ProvinceName' => $electronics->province_name,
+                'CityName' => $electronics->city_name,
+                'ExpAreaName' => $electronics->exp_area_name,
+                'Address' => $electronics->address
+            ],
+            'Receiver' => [
+                'Company' => '',
+                'Name' => $order->receiver,
+                'Tel' => '',
+                'Mobile' => $order->mobile,
+                'PostCode' => $order->zipcode,
+                'ProvinceName' => $order->province_name,
+                'CityName' => $order->city_name,
+                'ExpAreaName' => $order->area_name,
+                'Address' => $order->address
+            ],
+            'Commodity' => $Commodity,
+            'IsNotice' => $electronics->is_notice,
+            'StartDate' => '',
+            'EndDate' => '',
+            'AddService' => [],
+            'Weight' => $Weight,
+            'Quantity' => $quantity,
+            // 'Volume' => 0,
+            'IsReturnPrintTemplate' => $electronics->is_return_temp,
+            'Remark' => $electronics->remark
+        ];
+        self::setReqUrl();
+        $res = self::sendPost($requestData, 1007);
+
+        //生成电子面单后,替换快递单号
+        if (isset($res['Success']) && $res['Success']) {
+            $order->expressno = $res['Order']['LogisticCode'];
+            $order->expressname = $electronics['shipper']['name'];
+            $order->save();
+        }
+        //入库
+        OrderElectronics::push($res, $order->order_sn, $electronics->customer_name, $electronics->customer_pwd);
+        return $res;
+    }
+
+    /**
+     * @ DateTime 2021-06-11
+     * @ 电子面单取消
+     * @return void
+     */
+    public static function cancel($param)
+    {
+        // 组装应用级参数
+        $requestData = [
+            'ShipperCode' => $param['shipper_code'],
+            'OrderCode' => $param['order_sn'],
+            'ExpNo' => $param['logistic_code'],
+            'CustomerName' => $param['customer_name'],
+            'CustomerPwd' => $param['customer_pwd']
+        ];
+        self::setReqUrl();
+        return self::sendPost($requestData, 1147);
+    }
+
+    /**
+     * @ DateTime 2021-06-11
+     * @ 单号余量查询
+     * @return void
+     */
+    public static function getOrderTraces($param)
+    {
+        $requestData = [
+            'ShipperCode' => $param['shipper_code'],
+            'StationCode' => '',
+            'StationName' => '',
+            'CustomerName' => $param['customer_name'],
+            'CustomerPwd' => $param['customer_pwd']
+        ];
+        self::setReqUrl();
+        return self::sendPost($requestData, 1127);
+    }
+
+    /**
+     * @ 物流查询
+     * @param [type] $param
+     * @return void
+     */
+    public static function getLogisticsQuery($param)
+    {
+        $requestData = [
+            'OrderCode' => $param['order_sn'],
+            'ShipperCode' => $param['shipper_code'],
+            'LogisticCode' => $param['logistic_code'],
+        ];
+        self::setReqUrl(1);
+        return self::sendPost($requestData, 1002);
+    }
+
+    //设置url
+    protected static function setReqUrl($type = 0)
+    {
+        $config = get_addon_config('shop');
+        if ($config['api_mode'] == 'sandbox') {
+            self::$reqUrl = 'http://sandboxapi.kdniao.com:8080/kdniaosandbox/gateway/exterfaceInvoke.json';
+        } else {
+            switch ($type) {
+                case 1: //即时物流
+                    self::$reqUrl = 'https://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx';
+                    break;
+                default: //电子面单
+                    self::$reqUrl = 'https://api.kdniao.com/api/EOrderService';
+            }
+        }
+    }
+
+
+    /**
+     * @ DateTime 2021-06-11
+     * @ 请求
+     * @return void
+     */
+    protected static function sendPost($requestData, $requestType)
+    {
+        $requestData = json_encode($requestData, JSON_UNESCAPED_UNICODE);
+        $config = get_addon_config('shop');
+        // 组装系统级参数
+        $data = array(
+            'EBusinessID' => $config['EBusinessID'],
+            'RequestType' => $requestType,
+            'RequestData' => urlencode($requestData),
+            'DataType' => '2',
+        );
+        $data['DataSign'] = self::encrypt($requestData, $config['kdNiaoApiKey']);
+        //以form表单形式提交post请求,post请求体中包含了应用级参数和系统级参数
+        $result = Http::post(self::$reqUrl, $data);
+        return (array)json_decode($result, true);
+    }
+
+    /**
+     * Sign签名生成
+     * @electronics data 内容   
+     * @electronics ApiKey ApiKey
+     * @return DataSign签名
+     */
+    protected static function encrypt($data, $apiKey)
+    {
+        return urlencode(base64_encode(md5($data . $apiKey)));
+    }
+}

+ 18 - 0
application/common/library/OrderException.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace app\common\library;
+
+
+use think\Exception;
+use Throwable;
+
+class OrderException extends Exception
+{
+    public function __construct($message = "", $code = 0, $data = [])
+    {
+        $this->message = $message;
+        $this->code = $code;
+        $this->data = $data;
+    }
+
+}

+ 296 - 0
application/common/library/SensitiveHelper.php

@@ -0,0 +1,296 @@
+<?php
+
+/**
+ * 敏感词类库.
+ * User: wanghui
+ * Date: 17/3/9
+ * Time: 上午9:11
+ */
+
+namespace app\common\library;
+
+class SensitiveHelper
+{
+    /**
+     * 待检测语句长度
+     *
+     * @var int
+     */
+    protected $contentLength = 0;
+
+    /**
+     * 敏感词单例
+     *
+     * @var object|null
+     */
+    private static $_instance = null;
+
+    /**
+     * 铭感词库树
+     *
+     * @var HashMap|null
+     */
+    protected $wordTree = null;
+
+    /**
+     * 存放待检测语句铭感词
+     *
+     * @var array|null
+     */
+    protected static $badWordList = null;
+
+    /**
+     * 获取单例
+     *
+     * @return self
+     */
+    public static function init()
+    {
+        if (!self::$_instance instanceof self) {
+            self::$_instance = new self();
+        }
+        return self::$_instance;
+    }
+
+    /**
+     * 构建铭感词树【文件模式】
+     *
+     * @param string $filepath
+     * @return $this
+     * @throws \Exception
+     */
+    public function setTreeByFile($filepath = '')
+    {
+        if (!file_exists($filepath)) {
+            throw new \Exception('词库文件不存在');
+        }
+
+        // 词库树初始化
+        $this->wordTree = new HashMap();
+
+        foreach ($this->yieldToReadFile($filepath) as $word) {
+            $this->buildWordToTree(trim($word));
+        }
+
+        return $this;
+    }
+
+
+    /**
+     * 构建铭感词树【数组模式】
+     *
+     * @param null $sensitiveWords
+     * @return $this
+     * @throws \Exception
+     */
+    public function setTree($sensitiveWords = null)
+    {
+        if (empty($sensitiveWords)) {
+            throw new \Exception('词库不能为空');
+        }
+
+        $this->wordTree = new HashMap();
+
+        foreach ($sensitiveWords as $word) {
+            $this->buildWordToTree($word);
+        }
+        return $this;
+    }
+
+    /**
+     * 检测文字中的敏感词
+     *
+     * @param string $content 待检测内容
+     * @param int $matchType 匹配类型 [默认为最小匹配规则]
+     * @param int $wordNum 需要获取的敏感词数量 [默认获取全部]
+     * @return array
+     */
+    public function getBadWord($content, $matchType = 1, $wordNum = 0)
+    {
+        $this->contentLength = mb_strlen($content, 'utf-8');
+        $badWordList = array();
+        for ($length = 0; $length < $this->contentLength; $length++) {
+            $matchFlag = 0;
+            $flag = false;
+            $tempMap = $this->wordTree;
+            for ($i = $length; $i < $this->contentLength; $i++) {
+                $keyChar = mb_substr($content, $i, 1, 'utf-8');
+
+                // 获取指定节点树
+                $nowMap = $tempMap->get($keyChar);
+
+                // 不存在节点树,直接返回
+                if (empty($nowMap)) {
+                    break;
+                }
+
+                // 存在,则判断是否为最后一个
+                $tempMap = $nowMap;
+
+                // 找到相应key,偏移量+1
+                $matchFlag++;
+
+                // 如果为最后一个匹配规则,结束循环,返回匹配标识数
+                if (false === $nowMap->get('ending')) {
+                    continue;
+                }
+
+                $flag = true;
+
+                // 最小规则,直接退出
+                if (1 === $matchType) {
+                    break;
+                }
+            }
+
+            if (!$flag) {
+                $matchFlag = 0;
+            }
+
+            // 找到相应key
+            if ($matchFlag <= 0) {
+                continue;
+            }
+
+            $badWordList[] = mb_substr($content, $length, $matchFlag, 'utf-8');
+
+            // 有返回数量限制
+            if ($wordNum > 0 && count($badWordList) == $wordNum) {
+                return $badWordList;
+            }
+
+            // 需匹配内容标志位往后移
+            $length = $length + $matchFlag - 1;
+        }
+        return $badWordList;
+    }
+
+
+    /**
+     * 替换敏感字字符
+     *
+     * @param $content
+     * @param $replaceChar
+     * @param string $sTag
+     * @param string $eTag
+     * @param int $matchType
+     * @return mixed
+     */
+    public function replace($content, $replaceChar = '', $sTag = '', $eTag = '', $matchType = 1)
+    {
+        if (empty($content)) {
+            throw new \Exception('请填写检测的内容');
+        }
+
+        if (empty(self::$badWordList)) {
+            $badWordList = $this->getBadWord($content, $matchType);
+        } else {
+            $badWordList = self::$badWordList;
+        }
+
+        // 未检测到敏感词,直接返回
+        if (empty($badWordList)) {
+            return $content;
+        }
+
+        foreach ($badWordList as $badWord) {
+            if ($sTag || $eTag) {
+                $replaceChar = $sTag . $badWord . $eTag;
+            }
+            $content = str_replace($badWord, $replaceChar, $content);
+        }
+        return $content;
+    }
+
+    /**
+     * 被检测内容是否合法,合法返回true,非法返回false
+     * @param $content
+     * @return bool
+     */
+    public function islegal($content)
+    {
+        $this->contentLength = mb_strlen($content, 'utf-8');
+
+        for ($length = 0; $length < $this->contentLength; $length++) {
+            $matchFlag = 0;
+
+            $tempMap = $this->wordTree;
+            for ($i = $length; $i < $this->contentLength; $i++) {
+                $keyChar = mb_substr($content, $i, 1, 'utf-8');
+
+                // 获取指定节点树
+                $nowMap = $tempMap->get($keyChar);
+
+                // 不存在节点树,直接返回
+                if (empty($nowMap)) {
+                    break;
+                }
+
+                // 找到相应key,偏移量+1
+                $tempMap = $nowMap;
+                $matchFlag++;
+
+                // 如果为最后一个匹配规则,结束循环,返回匹配标识数
+                if (false === $nowMap->get('ending')) {
+                    continue;
+                }
+
+                return false;
+            }
+
+            // 找到相应key
+            if ($matchFlag <= 0) {
+                continue;
+            }
+
+            // 需匹配内容标志位往后移
+            $length = $length + $matchFlag - 1;
+        }
+        return true;
+    }
+
+    protected function yieldToReadFile($filepath)
+    {
+        $fp = fopen($filepath, 'r');
+        while (!feof($fp)) {
+            yield fgets($fp);
+        }
+        fclose($fp);
+    }
+
+    // 将单个敏感词构建成树结构
+    protected function buildWordToTree($word = '')
+    {
+        if ('' === $word) {
+            return;
+        }
+        $tree = $this->wordTree;
+
+        $wordLength = mb_strlen($word, 'utf-8');
+        for ($i = 0; $i < $wordLength; $i++) {
+            $keyChar = mb_substr($word, $i, 1, 'utf-8');
+
+            // 获取子节点树结构
+            $tempTree = $tree->get($keyChar);
+
+            if ($tempTree) {
+                $tree = $tempTree;
+            } else {
+                // 设置标志位
+                $newTree = new HashMap();
+                $newTree->put('ending', false);
+
+                // 添加到集合
+                $tree->put($keyChar, $newTree);
+                $tree = $newTree;
+            }
+
+            // 到达最后一个节点
+            if ($i == $wordLength - 1) {
+                $tree->put('ending', true);
+            }
+        }
+
+        return;
+    }
+}

+ 186 - 0
application/common/library/Service.php

@@ -0,0 +1,186 @@
+<?php
+
+namespace app\common\library;
+
+use addons\shop\library\aip\AipContentCensor;
+use addons\shop\library\aip\AipNlp;
+use addons\shop\library\SensitiveHelper;
+use addons\shop\library\VicWord;
+use think\View;
+
+class Service
+{
+
+    /**
+     * 检测内容是否合法
+     * @param string $content 检测内容
+     * @param string $type    类型
+     * @return bool
+     */
+    public static function isContentLegal($content, $type = null)
+    {
+        $config = get_addon_config('shop');
+        $type = is_null($type) ? $config['audittype'] : $type;
+        if ($type == 'local') {
+            // 敏感词过滤
+            $handle = SensitiveHelper::init()->setTreeByFile(ADDON_PATH . 'shop/data/words.dic');
+            //首先检测是否合法
+            $isLegal = $handle->islegal($content);
+            return $isLegal ? true : false;
+        } elseif ($type == 'baiduyun') {
+            $client = new AipContentCensor($config['aip_appid'], $config['aip_apikey'], $config['aip_secretkey']);
+            $result = $client->textCensorUserDefined($content);
+            if (!isset($result['conclusionType']) || $result['conclusionType'] > 1) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 获取标题的关键字
+     * @param $title
+     * @return array
+     */
+    public static function getContentTags($title)
+    {
+        $arr = [];
+        $config = get_addon_config('shop');
+        if ($config['nlptype'] == 'local') {
+            !defined('_VIC_WORD_DICT_PATH_') && define('_VIC_WORD_DICT_PATH_', ADDON_PATH . 'shop/data/dict.json');
+            $handle = new VicWord('json');
+            $result = $handle->getAutoWord($title);
+            foreach ($result as $index => $item) {
+                $arr[] = $item[0];
+            }
+        } else {
+            $client = new AipNlp($config['aip_appid'], $config['aip_apikey'], $config['aip_secretkey']);
+            $result = $client->lexer($title);
+            if (isset($result['items'])) {
+                foreach ($result['items'] as $index => $item) {
+                    if (!in_array($item['pos'], ['v', 'vd', 'nd', 'a', 'ad', 'an', 'd', 'm', 'q', 'r', 'p', 'c', 'u', 'xc', 'w'])) {
+                        $arr[] = $item['item'];
+                    }
+                }
+            }
+        }
+        foreach ($arr as $index => $item) {
+            if (mb_strlen($item) == 1) {
+                unset($arr[$index]);
+            }
+        }
+        return array_filter(array_unique($arr));
+    }
+
+    /**
+     * 内容关键字自动加链接
+     */
+    public static function autolinks($value)
+    {
+        $links = [];
+
+        $value = preg_replace_callback('~(<a .*?>.*?</a>|<.*?>)~i', function ($match) use (&$links) {
+            return '<' . array_push($links, $match[1]) . '>';
+        }, $value);
+
+        $config = get_addon_config('shop');
+        $autolinks = $config['autolinks'];
+        $value = preg_replace_callback('/(' . implode('|', array_keys($autolinks)) . ')/i', function ($match) use ($autolinks) {
+            if (!isset($autolinks[$match[1]])) {
+                return $match[0];
+            } else {
+                return '<a href="' . $autolinks[$match[1]] . '" target="_blank">' . $match[0] . '</a>';
+            }
+        }, $value);
+        return preg_replace_callback('/<(\d+)>/', function ($match) use (&$links) {
+            return $links[$match[1] - 1];
+        }, $value);
+    }
+
+    /**
+     * @ 获取商品模板
+     * @return void
+     */
+    public static function getSourceTpl($tpl_id, $source_id)
+    {
+        if (!$tpl_id || !$source_id) {
+            return '';
+        }
+        $row = \addons\shop\model\Card::where('status', 'normal')->where('id', $tpl_id)->find();
+        if (empty($row)) {
+            return '';
+        }
+        $source = null;
+        switch ($row['type']) {
+            case 1:
+                $source = \addons\shop\model\Coupon::getCouponInfo($source_id);
+                break;
+            default:
+                $source = \addons\shop\model\Goods::get($source_id);
+        }
+        if (empty($source)) {
+            return '';
+        }
+        $view = new View();
+        $html = $view->fetch($row->content, ['source' => $source, 'tpl' => $row], [], [], true);
+        return $html;
+    }
+
+    /**
+     * @ 替换多余的卡片信息
+     * @param $value
+     * @return void
+     */
+    public static function replaceSourceTpl($value)
+    {
+        if (!empty($value)) {
+            return preg_replace('/(?<=data-id=\"shop\"\>).*?(?=\<div\s+data-id="end"\>(\s|[\r\n]|&nbsp;)*\<\/div\>)/is', '&nbsp;', $value);
+        }
+        return $value;
+    }
+
+    /**
+     * @ 商品模板转化
+     * @return void
+     */
+    public static function formatSourceTpl($value)
+    {
+        if (!empty($value)) {
+            $value = preg_replace_callback('/\<div\s+data-tpl\=\"(\d+)"\s+data\-source=\"(\w+)\"[\s\S]*?\<div\s+data-id\=\"end\"\>[\s\S]*?\<\/div\>[\s\S]*?\<\/div\>/i', function ($match) {
+                return \addons\shop\library\Service::getSourceTpl($match[1], $match[2]);
+            }, $value);
+        }
+        return $value;
+    }
+
+
+    /**
+     * @ uniapp商品模板转化
+     * @return void
+     */
+    public static function formatTplToUniapp($value)
+    {
+        if (!empty($value)) {
+            $value = preg_replace_callback("/href=\"(.*?)\"(\s|[\r\n]|&nbsp;)*data\-type=\"(goods|coupon)\"(\s|[\r\n]|&nbsp;)*data\-id=\"(\w+)\"/is", function ($matches) {
+                return 'href="/pages/' . $matches[3] . '/detail?id=' . $matches[5] . '"';
+            }, $value);
+        }
+        return $value;
+    }
+
+    /**
+     * 获取缓存标签和时长
+     * @param string $type
+     * @param array  $tag
+     * @return array
+     */
+    public static function getCacheKeyExpire($type, $tag = [])
+    {
+        $config = get_addon_config('shop');
+        $cache = !isset($tag['cache']) ? $config['cachelifetime'] : $tag['cache'];
+        $cache = in_array($cache, ['true', 'false', true, false], true) ? (in_array($cache, ['true', true], true) ? 0 : -1) : (int)$cache;
+        $cacheKey = $cache > -1 ? "shop-taglib-{$type}-" . md5(serialize($tag)) : false;
+        $cacheExpire = $cache > -1 ? $cache : null;
+        return [$cacheKey, $cacheExpire];
+    }
+}

+ 44 - 0
application/common/library/Theme.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace app\common\library;
+
+class Theme
+{
+    private static $config = [];
+
+    public static function get()
+    {
+        if (empty(self::$config)) {
+            $config = (array)json_decode(file_get_contents(self::getConfigFile()), true);
+            self::$config = $config;
+        }
+        return self::$config;
+    }
+
+    public static function set($config, $overwrite = false)
+    {
+        self::$config = $overwrite ? $config : array_merge(self::$config, $config);
+        file_put_contents(self::getConfigFile(), json_encode(self::$config, JSON_UNESCAPED_UNICODE));
+        return self::$config;
+    }
+
+    public static function render($config)
+    {
+        if (isset($config['tabbar']['list']) && is_array($config['tabbar']['list'])) {
+            $url = url('/', '', false, true);
+            $url = preg_replace("/\/([\w]+)\.php\//i", "/", $url);
+            $url = rtrim($url, "/");
+            foreach ($config['tabbar']['list'] as $index => &$item) {
+                $item['image'] = preg_match("/^\/assets\/addons/", $item['image']) ? $url . $item['image'] : cdnurl($item['image'], true);
+                $item['selectedImage'] = preg_match("/^\/assets\/addons/", $item['selectedImage']) ? $url . $item['selectedImage'] : cdnurl($item['selectedImage'], true);
+            }
+        }
+        return $config;
+    }
+
+    public static function getConfigFile()
+    {
+        return ADDON_PATH . 'shop' . DS . 'data' . DS . 'theme.json';
+    }
+
+}

+ 114 - 0
application/common/library/VicDict.php

@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * Created by PhpStorm.
+ * User: tanszhe
+ * Date: 2017/12/21
+ * Time: 下午8:16
+ */
+
+namespace app\common\library;
+
+class VicDict
+{
+    private $word = [];
+    /**
+     * 词典地址
+     * @var string
+     */
+    private $code = 'utf-8';
+
+    private $end = ['\\' => 1];
+
+    private $default_end = ['\\' => 1];
+
+    private $end_key = '\\';
+
+    private $type = 'igb';
+
+    public function __construct($type = 'igb')
+    {
+        $this->type = $type;
+        if (file_exists(_VIC_WORD_DICT_PATH_)) {
+            if ($type == 'igb') {
+                $this->word = igbinary_unserialize(file_get_contents(_VIC_WORD_DICT_PATH_));
+            } else {
+                $this->word = json_decode(file_get_contents(_VIC_WORD_DICT_PATH_), true);
+            }
+        }
+    }
+
+    /**
+     * @param string $word
+     * @param null|string $x 词性
+     * @return bool
+     */
+    public function add($word, $x = null)
+    {
+        $this->end = ['\\x' => $x] + $this->default_end;
+        $word = $this->filter($word);
+        if ($word) {
+            return $this->merge($word);
+        }
+        return false;
+    }
+
+    private function merge($word)
+    {
+        $ar = $this->toArr($word);
+        $br = $ar;
+        $wr = &$this->word;
+        foreach ($ar as $i => $v) {
+            array_shift($br);
+            if (!isset($wr[$v])) {
+                $wr[$v] = $this->dict($br, $this->end);
+                return true;
+            } else {
+                $wr = &$wr[$v];
+            }
+        }
+        if (!isset($wr[$this->end_key])) {
+            foreach ($this->end as $k => $v) {
+                $wr[$k] = $v;
+                $wr[$k] = $v;
+            }
+        }
+        return true;
+    }
+
+    public function save()
+    {
+        if ($this->type == 'igb') {
+            $str = igbinary_serialize($this->word);
+        } else {
+            $str = json_encode($this->word);
+        }
+        return file_put_contents(_VIC_WORD_DICT_PATH_, $str);
+    }
+
+    private function filter($word)
+    {
+        return str_replace(["\n", "\t"], '', trim($word));
+    }
+
+
+    private function dict($arr, $v, $i = 0)
+    {
+        if (isset($arr[$i])) {
+            return [$arr[$i] => $this->dict($arr, $v, $i + 1)];
+        } else {
+            return $v;
+        }
+    }
+
+    private function toArr($str)
+    {
+        $l = mb_strlen($str, $this->code);
+        $r = [];
+        for ($i = 0; $i < $l; $i++) {
+            $r[] = mb_substr($str, $i, 1, $this->code);
+        }
+        return $r;
+    }
+
+}

+ 270 - 0
application/common/library/VicWord.php

@@ -0,0 +1,270 @@
+<?php
+
+namespace app\common\library;
+/**
+ * Created by PhpStorm.
+ * User: tanszhe
+ * Date: 2017/12/21
+ * Time: 下午8:11
+ */
+
+
+class VicWord
+{
+    private $dict = [];
+
+    private $end = '\\';
+
+    private $auto = false;
+
+    private $count = 0;
+
+    /**
+     * @var string 词性
+     */
+    private $x = '\\x';
+
+    public function __construct($type = 'igb')
+    {
+        if (!file_exists(_VIC_WORD_DICT_PATH_)) {
+            return false;
+        }
+        if ($type == 'igb') {
+            $this->dict = igbinary_unserialize(file_get_contents(_VIC_WORD_DICT_PATH_));
+        } else {
+            $this->dict = json_decode(file_get_contents(_VIC_WORD_DICT_PATH_), true);
+        }
+    }
+
+    /**
+     * @param string $path
+     */
+    public function getWord($str)
+    {
+        $this->auto = false;
+        $str = $this->filter($str);
+        return $this->find($str);
+    }
+
+    /**
+     * @param string $path
+     */
+    public function getShortWord($str)
+    {
+        $this->auto = false;
+        $str = $this->filter($str);
+        return $this->shortfind($str);
+    }
+
+    /**
+     * @param string $path
+     */
+    public function getAutoWord($str)
+    {
+        $this->auto = true;
+        $str = $this->filter($str);
+        return $this->autoFind($str, ['long' => 1]);
+    }
+
+
+    private function filter($str)
+    {
+        return strtolower(trim($str));
+    }
+
+
+    private function getD(&$str, $i)
+    {
+        $o = ord($str[$i]);
+        if ($o < 128) {
+            $d = $str[$i];
+        } else {
+            $o = $o >> 4;
+            if ($o == 12) {
+                $d = $str[$i] . $str[++$i];
+            } elseif ($o === 14) {
+                $d = $str[$i] . $str[++$i] . $str[++$i];
+            } elseif ($o == 15) {
+                $d = $str[$i] . $str[++$i] . $str[++$i] . $str[++$i];
+            } else {
+                exit('我不认识的编码');
+            }
+        }
+        return [$d, $i];
+    }
+
+    private function autoFind($str, $auto_info = [])
+    {
+        if ($auto_info['long']) {
+            return $this->find($str, $auto_info);
+        } else {
+            return $this->shortfind($str, $auto_info);
+        }
+    }
+
+    private function reGet(&$r, $auto_info)
+    {
+        $auto_info['c'] = isset($auto_info['c']) ? $auto_info['c']++ : 1;
+        $l = count($r) - 1;
+        $p = [];
+        $str = '';
+        for ($i = $l; $i >= 0; $i--) {
+            $str = $r[$i][0] . $str;
+            $f = $r[$i][3];
+            array_unshift($p, $r[$i]);
+            unset($r[$i]);
+            if ($f == 1) {
+                break;
+            }
+        }
+        $this->count++;
+        $l = strlen($str);
+        if (isset($r[$i - 1])) {
+            $w = $r[$i - 1][1];
+        } else {
+            $w = 0;
+        }
+        if (isset($auto_info['pl']) && $l == $auto_info['pl']) {
+            $r = $p;
+            return false;
+        } elseif ($str && $auto_info['c'] < 3) {
+            $auto_info['pl'] = $l;
+            $auto_info['long'] = !$auto_info['long'];
+            $sr = $this->autoFind($str, $auto_info);
+            $sr = array_map(function ($v) use ($w) {
+                $v[1] += $w;
+                return $v;
+            }, $sr);
+            $r = array_merge($r, $this->getGoodWord($p, $sr));
+        }
+    }
+
+    private function getGoodWord($old, $new)
+    {
+        if (!$new) {
+            return $old;
+        }
+        if ($this->getUnknowCount($old) > $this->getUnknowCount($new)) {
+            return $new;
+        } else {
+            return $old;
+        }
+    }
+
+    private function getUnknowCount($ar)
+    {
+        $i = 0;
+        foreach ($ar as $v) {
+            if ($v[3] == 0) {
+                $i += strlen($v[0]);
+            }
+        }
+        return $i;
+    }
+
+
+    private function find($str, $auto_info = [])
+    {
+        $len = strlen($str);
+        $s = '';
+        $n = '';
+        $j = 0;
+        $r = [];
+        for ($i = 0; $i < $len; $i++) {
+            list($d, $i) = $this->getD($str, $i);
+
+            if (isset($wr[$d])) {
+                $s .= $d;
+                $wr = $wr[$d];
+            } else {
+                if (isset($wr[$this->end])) {
+                    $this->addNotFind($r, $n, $s, $j, $auto_info);
+                    $this->addResult($r, $s, $j, $wr[$this->x]);
+                    $n = '';
+                }
+                $wr = $this->dict;
+                if (isset($wr[$d])) {
+                    $s = $d;
+                    $wr = $wr[$d];
+                } else {
+                    $s = '';
+                }
+            }
+            $n .= $d;
+            $j = $i;
+        }
+        if (isset($wr[$this->end])) {
+            $this->addNotFind($r, $n, $s, $i, $auto_info);
+            $this->addResult($r, $s, $i, $wr[$this->x]);
+        } else {
+            $this->addNotFind($r, $n, '', $i, $auto_info);
+        }
+
+        return $r;
+    }
+
+
+    private function addNotFind(&$r, $n, $s, $i, $auto_info = [])
+    {
+        if ($n !== $s) {
+            $n = str_replace($s, '', $n);
+            $this->addResult($r, $n, $i - strlen($s), null, 0);
+            if ($this->auto) {
+                $this->reGet($r, $auto_info);
+            }
+        }
+    }
+
+
+    private function shortFind($str, $auto_info = [])
+    {
+        $len = strlen($str);
+        $s = '';
+        $n = '';
+        $r = [];
+        for ($i = 0; $i < $len; $i++) {
+            $j = $i;
+            list($d, $i) = $this->getD($str, $i);
+
+            if (isset($wr[$d])) {
+                $s .= $d;
+                $wr = $wr[$d];
+            } else {
+                if (isset($wr[$this->end])) {
+                    $this->addNotFind($r, $n, $s, $j, $auto_info);
+                    $this->addResult($r, $s, $j, $wr[$this->x]);
+                    $n = '';
+                }
+                $wr = $this->dict;
+                if (isset($wr[$d])) {
+                    $s = $d;
+                    $wr = $wr[$d];
+                } else {
+                    $s = '';
+                }
+            }
+
+            $n .= $d;
+
+            if (isset($wr[$this->end])) {
+                $this->addNotFind($r, $n, $s, $i, $auto_info);
+                $this->addResult($r, $s, $i, $wr[$this->x]);
+                $wr = $this->dict;
+                $s = '';
+                $n = '';
+            }
+        }
+        if (isset($wr[$this->end])) {
+            $this->addNotFind($r, $n, $s, $i, $auto_info);
+            $this->addResult($r, $s, $i, $wr[$this->x]);
+        } else {
+            $this->addNotFind($r, $n, '', $i, $auto_info);
+        }
+        return $r;
+    }
+
+    private function addResult(&$r, $k, $i, $x, $find = 1)
+    {
+        $r[] = [$k, $i, $x, $find];
+    }
+}

+ 139 - 0
application/common/library/Wechat/Service.php

@@ -0,0 +1,139 @@
+<?php
+
+namespace app\common\library\Wechat;
+
+use fast\Http;
+use GuzzleHttp\Client;
+use GuzzleHttp\Psr7\Response;
+use think\Cache;
+use think\Log;
+use think\Config;
+
+/**
+ * 小程序服务类
+ */
+class Service
+{
+    private $appId = '';
+    private $appSecret = '';
+
+    public function __construct()
+    {
+        $config = get_addon_config('shop');
+        $this->appId = $config['wx_appid'];
+        $this->appSecret = $config['wx_app_secret'];
+    }
+
+    /**
+     * 批量并发发送
+     * @param array $pushList
+     * @return bool
+     */
+    public function subscribeMessageSendMultiple($pushList)
+    {
+        $url = 'https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=' . $this->getAccessToken();
+        $concurrentSize = 10;
+        $promises = [];
+        $client = new Client();
+        $promiseProcess = function ($promises) {
+            $results = \GuzzleHttp\Promise\Utils::unwrap($promises);
+            foreach ($results as $key => $response) {
+                $res = (array)json_decode($response->getBody()->getContents(), true);
+                if (isset($res['errmsg']) && Config::get('app_debug')) {
+                    Log::write($res, 'send_msg');
+                }
+            }
+        };
+        foreach ($pushList as $index => $item) {
+            $promises[] = $client->postAsync($url, ['body' => json_encode($item, JSON_UNESCAPED_UNICODE)]);
+            if (count($promises) == $concurrentSize) {
+                $promiseProcess($promises);
+                $promises = [];
+            }
+        }
+        if ($promises) {
+            $promiseProcess($promises);
+        }
+        return true;
+    }
+
+    /**
+     * 单次异步发送
+     * @param array $pushData
+     * @return bool
+     */
+    public function subscribeMessageSend($pushData)
+    {
+        $url = 'https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=' . $this->getAccessToken();
+        $client = new Client();
+        $client->postAsync($url, ['body' => json_encode($pushData, JSON_UNESCAPED_UNICODE)])->then(function (Response $response) {
+            $res = (array)json_decode($response->getBody()->getContents(), true);
+            if (isset($res['errmsg']) && Config::get('app_debug')) {
+                Log::write($res, 'send_msg');
+                return false;
+            }
+        })->wait();
+        return true;
+    }
+
+    //获取access_token
+    public function getAccessToken()
+    {
+        $access_token = Cache::get('shop' . $this->appId);
+        if (!$access_token) {
+            $url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' . $this->appId . '&secret=' . $this->appSecret;
+            $res = Http::get($url);
+            $res = (array)json_decode($res, true);
+            if (isset($res['access_token'])) {
+                $access_token = $res['access_token'];
+            } elseif (Config::get('app_debug')) {
+                Log::write('code:' . $res['errcode'] . ',message:' . $res['errmsg'], 'access_token');
+            }
+            Cache::set('shop' . $this->appId, $access_token, 7000);
+        }
+        return $access_token;
+    }
+
+    //生成小程序码
+    public function getWxCodeUnlimited($param)
+    {
+        $url = 'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=' . $this->getAccessToken();
+        $data = array_merge([
+            'width' => '280'
+        ], $param);
+        return Http::post($url, json_encode($data, JSON_UNESCAPED_UNICODE));
+    }
+
+    //获取手机号
+    public function getWechatMobile($code)
+    {
+        $access_token = $this->getAccessToken();
+        $url = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={$access_token}";
+        $res = Http::post($url, json_encode(['code' => $code]));
+        $res = (array)json_decode($res, true);
+        if (!isset($res['phone_info']) && config('app_debug')) {
+            \think\Log::write($res);
+        }
+        return $res['phone_info'] ?? [];
+    }
+
+    //获取Session信息
+    public function getWechatSession($code)
+    {
+        $params = [
+            'appid'      => $this->appId,
+            'secret'     => $this->appSecret,
+            'js_code'    => $code,
+            'grant_type' => 'authorization_code'
+        ];
+        $result = Http::sendRequest("https://api.weixin.qq.com/sns/jscode2session", $params, 'GET');
+        if ($result['ret']) {
+            $json = (array)json_decode($result['msg'], true);
+            return $json;
+        }
+        if (config('app_debug')) {
+            Log::write($result);
+        }
+        return ['errmsg' => config('app_debug') ? $result['msg'] : '网络错误'];
+    }
+}

+ 27 - 0
application/common/library/aip/AipContentCensor.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace app\common\library\aip;
+
+/*
+* Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
+*
+* Licensed under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License. You may obtain a copy of
+* the License at
+*
+* Http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+* License for the specific language governing permissions and limitations under
+* the License.
+*/
+
+/**
+ * 内容审核
+ */
+class AipContentCensor extends AipImageCensor
+{
+
+}

+ 74 - 0
application/common/library/aip/AipImageCensor.php

@@ -0,0 +1,74 @@
+<?php
+
+namespace app\common\library\aip;
+
+/*
+* Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
+*
+* Licensed under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License. You may obtain a copy of
+* the License at
+*
+* Http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+* License for the specific language governing permissions and limitations under
+* the License.
+*/
+
+
+use addons\shop\library\aip\lib\AipBase;
+
+/**
+ * 黄反识别
+ */
+class AipImageCensor extends AipBase
+{
+
+    /**
+     * @var string
+     */
+    private $imageCensorUserDefinedUrl = 'https://aip.baidubce.com/rest/2.0/solution/v1/img_censor/v2/user_defined';
+
+    /**
+     * @var string
+     */
+    private $textCensorUserDefinedUrl = 'https://aip.baidubce.com/rest/2.0/solution/v1/text_censor/v2/user_defined';
+
+
+    /**
+     * @param string $image 图像
+     * @return array
+     */
+    public function imageCensorUserDefined($image)
+    {
+
+        $data = array();
+
+        $isUrl = substr(trim($image), 0, 4) === 'http';
+        if (!$isUrl) {
+            $data['image'] = base64_encode($image);
+        } else {
+            $data['imgUrl'] = $image;
+        }
+
+        return $this->request($this->imageCensorUserDefinedUrl, $data);
+    }
+
+    /**
+     * @param string $text
+     * @return array
+     */
+    public function textCensorUserDefined($text)
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        return $this->request($this->textCensorUserDefinedUrl, $data);
+    }
+
+}

+ 456 - 0
application/common/library/aip/AipNlp.php

@@ -0,0 +1,456 @@
+<?php
+
+namespace app\common\library\aip;
+
+/*
+* Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
+*
+* Licensed under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License. You may obtain a copy of
+* the License at
+*
+* Http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+* License for the specific language governing permissions and limitations under
+* the License.
+*/
+
+use addons\shop\library\aip\lib\AipBase;
+
+class AipNlp extends AipBase
+{
+
+    /**
+     * 词法分析 lexer api url
+     * @var string
+     */
+    private $lexerUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/lexer';
+
+    /**
+     * 词法分析(定制版) lexer_custom api url
+     * @var string
+     */
+    private $lexerCustomUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/lexer_custom';
+
+    /**
+     * 依存句法分析 dep_parser api url
+     * @var string
+     */
+    private $depParserUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/depparser';
+
+    /**
+     * 词向量表示 word_embedding api url
+     * @var string
+     */
+    private $wordEmbeddingUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v2/word_emb_vec';
+
+    /**
+     * DNN语言模型 dnnlm_cn api url
+     * @var string
+     */
+    private $dnnlmCnUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v2/dnnlm_cn';
+
+    /**
+     * 词义相似度 word_sim_embedding api url
+     * @var string
+     */
+    private $wordSimEmbeddingUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v2/word_emb_sim';
+
+    /**
+     * 短文本相似度 simnet api url
+     * @var string
+     */
+    private $simnetUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v2/simnet';
+
+    /**
+     * 评论观点抽取 comment_tag api url
+     * @var string
+     */
+    private $commentTagUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v2/comment_tag';
+
+    /**
+     * 情感倾向分析 sentiment_classify api url
+     * @var string
+     */
+    private $sentimentClassifyUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/sentiment_classify';
+
+    /**
+     * 文章标签 keyword api url
+     * @var string
+     */
+    private $keywordUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/keyword';
+
+    /**
+     * 文章分类 topic api url
+     * @var string
+     */
+    private $topicUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/topic';
+
+    /**
+     * 文本纠错 ecnet api url
+     * @var string
+     */
+    private $ecnetUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/ecnet';
+
+    /**
+     * 对话情绪识别接口 emotion api url
+     * @var string
+     */
+    private $emotionUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/emotion';
+
+    /**
+     * 新闻摘要接口 news_summary api url
+     * @var string
+     */
+    private $newsSummaryUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/news_summary';
+
+    /**
+     * 地址识别接口 address api url
+     * @var string
+     */
+    private $addressUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/address';
+
+    /**
+     * 格式化结果
+     * @param $content string
+     * @return mixed
+     */
+    protected function proccessResult($content)
+    {
+        return json_decode(mb_convert_encoding($content, 'UTF8', 'GBK'), true, 512, JSON_BIGINT_AS_STRING);
+    }
+
+    /**
+     * 词法分析接口
+     *
+     * @param string $text    - 待分析文本(目前仅支持GBK编码),长度不超过65536字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function lexer($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->lexerUrl, $data);
+    }
+
+    /**
+     * 词法分析(定制版)接口
+     *
+     * @param string $text    - 待分析文本(目前仅支持GBK编码),长度不超过65536字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function lexerCustom($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->lexerCustomUrl, $data);
+    }
+
+    /**
+     * 依存句法分析接口
+     *
+     * @param string $text    - 待分析文本(目前仅支持GBK编码),长度不超过256字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        mode 模型选择。默认值为0,可选值mode=0(对应web模型);mode=1(对应query模型)
+     * @return array
+     */
+    public function depParser($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->depParserUrl, $data);
+    }
+
+    /**
+     * 词向量表示接口
+     *
+     * @param string $word    - 文本内容(GBK编码),最大64字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function wordEmbedding($word, $options = array())
+    {
+
+        $data = array();
+
+        $data['word'] = $word;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->wordEmbeddingUrl, $data);
+    }
+
+    /**
+     * DNN语言模型接口
+     *
+     * @param string $text    - 文本内容(GBK编码),最大512字节,不需要切词
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function dnnlm($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->dnnlmCnUrl, $data);
+    }
+
+    /**
+     * 词义相似度接口
+     *
+     * @param string $word1   - 词1(GBK编码),最大64字节
+     * @param string $word2   - 词1(GBK编码),最大64字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        mode 预留字段,可选择不同的词义相似度模型。默认值为0,目前仅支持mode=0
+     * @return array
+     */
+    public function wordSimEmbedding($word1, $word2, $options = array())
+    {
+
+        $data = array();
+
+        $data['word_1'] = $word1;
+        $data['word_2'] = $word2;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->wordSimEmbeddingUrl, $data);
+    }
+
+    /**
+     * 短文本相似度接口
+     *
+     * @param string $text1   - 待比较文本1(GBK编码),最大512字节
+     * @param string $text2   - 待比较文本2(GBK编码),最大512字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        model 默认为"BOW",可选"BOW"、"CNN"与"GRNN"
+     * @return array
+     */
+    public function simnet($text1, $text2, $options = array())
+    {
+
+        $data = array();
+
+        $data['text_1'] = $text1;
+        $data['text_2'] = $text2;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->simnetUrl, $data);
+    }
+
+    /**
+     * 评论观点抽取接口
+     *
+     * @param string $text    - 评论内容(GBK编码),最大10240字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        type 评论行业类型,默认为4(餐饮美食)
+     * @return array
+     */
+    public function commentTag($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->commentTagUrl, $data);
+    }
+
+    /**
+     * 情感倾向分析接口
+     *
+     * @param string $text    - 文本内容(GBK编码),最大102400字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function sentimentClassify($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->sentimentClassifyUrl, $data);
+    }
+
+    /**
+     * 文章标签接口
+     *
+     * @param string $title   - 篇章的标题,最大80字节
+     * @param string $content - 篇章的正文,最大65535字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function keyword($title, $content, $options = array())
+    {
+
+        $data = array();
+
+        $data['title'] = $title;
+        $data['content'] = $content;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->keywordUrl, $data);
+    }
+
+    /**
+     * 文章分类接口
+     *
+     * @param string $title   - 篇章的标题,最大80字节
+     * @param string $content - 篇章的正文,最大65535字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function topic($title, $content, $options = array())
+    {
+
+        $data = array();
+
+        $data['title'] = $title;
+        $data['content'] = $content;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->topicUrl, $data);
+    }
+
+    /**
+     * 文本纠错接口
+     *
+     * @param string $text    - 待纠错文本,输入限制511字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function ecnet($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->ecnetUrl, $data);
+    }
+
+    /**
+     * 对话情绪识别接口接口
+     *
+     * @param string $text    - 待识别情感文本,输入限制512字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        scene default(默认项-不区分场景),talk(闲聊对话-如度秘聊天等),task(任务型对话-如导航对话等),customer_service(客服对话-如电信/银行客服等)
+     * @return array
+     */
+    public function emotion($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->emotionUrl, $data);
+    }
+
+    /**
+     * 新闻摘要接口接口
+     *
+     * @param string  $content       - 字符串(限3000字符数以内)字符串仅支持GBK编码,长度需小于3000字符数(即6000字节),请输入前确认字符数没有超限,若字符数超长会返回错误。正文中如果包含段落信息,请使用"\n"分隔,段落信息算法中有重要的作用,请尽量保留
+     * @param integer $maxSummaryLen - 此数值将作为摘要结果的最大长度。例如:原文长度1000字,本参数设置为150,则摘要结果的最大长度是150字;推荐最优区间:200-500字
+     * @param array   $options       - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                               title 字符串(限200字符数)字符串仅支持GBK编码,长度需小于200字符数(即400字节),请输入前确认字符数没有超限,若字符数超长会返回错误。标题在算法中具有重要的作用,若文章确无标题,输入参数的“标题”字段为空即可
+     * @return array
+     */
+    public function newsSummary($content, $maxSummaryLen, $options = array())
+    {
+
+        $data = array();
+
+        $data['content'] = $content;
+        $data['max_summary_len'] = $maxSummaryLen;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->newsSummaryUrl, $data);
+    }
+
+    /**
+     * 地址识别接口接口
+     *
+     * @param string $text    - 待识别的文本内容,不超过1000字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function address($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->addressUrl, $data);
+    }
+}

+ 1155 - 0
application/common/library/aip/AipOcr.php

@@ -0,0 +1,1155 @@
+<?php
+
+namespace app\common\library\aip;
+
+/*
+* Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
+*
+* Licensed under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License. You may obtain a copy of
+* the License at
+*
+* Http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+* License for the specific language governing permissions and limitations under
+* the License.
+*/
+
+use addons\shop\library\aip\lib\AipBase;
+
+class AipOcr extends AipBase
+{
+
+    /**
+     * 通用文字识别 general_basic api url
+     * @var string
+     */
+    private $generalBasicUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic';
+
+    /**
+     * 通用文字识别(高精度版) accurate_basic api url
+     * @var string
+     */
+    private $accurateBasicUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/accurate_basic';
+
+    /**
+     * 通用文字识别(含位置信息版) general api url
+     * @var string
+     */
+    private $generalUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/general';
+
+    /**
+     * 通用文字识别(含位置高精度版) accurate api url
+     * @var string
+     */
+    private $accurateUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/accurate';
+
+    /**
+     * 通用文字识别(含生僻字版) general_enhanced api url
+     * @var string
+     */
+    private $generalEnhancedUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/general_enhanced';
+
+    /**
+     * 网络图片文字识别 web_image api url
+     * @var string
+     */
+    private $webImageUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/webimage';
+
+    /**
+     * 身份证识别 idcard api url
+     * @var string
+     */
+    private $idcardUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/idcard';
+
+    /**
+     * 银行卡识别 bankcard api url
+     * @var string
+     */
+    private $bankcardUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/bankcard';
+
+    /**
+     * 驾驶证识别 driving_license api url
+     * @var string
+     */
+    private $drivingLicenseUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/driving_license';
+
+    /**
+     * 行驶证识别 vehicle_license api url
+     * @var string
+     */
+    private $vehicleLicenseUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/vehicle_license';
+
+    /**
+     * 车牌识别 license_plate api url
+     * @var string
+     */
+    private $licensePlateUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/license_plate';
+
+    /**
+     * 营业执照识别 business_license api url
+     * @var string
+     */
+    private $businessLicenseUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/business_license';
+
+    /**
+     * 通用票据识别 receipt api url
+     * @var string
+     */
+    private $receiptUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/receipt';
+
+    /**
+     * 火车票识别 train_ticket api url
+     * @var string
+     */
+    private $trainTicketUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/train_ticket';
+
+    /**
+     * 出租车票识别 taxi_receipt api url
+     * @var string
+     */
+    private $taxiReceiptUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/taxi_receipt';
+
+    /**
+     * 表格文字识别同步接口 form api url
+     * @var string
+     */
+    private $formUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/form';
+
+    /**
+     * 表格文字识别 table_recognize api url
+     * @var string
+     */
+    private $tableRecognizeUrl = 'https://aip.baidubce.com/rest/2.0/solution/v1/form_ocr/request';
+
+    /**
+     * 表格识别结果 table_result_get api url
+     * @var string
+     */
+    private $tableResultGetUrl = 'https://aip.baidubce.com/rest/2.0/solution/v1/form_ocr/get_request_result';
+
+    /**
+     * VIN码识别 vin_code api url
+     * @var string
+     */
+    private $vinCodeUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/vin_code';
+
+    /**
+     * 定额发票识别 quota_invoice api url
+     * @var string
+     */
+    private $quotaInvoiceUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/quota_invoice';
+
+    /**
+     * 户口本识别 household_register api url
+     * @var string
+     */
+    private $householdRegisterUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/household_register';
+
+    /**
+     * 港澳通行证识别 HK_Macau_exitentrypermit api url
+     * @var string
+     */
+    private $HKMacauExitentrypermitUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/HK_Macau_exitentrypermit';
+
+    /**
+     * 台湾通行证识别 taiwan_exitentrypermit api url
+     * @var string
+     */
+    private $taiwanExitentrypermitUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/taiwan_exitentrypermit';
+
+    /**
+     * 出生医学证明识别 birth_certificate api url
+     * @var string
+     */
+    private $birthCertificateUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/birth_certificate';
+
+    /**
+     * 机动车销售发票识别 vehicle_invoice api url
+     * @var string
+     */
+    private $vehicleInvoiceUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/vehicle_invoice';
+
+    /**
+     * 车辆合格证识别 vehicle_certificate api url
+     * @var string
+     */
+    private $vehicleCertificateUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/vehicle_certificate';
+
+    /**
+     * 税务局通用机打发票识别 invoice api url
+     * @var string
+     */
+    private $invoiceUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/invoice';
+
+    /**
+     * 行程单识别 air_ticket api url
+     * @var string
+     */
+    private $airTicketUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/air_ticket';
+
+    /**
+     * 保单识别 insurance_documents api url
+     * @var string
+     */
+    private $insuranceDocumentsUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/insurance_documents';
+
+    /**
+     * 增值税发票识别 vat_invoice api url
+     * @var string
+     */
+    private $vatInvoiceUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/vat_invoice';
+
+    /**
+     * 二维码识别 qrcode api url
+     * @var string
+     */
+    private $qrcodeUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/qrcode';
+
+    /**
+     * 数字识别 numbers api url
+     * @var string
+     */
+    private $numbersUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/numbers';
+
+    /**
+     * 彩票识别 lottery api url
+     * @var string
+     */
+    private $lotteryUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/lottery';
+
+    /**
+     * 护照识别 passport api url
+     * @var string
+     */
+    private $passportUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/passport';
+
+    /**
+     * 名片识别 business_card api url
+     * @var string
+     */
+    private $businessCardUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/business_card';
+
+    /**
+     * 手写文字识别 handwriting api url
+     * @var string
+     */
+    private $handwritingUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/handwriting';
+
+    /**
+     * 自定义模板文字识别 custom api url
+     * @var string
+     */
+    private $customUrl = 'https://aip.baidubce.com/rest/2.0/solution/v1/iocr/recognise';
+
+
+    /**
+     * 通用文字识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        language_type 识别语言类型,默认为CHN_ENG。可选值包括:<br>- CHN_ENG:中英文混合;<br>- ENG:英文;<br>- POR:葡萄牙语;<br>- FRE:法语;<br>- GER:德语;<br>- ITA:意大利语;<br>- SPA:西班牙语;<br>- RUS:俄语;<br>- JAP:日语;<br>- KOR:韩语;
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        detect_language 是否检测语言,默认不检测。当前支持(中文、英语、日语、韩语)
+     *                        probability 是否返回识别结果中每一行的置信度
+     * @return array
+     */
+    public function basicGeneral($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->generalBasicUrl, $data);
+    }
+
+    /**
+     * 通用文字识别接口
+     *
+     * @param string $url     - 图片完整URL,URL长度不超过1024字节,URL对应的图片base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式,当image字段存在时url字段失效
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        language_type 识别语言类型,默认为CHN_ENG。可选值包括:<br>- CHN_ENG:中英文混合;<br>- ENG:英文;<br>- POR:葡萄牙语;<br>- FRE:法语;<br>- GER:德语;<br>- ITA:意大利语;<br>- SPA:西班牙语;<br>- RUS:俄语;<br>- JAP:日语;<br>- KOR:韩语;
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        detect_language 是否检测语言,默认不检测。当前支持(中文、英语、日语、韩语)
+     *                        probability 是否返回识别结果中每一行的置信度
+     * @return array
+     */
+    public function basicGeneralUrl($url, $options = array())
+    {
+
+        $data = array();
+
+        $data['url'] = $url;
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->generalBasicUrl, $data);
+    }
+
+    /**
+     * 通用文字识别(高精度版)接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        probability 是否返回识别结果中每一行的置信度
+     * @return array
+     */
+    public function basicAccurate($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->accurateBasicUrl, $data);
+    }
+
+    /**
+     * 通用文字识别(含位置信息版)接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        recognize_granularity 是否定位单字符位置,big:不定位单字符位置,默认值;small:定位单字符位置
+     *                        language_type 识别语言类型,默认为CHN_ENG。可选值包括:<br>- CHN_ENG:中英文混合;<br>- ENG:英文;<br>- POR:葡萄牙语;<br>- FRE:法语;<br>- GER:德语;<br>- ITA:意大利语;<br>- SPA:西班牙语;<br>- RUS:俄语;<br>- JAP:日语;<br>- KOR:韩语;
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        detect_language 是否检测语言,默认不检测。当前支持(中文、英语、日语、韩语)
+     *                        vertexes_location 是否返回文字外接多边形顶点位置,不支持单字位置。默认为false
+     *                        probability 是否返回识别结果中每一行的置信度
+     * @return array
+     */
+    public function general($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->generalUrl, $data);
+    }
+
+    /**
+     * 通用文字识别(含位置信息版)接口
+     *
+     * @param string $url     - 图片完整URL,URL长度不超过1024字节,URL对应的图片base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式,当image字段存在时url字段失效
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        recognize_granularity 是否定位单字符位置,big:不定位单字符位置,默认值;small:定位单字符位置
+     *                        language_type 识别语言类型,默认为CHN_ENG。可选值包括:<br>- CHN_ENG:中英文混合;<br>- ENG:英文;<br>- POR:葡萄牙语;<br>- FRE:法语;<br>- GER:德语;<br>- ITA:意大利语;<br>- SPA:西班牙语;<br>- RUS:俄语;<br>- JAP:日语;<br>- KOR:韩语;
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        detect_language 是否检测语言,默认不检测。当前支持(中文、英语、日语、韩语)
+     *                        vertexes_location 是否返回文字外接多边形顶点位置,不支持单字位置。默认为false
+     *                        probability 是否返回识别结果中每一行的置信度
+     * @return array
+     */
+    public function generalUrl($url, $options = array())
+    {
+
+        $data = array();
+
+        $data['url'] = $url;
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->generalUrl, $data);
+    }
+
+    /**
+     * 通用文字识别(含位置高精度版)接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        recognize_granularity 是否定位单字符位置,big:不定位单字符位置,默认值;small:定位单字符位置
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        vertexes_location 是否返回文字外接多边形顶点位置,不支持单字位置。默认为false
+     *                        probability 是否返回识别结果中每一行的置信度
+     * @return array
+     */
+    public function accurate($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->accurateUrl, $data);
+    }
+
+    /**
+     * 通用文字识别(含生僻字版)接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        language_type 识别语言类型,默认为CHN_ENG。可选值包括:<br>- CHN_ENG:中英文混合;<br>- ENG:英文;<br>- POR:葡萄牙语;<br>- FRE:法语;<br>- GER:德语;<br>- ITA:意大利语;<br>- SPA:西班牙语;<br>- RUS:俄语;<br>- JAP:日语;<br>- KOR:韩语;
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        detect_language 是否检测语言,默认不检测。当前支持(中文、英语、日语、韩语)
+     *                        probability 是否返回识别结果中每一行的置信度
+     * @return array
+     */
+    public function enhancedGeneral($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->generalEnhancedUrl, $data);
+    }
+
+    /**
+     * 通用文字识别(含生僻字版)接口
+     *
+     * @param string $url     - 图片完整URL,URL长度不超过1024字节,URL对应的图片base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式,当image字段存在时url字段失效
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        language_type 识别语言类型,默认为CHN_ENG。可选值包括:<br>- CHN_ENG:中英文混合;<br>- ENG:英文;<br>- POR:葡萄牙语;<br>- FRE:法语;<br>- GER:德语;<br>- ITA:意大利语;<br>- SPA:西班牙语;<br>- RUS:俄语;<br>- JAP:日语;<br>- KOR:韩语;
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        detect_language 是否检测语言,默认不检测。当前支持(中文、英语、日语、韩语)
+     *                        probability 是否返回识别结果中每一行的置信度
+     * @return array
+     */
+    public function enhancedGeneralUrl($url, $options = array())
+    {
+
+        $data = array();
+
+        $data['url'] = $url;
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->generalEnhancedUrl, $data);
+    }
+
+    /**
+     * 网络图片文字识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        detect_language 是否检测语言,默认不检测。当前支持(中文、英语、日语、韩语)
+     * @return array
+     */
+    public function webImage($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->webImageUrl, $data);
+    }
+
+    /**
+     * 网络图片文字识别接口
+     *
+     * @param string $url     - 图片完整URL,URL长度不超过1024字节,URL对应的图片base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式,当image字段存在时url字段失效
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        detect_language 是否检测语言,默认不检测。当前支持(中文、英语、日语、韩语)
+     * @return array
+     */
+    public function webImageUrl($url, $options = array())
+    {
+
+        $data = array();
+
+        $data['url'] = $url;
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->webImageUrl, $data);
+    }
+
+    /**
+     * 身份证识别接口
+     *
+     * @param string $image      - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param string $idCardSide - front:身份证含照片的一面;back:身份证带国徽的一面
+     * @param array  $options    - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                           detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                           detect_risk 是否开启身份证风险类型(身份证复印件、临时身份证、身份证翻拍、修改过的身份证)功能,默认不开启,即:false。可选值:true-开启;false-不开启
+     * @return array
+     */
+    public function idcard($image, $idCardSide, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+        $data['id_card_side'] = $idCardSide;
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->idcardUrl, $data);
+    }
+
+    /**
+     * 银行卡识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function bankcard($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->bankcardUrl, $data);
+    }
+
+    /**
+     * 驾驶证识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     * @return array
+     */
+    public function drivingLicense($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->drivingLicenseUrl, $data);
+    }
+
+    /**
+     * 行驶证识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        accuracy normal 使用快速服务,1200ms左右时延;缺省或其它值使用高精度服务,1600ms左右时延
+     * @return array
+     */
+    public function vehicleLicense($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->vehicleLicenseUrl, $data);
+    }
+
+    /**
+     * 车牌识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        multi_detect 是否检测多张车牌,默认为false,当置为true的时候可以对一张图片内的多张车牌进行识别
+     * @return array
+     */
+    public function licensePlate($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->licensePlateUrl, $data);
+    }
+
+    /**
+     * 营业执照识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function businessLicense($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->businessLicenseUrl, $data);
+    }
+
+    /**
+     * 通用票据识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        recognize_granularity 是否定位单字符位置,big:不定位单字符位置,默认值;small:定位单字符位置
+     *                        probability 是否返回识别结果中每一行的置信度
+     *                        accuracy normal 使用快速服务,1200ms左右时延;缺省或其它值使用高精度服务,1600ms左右时延
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     * @return array
+     */
+    public function receipt($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->receiptUrl, $data);
+    }
+
+    /**
+     * 火车票识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function trainTicket($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->trainTicketUrl, $data);
+    }
+
+    /**
+     * 出租车票识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function taxiReceipt($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->taxiReceiptUrl, $data);
+    }
+
+    /**
+     * 表格文字识别同步接口接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function form($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->formUrl, $data);
+    }
+
+    /**
+     * 表格文字识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function tableRecognitionAsync($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->tableRecognizeUrl, $data);
+    }
+
+    /**
+     * 表格识别结果接口
+     *
+     * @param string $requestId - 发送表格文字识别请求时返回的request id
+     * @param array  $options   - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                          result_type 期望获取结果的类型,取值为“excel”时返回xls文件的地址,取值为“json”时返回json格式的字符串,默认为”excel”
+     * @return array
+     */
+    public function getTableRecognitionResult($requestId, $options = array())
+    {
+
+        $data = array();
+
+        $data['request_id'] = $requestId;
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->tableResultGetUrl, $data);
+    }
+
+    /**
+     * VIN码识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function vinCode($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->vinCodeUrl, $data);
+    }
+
+    /**
+     * 定额发票识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function quotaInvoice($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->quotaInvoiceUrl, $data);
+    }
+
+    /**
+     * 户口本识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function householdRegister($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->householdRegisterUrl, $data);
+    }
+
+    /**
+     * 港澳通行证识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function HKMacauExitentrypermit($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->HKMacauExitentrypermitUrl, $data);
+    }
+
+    /**
+     * 台湾通行证识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function taiwanExitentrypermit($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->taiwanExitentrypermitUrl, $data);
+    }
+
+    /**
+     * 出生医学证明识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function birthCertificate($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->birthCertificateUrl, $data);
+    }
+
+    /**
+     * 机动车销售发票识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function vehicleInvoice($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->vehicleInvoiceUrl, $data);
+    }
+
+    /**
+     * 车辆合格证识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function vehicleCertificate($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->vehicleCertificateUrl, $data);
+    }
+
+    /**
+     * 税务局通用机打发票识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        location 是否输出位置信息,true:输出位置信息,false:不输出位置信息,默认false
+     * @return array
+     */
+    public function invoice($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->invoiceUrl, $data);
+    }
+
+    /**
+     * 行程单识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        location 是否输出位置信息,true:输出位置信息,false:不输出位置信息,默认false
+     * @return array
+     */
+    public function airTicket($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->airTicketUrl, $data);
+    }
+
+    /**
+     * 保单识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        rkv_business 是否进行商业逻辑处理,rue:进行商业逻辑处理,false:不进行商业逻辑处理,默认true
+     * @return array
+     */
+    public function insuranceDocuments($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->insuranceDocumentsUrl, $data);
+    }
+
+    /**
+     * 增值税发票识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function vatInvoice($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->vatInvoiceUrl, $data);
+    }
+
+    /**
+     * 二维码识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function qrcode($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->qrcodeUrl, $data);
+    }
+
+    /**
+     * 数字识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        recognize_granularity 是否定位单字符位置,big:不定位单字符位置,默认值;small:定位单字符位置
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     * @return array
+     */
+    public function numbers($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->numbersUrl, $data);
+    }
+
+    /**
+     * 彩票识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        recognize_granularity 是否定位单字符位置,big:不定位单字符位置,默认值;small:定位单字符位置
+     * @return array
+     */
+    public function lottery($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->lotteryUrl, $data);
+    }
+
+    /**
+     * 护照识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function passport($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->passportUrl, $data);
+    }
+
+    /**
+     * 名片识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function businessCard($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->businessCardUrl, $data);
+    }
+
+    /**
+     * 手写文字识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        recognize_granularity 是否定位单字符位置,big:不定位单字符位置,默认值;small:定位单字符位置
+     * @return array
+     */
+    public function handwriting($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->handwritingUrl, $data);
+    }
+
+    /**
+     * 自定义模板文字识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        templateSign 您在自定义文字识别平台制作的模板的ID
+     *                        classifierId 分类器Id。这个参数和templateSign至少存在一个,优先使用templateSign。存在templateSign时,表示使用指定模板;如果没有templateSign而有classifierId,表示使用分类器去判断使用哪个模板
+     * @return array
+     */
+    public function custom($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->customUrl, $data);
+    }
+
+    /**
+     * 同步请求
+     * @param string $image 图像读取
+     * @param options 接口可选参数
+     * @return array
+     */
+    public function tableRecognition($image, $options = array(), $timeout = 10000)
+    {
+        $result = $this->tableRecognitionAsync($image);
+        if (isset($result['error_code'])) {
+            return $result;
+        }
+        $requestId = $result['result'][0]['request_id'];
+        $count = ceil($timeout / 1000);
+        for ($i = 0; $i < $count; $i++) {
+            $result = $this->getTableRecognitionResult($requestId, $options);
+            // 完成
+            if ($result['result']['ret_code'] == 3) {
+                break;
+            }
+            sleep(1);
+        }
+        return $result;
+    }
+
+}
+

+ 408 - 0
application/common/library/aip/lib/AipBase.php

@@ -0,0 +1,408 @@
+<?php
+
+namespace app\common\library\aip\lib;
+
+/*
+* Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
+*
+* Licensed under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License. You may obtain a copy of
+* the License at
+*
+* Http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+* License for the specific language governing permissions and limitations under
+* the License.
+*/
+
+use Exception;
+
+/**
+ * Aip Base 基类
+ */
+class AipBase
+{
+
+    /**
+     * 获取access token url
+     * @var string
+     */
+    protected $accessTokenUrl = 'https://aip.baidubce.com/oauth/2.0/token';
+
+    /**
+     * 反馈接口
+     * @var string
+     */
+    protected $reportUrl = 'https://aip.baidubce.com/rpc/2.0/feedback/v1/report';
+
+    /**
+     * appId
+     * @var string
+     */
+    protected $appId = '';
+
+    /**
+     * apiKey
+     * @var string
+     */
+    protected $apiKey = '';
+
+    /**
+     * secretKey
+     * @var string
+     */
+    protected $secretKey = '';
+
+    /**
+     * 权限
+     * @var array
+     */
+    protected $scope = 'brain_all_scope';
+
+    /**
+     * @param string $appId
+     * @param string $apiKey
+     * @param string $secretKey
+     */
+    public function __construct($appId, $apiKey, $secretKey)
+    {
+        $this->appId = trim($appId);
+        $this->apiKey = trim($apiKey);
+        $this->secretKey = trim($secretKey);
+        $this->isCloudUser = null;
+        $this->client = new AipHttpClient();
+        $this->version = '2_2_2';
+        $this->proxies = array();
+    }
+
+    /**
+     * 查看版本
+     * @return string
+     *
+     */
+    public function getVersion()
+    {
+        return $this->version;
+    }
+
+    /**
+     * 连接超时
+     * @param int $ms 毫秒
+     */
+    public function setConnectionTimeoutInMillis($ms)
+    {
+        $this->client->setConnectionTimeoutInMillis($ms);
+    }
+
+    /**
+     * 响应超时
+     * @param int $ms 毫秒
+     */
+    public function setSocketTimeoutInMillis($ms)
+    {
+        $this->client->setSocketTimeoutInMillis($ms);
+    }
+
+    /**
+     * 代理
+     * @param array $proxy
+     * @return string
+     *
+     */
+    public function setProxies($proxies)
+    {
+        $this->client->setConf($proxies);
+    }
+
+    /**
+     * 处理请求参数
+     * @param string $url
+     * @param array  $params
+     * @param array  $data
+     * @param array  $headers
+     */
+    protected function proccessRequest($url, &$params, &$data, $headers)
+    {
+        $params['aipSdk'] = 'php';
+        $params['aipSdkVersion'] = $this->version;
+    }
+
+    /**
+     * Api 请求
+     * @param string $url
+     * @param mixed  $data
+     * @return mixed
+     */
+    protected function request($url, $data, $headers = array())
+    {
+        try {
+            $result = $this->validate($url, $data);
+            if ($result !== true) {
+                return $result;
+            }
+
+            $params = array();
+            $authObj = $this->auth();
+
+            if ($this->isCloudUser === false) {
+                $params['access_token'] = $authObj['access_token'];
+            }
+
+            // 特殊处理
+            $this->proccessRequest($url, $params, $data, $headers);
+
+            $headers = $this->getAuthHeaders('POST', $url, $params, $headers);
+            $response = $this->client->post($url, $data, $params, $headers);
+
+            $obj = $this->proccessResult($response['content']);
+
+            if (!$this->isCloudUser && isset($obj['error_code']) && $obj['error_code'] == 110) {
+                $authObj = $this->auth(true);
+                $params['access_token'] = $authObj['access_token'];
+                $response = $this->client->post($url, $data, $params, $headers);
+                $obj = $this->proccessResult($response['content']);
+            }
+
+            if (empty($obj) || !isset($obj['error_code'])) {
+                $this->writeAuthObj($authObj);
+            }
+        } catch (Exception $e) {
+            return array(
+                'error_code' => 'SDK108',
+                'error_msg'  => 'connection or read data timeout',
+            );
+        }
+
+        return $obj;
+    }
+
+    /**
+     * Api 多个并发请求
+     * @param string $url
+     * @param mixed  $data
+     * @return mixed
+     */
+    protected function multi_request($url, $data)
+    {
+        try {
+            $params = array();
+            $authObj = $this->auth();
+            $headers = $this->getAuthHeaders('POST', $url);
+
+            if ($this->isCloudUser === false) {
+                $params['access_token'] = $authObj['access_token'];
+            }
+
+            $responses = $this->client->multi_post($url, $data, $params, $headers);
+
+            $is_success = false;
+            foreach ($responses as $response) {
+                $obj = $this->proccessResult($response['content']);
+
+                if (empty($obj) || !isset($obj['error_code'])) {
+                    $is_success = true;
+                }
+
+                if (!$this->isCloudUser && isset($obj['error_code']) && $obj['error_code'] == 110) {
+                    $authObj = $this->auth(true);
+                    $params['access_token'] = $authObj['access_token'];
+                    $responses = $this->client->post($url, $data, $params, $headers);
+                    break;
+                }
+            }
+
+            if ($is_success) {
+                $this->writeAuthObj($authObj);
+            }
+
+            $objs = array();
+            foreach ($responses as $response) {
+                $objs[] = $this->proccessResult($response['content']);
+            }
+
+        } catch (Exception $e) {
+            return array(
+                'error_code' => 'SDK108',
+                'error_msg'  => 'connection or read data timeout',
+            );
+        }
+
+        return $objs;
+    }
+
+    /**
+     * 格式检查
+     * @param string $url
+     * @param array  $data
+     * @return mix
+     */
+    protected function validate($url, &$data)
+    {
+        return true;
+    }
+
+    /**
+     * 格式化结果
+     * @param $content string
+     * @return mixed
+     */
+    protected function proccessResult($content)
+    {
+        return json_decode($content, true);
+    }
+
+    /**
+     * 返回 access token 路径
+     * @return string
+     */
+    private function getAuthFilePath()
+    {
+        $aipTempPath = RUNTIME_PATH . 'aip' . DIRECTORY_SEPARATOR;
+
+        if (!is_dir($aipTempPath)) {
+            mkdir($aipTempPath, 0777, true);
+        }
+        return $aipTempPath . md5($this->apiKey);
+        //return dirname(__FILE__) . DIRECTORY_SEPARATOR . md5($this->apiKey);
+    }
+
+    /**
+     * 写入本地文件
+     * @param array $obj
+     * @return void
+     */
+    private function writeAuthObj($obj)
+    {
+        if ($obj === null || (isset($obj['is_read']) && $obj['is_read'] === true)) {
+            return;
+        }
+
+        $obj['time'] = time();
+        $obj['is_cloud_user'] = $this->isCloudUser;
+        @file_put_contents($this->getAuthFilePath(), json_encode($obj));
+    }
+
+    /**
+     * 读取本地缓存
+     * @return array
+     */
+    private function readAuthObj()
+    {
+        $authFile = $this->getAuthFilePath();
+        $content = is_file($authFile) ? @file_get_contents($authFile) : false;
+        if ($content !== false) {
+            $obj = json_decode($content, true);
+            $this->isCloudUser = $obj['is_cloud_user'];
+            $obj['is_read'] = true;
+            if ($this->isCloudUser || $obj['time'] + $obj['expires_in'] - 30 > time()) {
+                return $obj;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * 认证
+     * @param bool $refresh 是否刷新
+     * @return array
+     */
+    private function auth($refresh = false)
+    {
+
+        //非过期刷新
+        if (!$refresh) {
+            $obj = $this->readAuthObj();
+            if (!empty($obj)) {
+                return $obj;
+            }
+        }
+
+        $response = $this->client->get($this->accessTokenUrl, array(
+            'grant_type'    => 'client_credentials',
+            'client_id'     => $this->apiKey,
+            'client_secret' => $this->secretKey,
+        ));
+
+        $obj = json_decode($response['content'], true);
+
+        $this->isCloudUser = !$this->isPermission($obj);
+        return $obj;
+    }
+
+    /**
+     * 判断认证是否有权限
+     * @param array $authObj
+     * @return boolean
+     */
+    protected function isPermission($authObj)
+    {
+        if (empty($authObj) || !isset($authObj['scope'])) {
+            return false;
+        }
+
+        $scopes = explode(' ', $authObj['scope']);
+
+        return in_array($this->scope, $scopes);
+    }
+
+    /**
+     * @param string $method HTTP method
+     * @param string $url
+     * @param array  $param  参数
+     * @return array
+     */
+    private function getAuthHeaders($method, $url, $params = array(), $headers = array())
+    {
+
+        //不是云的老用户则不用在header中签名 认证
+        if ($this->isCloudUser === false) {
+            return $headers;
+        }
+
+        $obj = parse_url($url);
+        if (!empty($obj['query'])) {
+            foreach (explode('&', $obj['query']) as $kv) {
+                if (!empty($kv)) {
+                    list($k, $v) = explode('=', $kv, 2);
+                    $params[$k] = $v;
+                }
+            }
+        }
+
+        //UTC 时间戳
+        $timestamp = gmdate('Y-m-d\TH:i:s\Z');
+        $headers['Host'] = isset($obj['port']) ? sprintf('%s:%s', $obj['host'], $obj['port']) : $obj['host'];
+        $headers['x-bce-date'] = $timestamp;
+
+        //签名
+        $headers['authorization'] = AipSampleSigner::sign(array(
+            'ak' => $this->apiKey,
+            'sk' => $this->secretKey,
+        ), $method, $obj['path'], $headers, $params, array(
+            'timestamp'     => $timestamp,
+            'headersToSign' => array_keys($headers),
+        ));
+
+        return $headers;
+    }
+
+    /**
+     * 反馈
+     *
+     * @param array $feedbacks
+     * @return array
+     */
+    public function report($feedback)
+    {
+
+        $data = array();
+
+        $data['feedback'] = $feedback;
+
+        return $this->request($this->reportUrl, $data);
+    }
+}

+ 227 - 0
application/common/library/aip/lib/AipHttpClient.php

@@ -0,0 +1,227 @@
+<?php
+
+namespace app\common\library\aip\lib;
+/*
+* Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
+*
+* Licensed under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License. You may obtain a copy of
+* the License at
+*
+* Http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+* License for the specific language governing permissions and limitations under
+* the License.
+*/
+
+/**
+ * Http Client
+ */
+class AipHttpClient
+{
+
+    /**
+     * HttpClient
+     * @param array $headers HTTP header
+     */
+    public function __construct($headers = array())
+    {
+        $this->headers = $this->buildHeaders($headers);
+        $this->connectTimeout = 60000;
+        $this->socketTimeout = 60000;
+        $this->conf = array();
+    }
+
+    /**
+     * 连接超时
+     * @param int $ms 毫秒
+     */
+    public function setConnectionTimeoutInMillis($ms)
+    {
+        $this->connectTimeout = $ms;
+    }
+
+    /**
+     * 响应超时
+     * @param int $ms 毫秒
+     */
+    public function setSocketTimeoutInMillis($ms)
+    {
+        $this->socketTimeout = $ms;
+    }
+
+    /**
+     * 配置
+     * @param array $conf
+     */
+    public function setConf($conf)
+    {
+        $this->conf = $conf;
+    }
+
+    /**
+     * 请求预处理
+     * @param resource $ch
+     */
+    public function prepare($ch)
+    {
+        foreach ($this->conf as $key => $value) {
+            curl_setopt($ch, $key, $value);
+        }
+    }
+
+    /**
+     * @param  string $url
+     * @param  array  $data    HTTP POST BODY
+     * @param  array  $param   HTTP URL
+     * @param  array  $headers HTTP header
+     * @return array
+     */
+    public function post($url, $data = array(), $params = array(), $headers = array())
+    {
+        $url = $this->buildUrl($url, $params);
+        $headers = array_merge($this->headers, $this->buildHeaders($headers));
+
+        $ch = curl_init();
+        $this->prepare($ch);
+        curl_setopt($ch, CURLOPT_URL, $url);
+        curl_setopt($ch, CURLOPT_POST, 1);
+        curl_setopt($ch, CURLOPT_HEADER, false);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+        curl_setopt($ch, CURLOPT_POSTFIELDS, is_array($data) ? http_build_query($data) : $data);
+        curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->socketTimeout);
+        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $this->connectTimeout);
+        $content = curl_exec($ch);
+        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+
+        if ($code === 0) {
+            throw new Exception(curl_error($ch));
+        }
+
+        curl_close($ch);
+        return array(
+            'code'    => $code,
+            'content' => $content,
+        );
+    }
+
+    /**
+     * @param  string $url
+     * @param  array  $datas   HTTP POST BODY
+     * @param  array  $param   HTTP URL
+     * @param  array  $headers HTTP header
+     * @return array
+     */
+    public function multi_post($url, $datas = array(), $params = array(), $headers = array())
+    {
+        $url = $this->buildUrl($url, $params);
+        $headers = array_merge($this->headers, $this->buildHeaders($headers));
+
+        $chs = array();
+        $result = array();
+        $mh = curl_multi_init();
+        foreach ($datas as $data) {
+            $ch = curl_init();
+            $chs[] = $ch;
+            $this->prepare($ch);
+            curl_setopt($ch, CURLOPT_URL, $url);
+            curl_setopt($ch, CURLOPT_POST, 1);
+            curl_setopt($ch, CURLOPT_HEADER, false);
+            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+            curl_setopt($ch, CURLOPT_POSTFIELDS, is_array($data) ? http_build_query($data) : $data);
+            curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->socketTimeout);
+            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $this->connectTimeout);
+            curl_multi_add_handle($mh, $ch);
+        }
+
+        $running = null;
+        do {
+            curl_multi_exec($mh, $running);
+            usleep(100);
+        } while ($running);
+
+        foreach ($chs as $ch) {
+            $content = curl_multi_getcontent($ch);
+            $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+            $result[] = array(
+                'code'    => $code,
+                'content' => $content,
+            );
+            curl_multi_remove_handle($mh, $ch);
+        }
+        curl_multi_close($mh);
+
+        return $result;
+    }
+
+    /**
+     * @param  string $url
+     * @param  array  $param   HTTP URL
+     * @param  array  $headers HTTP header
+     * @return array
+     */
+    public function get($url, $params = array(), $headers = array())
+    {
+        $url = $this->buildUrl($url, $params);
+        $headers = array_merge($this->headers, $this->buildHeaders($headers));
+
+        $ch = curl_init();
+        $this->prepare($ch);
+        curl_setopt($ch, CURLOPT_URL, $url);
+        curl_setopt($ch, CURLOPT_HEADER, false);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+        curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->socketTimeout);
+        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $this->connectTimeout);
+        $content = curl_exec($ch);
+        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+
+        if ($code === 0) {
+            throw new Exception(curl_error($ch));
+        }
+
+        curl_close($ch);
+        return array(
+            'code'    => $code,
+            'content' => $content,
+        );
+    }
+
+    /**
+     * 构造 header
+     * @param  array $headers
+     * @return array
+     */
+    private function buildHeaders($headers)
+    {
+        $result = array();
+        foreach ($headers as $k => $v) {
+            $result[] = sprintf('%s:%s', $k, $v);
+        }
+        return $result;
+    }
+
+    /**
+     *
+     * @param  string $url
+     * @param  array  $params 参数
+     * @return string
+     */
+    private function buildUrl($url, $params)
+    {
+        if (!empty($params)) {
+            $str = http_build_query($params);
+            return $url . (strpos($url, '?') === false ? '?' : '&') . $str;
+        } else {
+            return $url;
+        }
+    }
+}

+ 181 - 0
application/common/library/aip/lib/AipHttpUtil.php

@@ -0,0 +1,181 @@
+<?php
+
+namespace app\common\library\aip\lib;
+/*
+* Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
+*
+* Licensed under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License. You may obtain a copy of
+* the License at
+*
+* Http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+* License for the specific language governing permissions and limitations under
+* the License.
+*/
+
+/**
+ * BCE Util
+ */
+class AipHttpUtil
+{
+    // 根据RFC 3986,除了:
+    //   1.大小写英文字符
+    //   2.阿拉伯数字
+    //   3.点'.'、波浪线'~'、减号'-'以及下划线'_'
+    // 以外都要编码
+    public static $PERCENT_ENCODED_STRINGS;
+
+    //填充编码数组
+    public static function __init()
+    {
+        AipHttpUtil::$PERCENT_ENCODED_STRINGS = array();
+        for ($i = 0; $i < 256; ++$i) {
+            AipHttpUtil::$PERCENT_ENCODED_STRINGS[$i] = sprintf("%%%02X", $i);
+        }
+
+        //a-z不编码
+        foreach (range('a', 'z') as $ch) {
+            AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord($ch)] = $ch;
+        }
+
+        //A-Z不编码
+        foreach (range('A', 'Z') as $ch) {
+            AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord($ch)] = $ch;
+        }
+
+        //0-9不编码
+        foreach (range('0', '9') as $ch) {
+            AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord($ch)] = $ch;
+        }
+
+        //以下4个字符不编码
+        AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord('-')] = '-';
+        AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord('.')] = '.';
+        AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord('_')] = '_';
+        AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord('~')] = '~';
+    }
+
+    /**
+     * 在uri编码中不能对'/'编码
+     * @param  string $path
+     * @return string
+     */
+    public static function urlEncodeExceptSlash($path)
+    {
+        return str_replace("%2F", "/", AipHttpUtil::urlEncode($path));
+    }
+
+    /**
+     * 使用编码数组编码
+     * @param  string $path
+     * @return string
+     */
+    public static function urlEncode($value)
+    {
+        $result = '';
+        for ($i = 0; $i < strlen($value); ++$i) {
+            $result .= AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord($value[$i])];
+        }
+        return $result;
+    }
+
+    /**
+     * 生成标准化QueryString
+     * @param  array $parameters
+     * @return array
+     */
+    public static function getCanonicalQueryString(array $parameters)
+    {
+        //没有参数,直接返回空串
+        if (count($parameters) == 0) {
+            return '';
+        }
+
+        $parameterStrings = array();
+        foreach ($parameters as $k => $v) {
+            //跳过Authorization字段
+            if (strcasecmp('Authorization', $k) == 0) {
+                continue;
+            }
+            if (!isset($k)) {
+                throw new \InvalidArgumentException(
+                    "parameter key should not be null"
+                );
+            }
+            if (isset($v)) {
+                //对于有值的,编码后放在=号两边
+                $parameterStrings[] = AipHttpUtil::urlEncode($k)
+                    . '=' . AipHttpUtil::urlEncode((string)$v);
+            } else {
+                //对于没有值的,只将key编码后放在=号的左边,右边留空
+                $parameterStrings[] = AipHttpUtil::urlEncode($k) . '=';
+            }
+        }
+        //按照字典序排序
+        sort($parameterStrings);
+
+        //使用'&'符号连接它们
+        return implode('&', $parameterStrings);
+    }
+
+    /**
+     * 生成标准化uri
+     * @param  string $path
+     * @return string
+     */
+    public static function getCanonicalURIPath($path)
+    {
+        //空路径设置为'/'
+        if (empty($path)) {
+            return '/';
+        } else {
+            //所有的uri必须以'/'开头
+            if ($path[0] == '/') {
+                return AipHttpUtil::urlEncodeExceptSlash($path);
+            } else {
+                return '/' . AipHttpUtil::urlEncodeExceptSlash($path);
+            }
+        }
+    }
+
+    /**
+     * 生成标准化http请求头串
+     * @param  array $headers
+     * @return array
+     */
+    public static function getCanonicalHeaders($headers)
+    {
+        //如果没有headers,则返回空串
+        if (count($headers) == 0) {
+            return '';
+        }
+
+        $headerStrings = array();
+        foreach ($headers as $k => $v) {
+            //跳过key为null的
+            if ($k === null) {
+                continue;
+            }
+            //如果value为null,则赋值为空串
+            if ($v === null) {
+                $v = '';
+            }
+            //trim后再encode,之后使用':'号连接起来
+            $headerStrings[] = AipHttpUtil::urlEncode(strtolower(trim($k))) . ':' . AipHttpUtil::urlEncode(trim($v));
+        }
+        //字典序排序
+        sort($headerStrings);
+
+        //用'\n'把它们连接起来
+        return implode("\n", $headerStrings);
+    }
+}
+
+AipHttpUtil::__init();
+
+
+

+ 181 - 0
application/common/library/aip/lib/AipSampleSigner.php

@@ -0,0 +1,181 @@
+<?php
+
+namespace app\common\library\aip\lib;
+
+
+class AipSampleSigner
+{
+
+    const BCE_AUTH_VERSION = "bce-auth-v1";
+    const BCE_PREFIX = 'x-bce-';
+
+    //不指定headersToSign情况下,默认签名http头,包括:
+    //    1.host
+    //    2.content-length
+    //    3.content-type
+    //    4.content-md5
+    public static $defaultHeadersToSign;
+
+    public static function __init()
+    {
+        AipSampleSigner::$defaultHeadersToSign = array(
+            "host",
+            "content-length",
+            "content-type",
+            "content-md5",
+        );
+    }
+
+    /**
+     * 签名
+     * @param  array  $credentials
+     * @param  string $httpMethod
+     * @param  string $path
+     * @param  array  $headers
+     * @param  string $params
+     * @param  array  $options
+     * @return string
+     */
+    public static function sign(
+        array $credentials,
+        $httpMethod,
+        $path,
+        $headers,
+        $params,
+        $options = array()
+    )
+    {
+        //设定签名有效时间
+        if (!isset($options[AipSignOption::EXPIRATION_IN_SECONDS])) {
+            //默认值1800秒
+            $expirationInSeconds = AipSignOption::DEFAULT_EXPIRATION_IN_SECONDS;
+        } else {
+            $expirationInSeconds = $options[AipSignOption::EXPIRATION_IN_SECONDS];
+        }
+
+        //解析ak sk
+        $accessKeyId = $credentials['ak'];
+        $secretAccessKey = $credentials['sk'];
+
+        //设定时间戳,注意:如果自行指定时间戳需要为UTC时间
+        if (!isset($options[AipSignOption::TIMESTAMP])) {
+            //默认值当前时间
+            $timestamp = gmdate('Y-m-d\TH:i:s\Z');
+        } else {
+            $timestamp = $options[AipSignOption::TIMESTAMP];
+        }
+
+        //生成authString
+        $authString = AipSampleSigner::BCE_AUTH_VERSION . '/' . $accessKeyId . '/'
+            . $timestamp . '/' . $expirationInSeconds;
+
+        //使用sk和authString生成signKey
+        $signingKey = hash_hmac('sha256', $authString, $secretAccessKey);
+
+        //生成标准化URI
+        $canonicalURI = AipHttpUtil::getCanonicalURIPath($path);
+
+        //生成标准化QueryString
+        $canonicalQueryString = AipHttpUtil::getCanonicalQueryString($params);
+
+        //填充headersToSign,也就是指明哪些header参与签名
+        $headersToSign = null;
+        if (isset($options[AipSignOption::HEADERS_TO_SIGN])) {
+            $headersToSign = $options[AipSignOption::HEADERS_TO_SIGN];
+        }
+
+        //生成标准化header
+        $canonicalHeader = AipHttpUtil::getCanonicalHeaders(
+            AipSampleSigner::getHeadersToSign($headers, $headersToSign)
+        );
+
+        //整理headersToSign,以';'号连接
+        $signedHeaders = '';
+        if ($headersToSign !== null) {
+            $signedHeaders = strtolower(
+                trim(implode(";", $headersToSign))
+            );
+        }
+
+        //组成标准请求串
+        $canonicalRequest = "$httpMethod\n$canonicalURI\n"
+            . "$canonicalQueryString\n$canonicalHeader";
+
+        //使用signKey和标准请求串完成签名
+        $signature = hash_hmac('sha256', $canonicalRequest, $signingKey);
+
+        //组成最终签名串
+        $authorizationHeader = "$authString/$signedHeaders/$signature";
+
+        return $authorizationHeader;
+    }
+
+    /**
+     * 根据headsToSign过滤应该参与签名的header
+     * @param  array $headers
+     * @param  array $headersToSign
+     * @return array
+     */
+    public static function getHeadersToSign($headers, $headersToSign)
+    {
+
+        //value被trim后为空串的header不参与签名
+        $filter_empty = function ($v) {
+            return trim((string)$v) !== '';
+        };
+        $headers = array_filter($headers, $filter_empty);
+
+        //处理headers的key:去掉前后的空白并转化成小写
+        $trim_and_lower = function ($str) {
+            return strtolower(trim($str));
+        };
+        $temp = array();
+        $process_keys = function ($k, $v) use (&$temp, $trim_and_lower) {
+            $temp[$trim_and_lower($k)] = $v;
+        };
+        array_map($process_keys, array_keys($headers), $headers);
+        $headers = $temp;
+
+        //取出headers的key以备用
+        $header_keys = array_keys($headers);
+
+        $filtered_keys = null;
+        if ($headersToSign !== null) {
+            //如果有headersToSign,则根据headersToSign过滤
+
+            //预处理headersToSign:去掉前后的空白并转化成小写
+            $headersToSign = array_map($trim_and_lower, $headersToSign);
+
+            //只选取在headersToSign里面的header
+            $filtered_keys = array_intersect_key($header_keys, $headersToSign);
+
+        } else {
+            //如果没有headersToSign,则根据默认规则来选取headers
+            $filter_by_default = function ($k) {
+                return AipSampleSigner::isDefaultHeaderToSign($k);
+            };
+            $filtered_keys = array_filter($header_keys, $filter_by_default);
+        }
+
+        //返回需要参与签名的header
+        return array_intersect_key($headers, array_flip($filtered_keys));
+    }
+
+    /**
+     * 检查header是不是默认参加签名的:
+     * 1.是host、content-type、content-md5、content-length之一
+     * 2.以x-bce开头
+     * @param  array $header
+     * @return boolean
+     */
+    public static function isDefaultHeaderToSign($header)
+    {
+        $header = strtolower(trim($header));
+        if (in_array($header, AipSampleSigner::$defaultHeadersToSign)) {
+            return true;
+        }
+        return substr_compare($header, AipSampleSigner::BCE_PREFIX, 0, strlen(AipSampleSigner::BCE_PREFIX)) == 0;
+    }
+}
+
+AipSampleSigner::__init();

+ 19 - 0
application/common/library/aip/lib/AipSignOption.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace app\common\library\aip\lib;
+
+
+class AipSignOption
+{
+    const EXPIRATION_IN_SECONDS = 'expirationInSeconds';
+
+    const HEADERS_TO_SIGN = 'headersToSign';
+
+    const TIMESTAMP = 'timestamp';
+
+    const DEFAULT_EXPIRATION_IN_SECONDS = 1800;
+
+    const MIN_EXPIRATION_IN_SECONDS = 300;
+
+    const MAX_EXPIRATION_IN_SECONDS = 129600;
+}

+ 18 - 0
application/common/library/coupon/Discount.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace app\common\library\coupon;
+
+//订单满xx打xx折
+class Discount implements calculateInterface
+{
+    public function calculate($result_data, $money)
+    {
+        $arr = is_array($result_data) ? $result_data : (array)json_decode($result_data, true);
+        if ($arr['money'] > 0 && $arr['money'] > $money) {
+            throw new \Exception('订单金额未满足优惠条件');
+        }
+        $new_money = bcmul(bcdiv($arr['number'], 10, 2), $money, 2);
+        $coupon_money = bcsub($money, $new_money, 2);
+        return [$new_money, $coupon_money];
+    }
+}

+ 19 - 0
application/common/library/coupon/FullReduction.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace app\common\library\coupon;
+
+//订单满xx减xx
+class FullReduction implements calculateInterface
+{
+
+    public function calculate($result_data, $money)
+    {
+        $arr = is_array($result_data) ? $result_data : (array)json_decode($result_data, true);
+        if ($arr['money'] > 0 && $arr['money'] > $money) {
+            throw new \Exception('订单金额未满足优惠条件');
+        }
+        $coupon_money = $arr['number'];
+        $new_money = bcsub($money, $coupon_money, 2);
+        return [$new_money, $coupon_money];
+    }
+}

+ 18 - 0
application/common/library/coupon/ToCalculate.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace app\common\library\coupon;
+
+class ToCalculate
+{
+    public $obj;
+
+    public function __construct(calculateInterface $obj)
+    {
+        $this->obj = $obj;
+    }
+
+    public function doing($result_data,$money)
+    {
+        return $this->obj->calculate($result_data,$money);
+    }
+}

+ 9 - 0
application/common/library/coupon/calculateInterface.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace app\common\library\coupon;
+
+
+interface calculateInterface
+{
+    public function calculate($result_data,$money);
+}

+ 420 - 0
application/common/library/hashids/Hashids.php

@@ -0,0 +1,420 @@
+<?php
+
+/*
+ * This file is part of Hashids.
+ *
+ * (c) Ivan Akimov <ivan@barreleye.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Hashids;
+
+use Hashids\Math\Bc;
+use Hashids\Math\Gmp;
+use RuntimeException;
+
+/**
+ * This is the hashids class.
+ *
+ * @author Ivan Akimov <ivan@barreleye.com>
+ * @author Vincent Klaiber <hello@vinkla.com>
+ * @author Johnson Page <jwpage@gmail.com>
+ */
+class Hashids implements HashidsInterface
+{
+    /**
+     * The seps divider.
+     *
+     * @var float
+     */
+    const SEP_DIV = 3.5;
+
+    /**
+     * The guard divider.
+     *
+     * @var float
+     */
+    const GUARD_DIV = 12;
+
+    /**
+     * The alphabet string.
+     *
+     * @var string
+     */
+    protected $alphabet;
+
+    /**
+     * Shuffled alphabets, referenced by alphabet and salt.
+     *
+     * @var array
+     */
+    protected $shuffledAlphabets;
+
+    /**
+     * The seps string.
+     *
+     * @var string
+     */
+    protected $seps = 'cfhistuCFHISTU';
+
+    /**
+     * The guards string.
+     *
+     * @var string
+     */
+    protected $guards;
+
+    /**
+     * The minimum hash length.
+     *
+     * @var int
+     */
+    protected $minHashLength;
+
+    /**
+     * The salt string.
+     *
+     * @var string
+     */
+    protected $salt;
+
+    /**
+     * The math class.
+     *
+     * @var \Hashids\Math\MathInterface
+     */
+    protected $math;
+
+    /**
+     * Create a new hashids instance.
+     *
+     * @param string $salt
+     * @param int $minHashLength
+     * @param string $alphabet
+     *
+     * @throws \Hashids\HashidsException
+     *
+     * @return void
+     */
+    public function __construct($salt = '', $minHashLength = 0, $alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890')
+    {
+        $this->salt = $salt;
+        $this->minHashLength = $minHashLength;
+        $this->alphabet = implode('', array_unique(str_split($alphabet)));
+        $this->math = $this->getMathExtension();
+
+        if (strlen($this->alphabet) < 16) {
+            throw new HashidsException('Alphabet must contain at least 16 unique characters.');
+        }
+
+        if (strpos($this->alphabet, ' ') !== false) {
+            throw new HashidsException('Alphabet can\'t contain spaces.');
+        }
+
+        $alphabetArray = str_split($this->alphabet);
+        $sepsArray = str_split($this->seps);
+
+        $this->seps = implode('', array_intersect($sepsArray, $alphabetArray));
+        $this->alphabet = implode('', array_diff($alphabetArray, $sepsArray));
+        $this->seps = $this->shuffle($this->seps, $this->salt);
+
+        if (!$this->seps || (strlen($this->alphabet) / strlen($this->seps)) > self::SEP_DIV) {
+            $sepsLength = (int) ceil(strlen($this->alphabet) / self::SEP_DIV);
+
+            if ($sepsLength > strlen($this->seps)) {
+                $diff = $sepsLength - strlen($this->seps);
+                $this->seps .= substr($this->alphabet, 0, $diff);
+                $this->alphabet = substr($this->alphabet, $diff);
+            }
+        }
+
+        $this->alphabet = $this->shuffle($this->alphabet, $this->salt);
+        $guardCount = (int) ceil(strlen($this->alphabet) / self::GUARD_DIV);
+
+        if (strlen($this->alphabet) < 3) {
+            $this->guards = substr($this->seps, 0, $guardCount);
+            $this->seps = substr($this->seps, $guardCount);
+        } else {
+            $this->guards = substr($this->alphabet, 0, $guardCount);
+            $this->alphabet = substr($this->alphabet, $guardCount);
+        }
+    }
+
+    /**
+     * Encode parameters to generate a hash.
+     *
+     * @param mixed $numbers
+     *
+     * @return string
+     */
+    public function encode(...$numbers)
+    {
+        $ret = '';
+
+        if (1 === count($numbers) && is_array($numbers[0])) {
+            $numbers = $numbers[0];
+        }
+
+        if (!$numbers) {
+            return $ret;
+        }
+
+        foreach ($numbers as $number) {
+            $isNumber = ctype_digit((string) $number);
+
+            if (!$isNumber) {
+                return $ret;
+            }
+        }
+
+        $alphabet = $this->alphabet;
+        $numbersSize = count($numbers);
+        $numbersHashInt = 0;
+
+        foreach ($numbers as $i => $number) {
+            $numbersHashInt += $this->math->intval($this->math->mod($number, ($i + 100)));
+        }
+
+        $lottery = $ret = $alphabet[$numbersHashInt % strlen($alphabet)];
+        foreach ($numbers as $i => $number) {
+            $alphabet = $this->shuffle($alphabet, substr($lottery.$this->salt.$alphabet, 0, strlen($alphabet)));
+            $ret .= $last = $this->hash($number, $alphabet);
+
+            if ($i + 1 < $numbersSize) {
+                $number %= (ord($last) + $i);
+                $sepsIndex = $this->math->intval($this->math->mod($number, strlen($this->seps)));
+                $ret .= $this->seps[$sepsIndex];
+            }
+        }
+
+        if (strlen($ret) < $this->minHashLength) {
+            $guardIndex = ($numbersHashInt + ord($ret[0])) % strlen($this->guards);
+
+            $guard = $this->guards[$guardIndex];
+            $ret = $guard.$ret;
+
+            if (strlen($ret) < $this->minHashLength) {
+                $guardIndex = ($numbersHashInt + ord($ret[2])) % strlen($this->guards);
+                $guard = $this->guards[$guardIndex];
+
+                $ret .= $guard;
+            }
+        }
+
+        $halfLength = (int) (strlen($alphabet) / 2);
+        while (strlen($ret) < $this->minHashLength) {
+            $alphabet = $this->shuffle($alphabet, $alphabet);
+            $ret = substr($alphabet, $halfLength).$ret.substr($alphabet, 0, $halfLength);
+
+            $excess = strlen($ret) - $this->minHashLength;
+            if ($excess > 0) {
+                $ret = substr($ret, $excess / 2, $this->minHashLength);
+            }
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Decode a hash to the original parameter values.
+     *
+     * @param string $hash
+     *
+     * @return array
+     */
+    public function decode($hash)
+    {
+        $ret = [];
+
+        if (!is_string($hash) || !($hash = trim($hash))) {
+            return $ret;
+        }
+
+        $alphabet = $this->alphabet;
+
+        $ret = [];
+
+        $hashBreakdown = str_replace(str_split($this->guards), ' ', $hash);
+        $hashArray = explode(' ', $hashBreakdown);
+
+        $i = count($hashArray) == 3 || count($hashArray) == 2 ? 1 : 0;
+
+        $hashBreakdown = $hashArray[$i];
+
+        if (isset($hashBreakdown[0])) {
+            $lottery = $hashBreakdown[0];
+            $hashBreakdown = substr($hashBreakdown, 1);
+
+            $hashBreakdown = str_replace(str_split($this->seps), ' ', $hashBreakdown);
+            $hashArray = explode(' ', $hashBreakdown);
+
+            foreach ($hashArray as $subHash) {
+                $alphabet = $this->shuffle($alphabet, substr($lottery.$this->salt.$alphabet, 0, strlen($alphabet)));
+                $result = $this->unhash($subHash, $alphabet);
+                if ($this->math->greaterThan($result, PHP_INT_MAX)) {
+                    $ret[] = $this->math->strval($result);
+                } else {
+                    $ret[] = $this->math->intval($result);
+                }
+            }
+
+            if ($this->encode($ret) != $hash) {
+                $ret = [];
+            }
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Encode hexadecimal values and generate a hash string.
+     *
+     * @param string $str
+     *
+     * @return string
+     */
+    public function encodeHex($str)
+    {
+        if (!ctype_xdigit((string) $str)) {
+            return '';
+        }
+
+        $numbers = trim(chunk_split($str, 12, ' '));
+        $numbers = explode(' ', $numbers);
+
+        foreach ($numbers as $i => $number) {
+            $numbers[$i] = hexdec('1'.$number);
+        }
+
+        return call_user_func_array([$this, 'encode'], $numbers);
+    }
+
+    /**
+     * Decode a hexadecimal hash.
+     *
+     * @param string $hash
+     *
+     * @return string
+     */
+    public function decodeHex($hash)
+    {
+        $ret = '';
+        $numbers = $this->decode($hash);
+
+        foreach ($numbers as $i => $number) {
+            $ret .= substr(dechex($number), 1);
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Shuffle alphabet by given salt.
+     *
+     * @param string $alphabet
+     * @param string $salt
+     *
+     * @return string
+     */
+    protected function shuffle($alphabet, $salt)
+    {
+        $key = $alphabet.' '.$salt;
+
+        if (isset($this->shuffledAlphabets[$key])) {
+            return $this->shuffledAlphabets[$key];
+        }
+
+        $saltLength = strlen($salt);
+
+        if (!$saltLength) {
+            return $alphabet;
+        }
+
+        for ($i = strlen($alphabet) - 1, $v = 0, $p = 0; $i > 0; $i--, $v++) {
+            $v %= $saltLength;
+            $p += $int = ord($salt[$v]);
+            $j = ($int + $v + $p) % $i;
+
+            $temp = $alphabet[$j];
+            $alphabet[$j] = $alphabet[$i];
+            $alphabet[$i] = $temp;
+        }
+
+        $this->shuffledAlphabets[$key] = $alphabet;
+
+        return $alphabet;
+    }
+
+    /**
+     * Hash given input value.
+     *
+     * @param string $input
+     * @param string $alphabet
+     *
+     * @return string
+     */
+    protected function hash($input, $alphabet)
+    {
+        $hash = '';
+        $alphabetLength = strlen($alphabet);
+
+        do {
+            $hash = $alphabet[$this->math->intval($this->math->mod($input, $alphabetLength))].$hash;
+
+            $input = $this->math->divide($input, $alphabetLength);
+        } while ($this->math->greaterThan($input, 0));
+
+        return $hash;
+    }
+
+    /**
+     * Unhash given input value.
+     *
+     * @param string $input
+     * @param string $alphabet
+     *
+     * @return int
+     */
+    protected function unhash($input, $alphabet)
+    {
+        $number = 0;
+        $inputLength = strlen($input);
+
+        if ($inputLength && $alphabet) {
+            $alphabetLength = strlen($alphabet);
+            $inputChars = str_split($input);
+
+            foreach ($inputChars as $char) {
+                $position = strpos($alphabet, $char);
+                $number = $this->math->multiply($number, $alphabetLength);
+                $number = $this->math->add($number, $position);
+            }
+        }
+
+        return $number;
+    }
+
+    /**
+     * Get BC Math or GMP extension.
+     *
+     * @codeCoverageIgnore
+     *
+     * @throws \RuntimeException
+     *
+     * @return \Hashids\Math\Bc|\Hashids\Math\Gmp
+     */
+    protected function getMathExtension()
+    {
+        if (extension_loaded('gmp')) {
+            return new Gmp();
+        }
+
+        if (extension_loaded('bcmath')) {
+            return new Bc();
+        }
+
+        throw new RuntimeException('Missing BC Math or GMP extension.');
+    }
+}

+ 24 - 0
application/common/library/hashids/HashidsException.php

@@ -0,0 +1,24 @@
+<?php
+
+/*
+ * This file is part of Hashids.
+ *
+ * (c) Ivan Akimov <ivan@barreleye.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Hashids;
+
+use InvalidArgumentException;
+
+/**
+ * This is the hashids exception class.
+ *
+ * @author Vincent Klaiber <hello@vinkla.com>
+ */
+class HashidsException extends InvalidArgumentException
+{
+    //
+}

+ 57 - 0
application/common/library/hashids/HashidsInterface.php

@@ -0,0 +1,57 @@
+<?php
+
+/*
+ * This file is part of Hashids.
+ *
+ * (c) Ivan Akimov <ivan@barreleye.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Hashids;
+
+/**
+ * This is the hashids interface.
+ *
+ * @author Ivan Akimov <ivan@barreleye.com>
+ * @author Vincent Klaiber <hello@vinkla.com>
+ */
+interface HashidsInterface
+{
+    /**
+     * Encode parameters to generate a hash.
+     *
+     * @param mixed $numbers
+     *
+     * @return string
+     */
+    public function encode(...$numbers);
+
+    /**
+     * Decode a hash to the original parameter values.
+     *
+     * @param string $hash
+     *
+     * @return array
+     */
+    public function decode($hash);
+
+    /**
+     * Encode hexadecimal values and generate a hash string.
+     *
+     * @param string $str
+     *
+     * @return string
+     */
+    public function encodeHex($str);
+
+    /**
+     * Decode a hexadecimal hash.
+     *
+     * @param string $hash
+     *
+     * @return string
+     */
+    public function decodeHex($hash);
+}

+ 20 - 0
application/common/library/hashids/LICENSE

@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2012-2018 Ivan Akimov <ivan@barreleye.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 123 - 0
application/common/library/hashids/Math/Bc.php

@@ -0,0 +1,123 @@
+<?php
+
+/*
+ * This file is part of Hashids.
+ *
+ * (c) Ivan Akimov <ivan@barreleye.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Hashids\Math;
+
+/**
+ * This is the Bc math class.
+ *
+ * @author Vincent Klaiber <hello@vinkla.com>
+ * @author Jakub Kramarz <lenwe@lenwe.net>
+ * @author Johnson Page <jwpage@gmail.com>
+ */
+class Bc implements MathInterface
+{
+    /**
+     * Add two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function add($a, $b)
+    {
+        return bcadd($a, $b, 0);
+    }
+
+    /**
+     * Multiply two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function multiply($a, $b)
+    {
+        return bcmul($a, $b, 0);
+    }
+
+    /**
+     * Divide two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function divide($a, $b)
+    {
+        return bcdiv($a, $b, 0);
+    }
+
+    /**
+     * Compute arbitrary-length integer modulo.
+     *
+     * @param string $n
+     * @param string $d
+     *
+     * @return string
+     */
+    public function mod($n, $d)
+    {
+        return bcmod($n, $d);
+    }
+
+    /**
+     * Compares two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return bool
+     */
+    public function greaterThan($a, $b)
+    {
+        return bccomp($a, $b, 0) > 0;
+    }
+
+    /**
+     * Converts arbitrary-length integer to PHP integer.
+     *
+     * @param string $a
+     *
+     * @return int
+     */
+    public function intval($a)
+    {
+        return intval($a);
+    }
+
+    /**
+     * Converts arbitrary-length integer to PHP string.
+     *
+     * @param string $a
+     *
+     * @return string
+     */
+    public function strval($a)
+    {
+        return $a;
+    }
+
+    /**
+     * Converts PHP integer to arbitrary-length integer.
+     *
+     * @param int $a
+     *
+     * @return string
+     */
+    public function get($a)
+    {
+        return $a;
+    }
+}

+ 123 - 0
application/common/library/hashids/Math/Gmp.php

@@ -0,0 +1,123 @@
+<?php
+
+/*
+ * This file is part of Hashids.
+ *
+ * (c) Ivan Akimov <ivan@barreleye.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Hashids\Math;
+
+/**
+ * This is the Gmp math class.
+ *
+ * @author Vincent Klaiber <hello@vinkla.com>
+ * @author Jakub Kramarz <lenwe@lenwe.net>
+ * @author Johnson Page <jwpage@gmail.com>
+ */
+class Gmp implements MathInterface
+{
+    /**
+     * Add two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function add($a, $b)
+    {
+        return gmp_add($a, $b);
+    }
+
+    /**
+     * Multiply two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function multiply($a, $b)
+    {
+        return gmp_mul($a, $b);
+    }
+
+    /**
+     * Divide two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function divide($a, $b)
+    {
+        return gmp_div_q($a, $b);
+    }
+
+    /**
+     * Compute arbitrary-length integer modulo.
+     *
+     * @param string $n
+     * @param string $d
+     *
+     * @return string
+     */
+    public function mod($n, $d)
+    {
+        return gmp_mod($n, $d);
+    }
+
+    /**
+     * Compares two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return bool
+     */
+    public function greaterThan($a, $b)
+    {
+        return gmp_cmp($a, $b) > 0;
+    }
+
+    /**
+     * Converts arbitrary-length integer to PHP integer.
+     *
+     * @param string $a
+     *
+     * @return int
+     */
+    public function intval($a)
+    {
+        return gmp_intval($a);
+    }
+
+    /**
+     * Converts arbitrary-length integer to PHP string.
+     *
+     * @param string $a
+     *
+     * @return string
+     */
+    public function strval($a)
+    {
+        return gmp_strval($a);
+    }
+
+    /**
+     * Converts PHP integer to arbitrary-length integer.
+     *
+     * @param int $a
+     *
+     * @return string
+     */
+    public function get($a)
+    {
+        return gmp_init($a);
+    }
+}

+ 99 - 0
application/common/library/hashids/Math/MathInterface.php

@@ -0,0 +1,99 @@
+<?php
+
+/*
+ * This file is part of Hashids.
+ *
+ * (c) Ivan Akimov <ivan@barreleye.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Hashids\Math;
+
+/**
+ * Interface for different math extensions.
+ *
+ * @author Vincent Klaiber <hello@vinkla.com>
+ * @author Jakub Kramarz <lenwe@lenwe.net>
+ * @author Johnson Page <jwpage@gmail.com>
+ */
+interface MathInterface
+{
+    /**
+     * Add two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function add($a, $b);
+
+    /**
+     * Multiply two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function multiply($a, $b);
+
+    /**
+     * Divide two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function divide($a, $b);
+
+    /**
+     * Compute arbitrary-length integer modulo.
+     *
+     * @param string $n
+     * @param string $d
+     *
+     * @return string
+     */
+    public function mod($n, $d);
+
+    /**
+     * Compares two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return bool
+     */
+    public function greaterThan($a, $b);
+
+    /**
+     * Converts arbitrary-length integer to PHP integer.
+     *
+     * @param string $a
+     *
+     * @return int
+     */
+    public function intval($a);
+
+    /**
+     * Converts arbitrary-length integer to PHP string.
+     *
+     * @param string $a
+     *
+     * @return string
+     */
+    public function strval($a);
+
+    /**
+     * Converts PHP integer to arbitrary-length integer.
+     *
+     * @param int $a
+     *
+     * @return string
+     */
+    public function get($a);
+}

+ 49 - 0
application/common/library/hashids/composer.json

@@ -0,0 +1,49 @@
+{
+    "name": "hashids/hashids",
+    "description": "Generate short, unique, non-sequential ids (like YouTube and Bitly) from numbers",
+    "keywords": ["hashids", "hashid", "hash", "ids", "youtube", "bitly", "encode", "decode", "obfuscate"],
+    "homepage": "http://hashids.org/php",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Ivan Akimov",
+            "email": "ivan@barreleye.com",
+            "homepage": "https://twitter.com/IvanAkimov"
+        },
+        {
+            "name": "Vincent Klaiber",
+            "email": "hello@vinkla.com",
+            "homepage": "https://vinkla.com"
+        }
+    ],
+    "require": {
+        "php": "^7.1.3"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "^7.0"
+    },
+    "autoload": {
+        "psr-4": {
+            "Hashids\\": "src/"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "Hashids\\Tests\\": "tests/"
+        }
+    },
+    "config": {
+        "preferred-install": "dist"
+    },
+    "extra": {
+        "branch-alias": {
+            "dev-master": "3.0-dev"
+        }
+    },
+    "suggest": {
+       "ext-bcmath": "Required to use BC Math arbitrary precision mathematics (*).",
+       "ext-gmp": "Required to use GNU multiple precision mathematics (*)."
+     },
+    "minimum-stability": "dev",
+    "prefer-stable": true
+}

+ 19 - 0
application/common/library/message/Email.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace app\common\library\message;
+
+use app\common\library\Email as EmailBin;
+
+class Email
+{
+
+    //发送邮箱通知
+    public function send($data)
+    {
+        $email = new EmailBin();
+        $email->subject($data['title'])
+            ->to($data['email'])
+            ->message($data['message'])
+            ->send();       
+    }
+}

+ 102 - 0
application/common/library/message/Mini.php

@@ -0,0 +1,102 @@
+<?php
+
+namespace app\common\library\message;
+
+use fast\Http;
+use GuzzleHttp\Client;
+use GuzzleHttp\Psr7\Response;
+use think\Cache;
+use think\Log;
+use think\Config;
+
+class Mini
+{
+    private $appId = '';
+    private $appSecret = '';
+
+    public function __construct()
+    {
+        $config = get_addon_config('shop');
+        $this->appId = $config['wx_appid'];
+        $this->appSecret = $config['wx_app_secret'];
+    }
+
+    /**
+     * 批量并发发送
+     * @param array $pushList
+     * @return bool
+     */
+    public function subscribeMessageSendMultiple($pushList)
+    {
+        $url = 'https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=' . $this->getAccessToken();
+        $concurrentSize = 10;
+        $promises = [];
+        $client = new Client();
+        $promiseProcess = function ($promises) {
+            $results = \GuzzleHttp\Promise\Utils::unwrap($promises);
+            foreach ($results as $key => $response) {
+                $res = (array)json_decode($response->getBody()->getContents(), true);
+                if (isset($res['errmsg']) && Config::get('app_debug')) {
+                    Log::write($res, 'send_msg');
+                }
+            }
+        };
+        foreach ($pushList as $index => $item) {
+            $promises[] = $client->postAsync($url, ['body' => json_encode($item, JSON_UNESCAPED_UNICODE)]);
+            if (count($promises) == $concurrentSize) {
+                $promiseProcess($promises);
+                $promises = [];
+            }
+        }
+        if ($promises) {
+            $promiseProcess($promises);
+        }
+        return true;
+    }
+
+    /**
+     * 单次异步发送
+     * @param array $pushData
+     * @return bool
+     */
+    public function send($pushData)
+    {
+        $url = 'https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=' . $this->getAccessToken();
+        $client = new Client();
+        $client->postAsync($url, ['body' => json_encode($pushData, JSON_UNESCAPED_UNICODE)])->then(function (Response $response) {
+            $res = (array)json_decode($response->getBody()->getContents(), true);
+            if (isset($res['errmsg']) && Config::get('app_debug')) {
+                Log::write($res, 'send_msg');
+                return false;
+            }
+        })->wait();
+        return true;
+    }
+
+    private function getAccessToken()
+    {
+        $access_token = Cache::get('shop' . $this->appId);
+        if (!$access_token) {
+            $url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' . $this->appId . '&secret=' . $this->appSecret;
+            $res = Http::get($url);
+            $res = (array)json_decode($res, true);
+            if (isset($res['access_token'])) {
+                $access_token = $res['access_token'];
+            } else if (Config::get('app_debug')) {
+                Log::write('code:' . $res['errcode'] . ',message:' . $res['errmsg'], 'access_token');
+            }
+            Cache::set('shop' . $this->appId, $access_token, 7000);
+        }
+        return $access_token;
+    }
+
+    //生成小程序码
+    public function getWxCodeUnlimited($param)
+    {
+        $url = 'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=' . $this->getAccessToken();
+        $data = array_merge([
+            'width' => '280'
+        ], $param);       
+        return Http::post($url, json_encode($data,JSON_UNESCAPED_UNICODE));    
+    }
+}

+ 19 - 0
application/common/library/message/Mobile.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace app\common\library\message;
+
+use think\Hook;
+
+class Mobile
+{
+
+    public function send($param)
+    {
+        $params = [
+            'mobile'   => $param['mobile'],
+            'template' => $param['template_id'],
+            'msg'      => $param['data'],
+        ];
+        Hook::listen('sms_notice', $params, null, true);
+    }
+}

+ 95 - 0
application/common/library/message/Mp.php

@@ -0,0 +1,95 @@
+<?php
+
+namespace app\common\library\message;
+
+use fast\Http;
+use GuzzleHttp\Client;
+use GuzzleHttp\Psr7\Response;
+use think\Cache;
+use think\Log;
+use think\Config;
+
+class Mp
+{
+
+    private $appId = '';
+    private $appSecret = '';
+
+    public function __construct()
+    {
+        $config = get_addon_config('shop');
+        $this->appId = $config['mp_appid'];
+        $this->appSecret = $config['mp_app_secret'];
+    }
+
+    private function getAccessToken()
+    {
+        $access_token = Cache::get('shop' . $this->appId);
+        if (!$access_token) {
+            $url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' . $this->appId . '&secret=' . $this->appSecret;
+            $res = Http::get($url);
+            $res = (array)json_decode($res, true);
+            if (isset($res['access_token'])) {
+                $access_token = $res['access_token'];
+            } else if (Config::get('app_debug')) {
+                Log::write('code:' . $res['errcode'] . ',message:' . $res['errmsg'], 'access_token');
+            }
+            Cache::set('shop' . $this->appId, $access_token, 7000);
+        }
+        return $access_token;
+    }
+
+
+
+    /**
+     * 批量并发发送
+     * @param array $pushList
+     * @return bool
+     */
+    public function sendTemplateMessageMultiple($pushList)
+    {
+        $url = 'https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=' . $this->getAccessToken();
+        $concurrentSize = 10;
+        $promises = [];
+        $client = new Client();
+        $promiseProcess = function ($promises) {
+            $results = \GuzzleHttp\Promise\Utils::unwrap($promises);
+            foreach ($results as $key => $response) {
+                $res = (array)json_decode($response->getBody()->getContents(), true);
+                if (isset($res['errmsg']) && Config::get('app_debug')) {
+                    Log::write($res, 'send_msg');
+                }
+            }
+        };
+        foreach ($pushList as $index => $item) {
+            $promises[] = $client->postAsync($url, ['body' => json_encode($item, JSON_UNESCAPED_UNICODE)]);
+            if (count($promises) == $concurrentSize) {
+                $promiseProcess($promises);
+                $promises = [];
+            }
+        }
+        if ($promises) {
+            $promiseProcess($promises);
+        }
+        return true;
+    }
+
+    /**
+     * 单次异步发送
+     * @param array $pushData
+     * @return bool
+     */
+    public function send($pushData)
+    {
+        $url = 'https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=' . $this->getAccessToken();
+        $client = new Client();
+        $client->postAsync($url, ['body' => json_encode($pushData, JSON_UNESCAPED_UNICODE)])->then(function (Response $response) {
+            $res = (array)json_decode($response->getBody()->getContents(), true);
+            if (isset($res['errmsg']) && Config::get('app_debug')) {
+                Log::write($res, 'send_msg');
+                return false;
+            }
+        })->wait();
+        return true;
+    }
+}

+ 36 - 0
application/common/library/message/Service.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace app\common\library\message;
+
+use addons\shop\library\message\Mini;
+use addons\shop\library\message\Mp;
+use addons\shop\library\message\Email;
+use addons\shop\library\message\Mobile;
+
+class Service
+{
+
+    //分发数据
+    //根据组装的数据发送
+    public static function send($type, $data)
+    {
+        switch ($type) {
+            case 1:
+                $obj = new Mp();
+                break;
+            case 2:
+                $obj = new Mini();
+                break;
+            case 3:
+                $obj = new Email();
+                break;
+            case 4:
+                $obj = new Mobile();
+                break;
+            default:
+                throw new \Exception('类型不存在');
+        }
+        //异步并发发送
+        $obj->send($data);
+    }
+}

+ 98 - 0
application/common/model/Address.php

@@ -0,0 +1,98 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+use traits\model\SoftDelete;
+
+/**
+ * 模型
+ */
+class Address extends Model
+{
+
+    use SoftDelete;
+
+    // 表名
+    protected $name = 'shop_address';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = 'deletetime';
+
+    // 追加属性
+    protected $append = [];
+
+    public static function init()
+    {
+        parent::init();
+        self::beforeWrite(function ($row) {
+            $changed = $row->getChangedData();
+            if (isset($changed['isdefault']) && $changed['isdefault']) {
+                $info = \addons\shop\model\Address::where('isdefault', 1)->where('user_id', $row['user_id'])->find();
+                if ($info && (!isset($row['id']) || $info['id'] != $row['id'])) {
+                    $info->isdefault = 0;
+                    $info->save();
+                }
+            }
+            $row['address'] = $row->address_full;
+        });
+    }
+
+    public function getAddressBaseAttr($value, $data)
+    {
+        $areainfo = $this->getAreainfoAttr($value, $data);
+        $province = $areainfo['province'] ? $areainfo['province']['name'] : '';
+        $city = $areainfo['city'] ? $areainfo['city']['name'] : '';
+        $area = $areainfo['area'] ? $areainfo['area']['name'] : '';
+        $value = $data['address'];
+        $value = preg_replace("/^{$province}{$city}{$area}/", "", $value);
+        return $value;
+    }
+
+    public function getAddressFullAttr($value, $data)
+    {
+        $areainfo = $this->getAreainfoAttr($value, $data);
+        $province = $areainfo['province'] ? $areainfo['province']['name'] : '';
+        $city = $areainfo['city'] ? $areainfo['city']['name'] : '';
+        $area = $areainfo['area'] ? $areainfo['area']['name'] : '';
+        $value = $data['address'];
+        $value = preg_replace("/^{$province}{$city}{$area}/", "", $value);
+        $value = $province . $city . $area . $value;
+        return $value;
+    }
+
+    /**
+     * 获取城市和地区信息
+     * @param $value
+     * @param $data
+     * @return array
+     */
+    public function getAreainfoAttr($value, $data)
+    {
+        $result = [
+            'province' => null,
+            'city'     => null,
+            'area'     => null,
+        ];
+        $areaList = Area::where('id', 'in', [$data['province_id'], $data['city_id'], $data['area_id']])->select();
+        foreach ($areaList as $index => $item) {
+            $levelName = ($item['level'] == 1 ? 'province' : ($item['level'] == 2 ? 'city' : 'area'));
+            $result[$levelName] = $item;
+        }
+        return $result;
+    }
+
+    /**
+     * 获取会员地址列表
+     * @param $user_id
+     * @return false|\PDOStatement|string|\think\Collection
+     */
+    public static function getAddressList($user_id)
+    {
+        $addressList = self::where('user_id', $user_id)->where('status', 'normal')->order('usednums desc,id desc')->select();
+        return $addressList;
+    }
+}

+ 108 - 88
application/common/model/Area.php

@@ -1,88 +1,108 @@
-<?php
-
-namespace app\common\model;
-
-use think\Cache;
-use think\Model;
-
-/**
- * 地区数据模型
- */
-class Area extends Model
-{
-
-    /**
-     * 根据经纬度获取当前地区信息
-     *
-     * @param string $lng 经度
-     * @param string $lat 纬度
-     * @return Area 城市信息
-     */
-    public static function getAreaFromLngLat($lng, $lat, $level = 3)
-    {
-        $namearr = [1 => 'geo:province', 2 => 'geo:city', 3 => 'geo:district'];
-        $rangearr = [1 => 15000, 2 => 1000, 3 => 200];
-        $geoname = $namearr[$level] ?? $namearr[3];
-        $georange = $rangearr[$level] ?? $rangearr[3];
-        // 读取范围内的ID
-        $redis = Cache::store('redis')->handler();
-        $georadiuslist = [];
-        if (method_exists($redis, 'georadius')) {
-            $georadiuslist = $redis->georadius($geoname, $lng, $lat, $georange, 'km', ['WITHDIST', 'COUNT' => 5, 'ASC']);
-        }
-
-        if ($georadiuslist) {
-            list($id, $distance) = $georadiuslist[0];
-        }
-        $id = isset($id) && $id ? $id : 3;
-        return self::get($id);
-    }
-
-    /**
-     * 根据经纬度获取省份
-     *
-     * @param string $lng 经度
-     * @param string $lat 纬度
-     * @return Area
-     */
-    public static function getProvinceFromLngLat($lng, $lat)
-    {
-        $provincedata = null;
-        $citydata = self::getCityFromLngLat($lng, $lat);
-        if ($citydata) {
-            $provincedata = self::get($citydata['pid']);
-        }
-        return $provincedata;
-    }
-
-    /**
-     * 根据经纬度获取城市
-     *
-     * @param string $lng 经度
-     * @param string $lat 纬度
-     * @return Area
-     */
-    public static function getCityFromLngLat($lng, $lat)
-    {
-        $citydata = null;
-        $districtdata = self::getDistrictFromLngLat($lng, $lat);
-        if ($districtdata) {
-            $citydata = self::get($districtdata['pid']);
-        }
-        return $citydata;
-    }
-
-    /**
-     * 根据经纬度获取地区
-     *
-     * @param string $lng 经度
-     * @param string $lat 纬度
-     * @return Area
-     */
-    public static function getDistrictFromLngLat($lng, $lat)
-    {
-        $districtdata = self::getAreaFromLngLat($lng, $lat, 3);
-        return $districtdata;
-    }
-
-}
+<?php
+
+namespace app\common\model;
+
+use think\Cache;
+use think\Model;
+
+/**
+ * 地区数据模型
+ */
+class Area extends Model
+{
+
+    // 表名
+    protected $name = 'shop_area';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = false;
+    protected $updateTime = false;
+    // 追加属性
+    protected $append = [
+    ];
+    protected static $config = [];
+
+    protected static $tagCount = 0;
+
+    protected static function init()
+    {
+        $config = get_addon_config('shop');
+        self::$config = $config;
+    }
+
+    /**
+     * 根据经纬度获取当前地区信息
+     *
+     * @param string $lng 经度
+     * @param string $lat 纬度
+     * @return Area|null
+     */
+    public static function getAreaFromLngLat($lng, $lat, $level = 3)
+    {
+        $namearr = [1 => 'geo:province', 2 => 'geo:city', 3 => 'geo:district'];
+        $rangearr = [1 => 15000, 2 => 1000, 3 => 200];
+        $geoname = isset($namearr[$level]) ? $namearr[$level] : $namearr[3];
+        $georange = isset($rangearr[$level]) ? $rangearr[$level] : $rangearr[3];
+        $neararea = [];
+        // 读取范围内的ID
+        $redis = Cache::store('redis')->handler();
+        $georadiuslist = [];
+        if (method_exists($redis, 'georadius')) {
+            $georadiuslist = $redis->georadius($geoname, $lng, $lat, $georange, 'km', ['WITHDIST', 'COUNT' => 5, 'ASC']);
+        }
+
+        if ($georadiuslist) {
+            list($id, $distance) = $georadiuslist[0];
+        }
+        $id = isset($id) && $id ? $id : 3;
+        return self::get($id);
+    }
+
+    /**
+     * 根据经纬度获取省份
+     *
+     * @param string $lng 经度
+     * @param string $lat 纬度
+     * @return array
+     */
+    public static function getProvinceFromLngLat($lng, $lat)
+    {
+        $provincedata = [];
+        $citydata = self::getCityFromLngLat($lng, $lat);
+        if ($citydata) {
+            $provincedata = self::get($citydata['pid']);
+        }
+        return $provincedata;
+    }
+
+    /**
+     * 根据经纬度获取城市
+     *
+     * @param string $lng 经度
+     * @param string $lat 纬度
+     * @return array
+     */
+    public static function getCityFromLngLat($lng, $lat)
+    {
+        $citydata = [];
+        $districtdata = self::getDistrictFromLngLat($lng, $lat);
+        if ($districtdata) {
+            $citydata = self::get($districtdata['pid']);
+        }
+        return $citydata;
+    }
+
+    /**
+     * 根据经纬度获取地区
+     *
+     * @param string $lng 经度
+     * @param string $lat 纬度
+     * @return Area|null
+     */
+    public static function getDistrictFromLngLat($lng, $lat)
+    {
+        $districtdata = self::getAreaFromLngLat($lng, $lat, 3);
+        return $districtdata;
+    }
+}

+ 47 - 0
application/common/model/Attribute.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+
+class Attribute extends Model
+{
+
+
+    // 表名
+    protected $name = 'shop_attribute';
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+        'type_text'
+    ];
+
+
+    public function getTypeList()
+    {
+        return ['radio' => __('Type radio'), 'checkbox' => __('Type checkbox')];
+    }
+
+
+    public function getTypeTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['type'] ?? '');
+        $list = $this->getTypeList();
+        return $list[$value] ?? '';
+    }
+
+    public function AttributeValue()
+    {
+        return $this->hasMany('AttributeValue', 'attribute_id', 'id');
+    }
+
+}

+ 61 - 0
application/common/model/AttributeValue.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+/**
+ * 商品属性
+ * Class AttributeValue
+ * @package addons\shop\model
+ */
+class AttributeValue extends Model
+{
+
+    // 表名
+    protected $name = 'shop_attribute_value';
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [];
+
+    /**
+     * 获取商品属性
+     * @param $attribute_ids
+     * @return array|false|\PDOStatement|string|\think\Collection
+     */
+    public static function getAttributeList($attribute_ids)
+    {
+        if (!$attribute_ids) {
+            return [];
+        }
+        $list = self::with([
+            'AttributeValue' => function ($query) use ($attribute_ids) {
+                $query->field('id,attribute_id,name')->where('id', 'in', $attribute_ids);
+            },
+            'Attribute'
+        ])
+            ->field('MIN(`id`) AS `id`, `attribute_id`')
+            ->where('id', 'in', $attribute_ids)
+            ->group('attribute_id')
+            ->select();
+        return $list;
+    }
+
+    public function AttributeValue()
+    {
+        return $this->hasMany('AttributeValue', 'attribute_id', 'attribute_id');
+    }
+
+    public function Attribute()
+    {
+        return $this->hasOne('Attribute', 'id', 'attribute_id', [], 'LEFT')->bind('name');
+    }
+}

+ 191 - 0
application/common/model/Block.php

@@ -0,0 +1,191 @@
+<?php
+
+namespace app\common\model;
+
+use app\common\library\Service;
+use think\Cache;
+use think\Db;
+use think\Model;
+use think\View;
+
+/**
+ * 区块模型
+ */
+class Block extends Model
+{
+    protected $name = "shop_block";
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    // 追加属性
+    protected $append = [
+    ];
+    protected static $config = [];
+
+    protected static $tagCount = 0;
+
+    protected static function init()
+    {
+        $config = get_addon_config('shop');
+        self::$config = $config;
+    }
+
+    public function getImageAttr($value, $data)
+    {
+        $value = $value ? $value : self::$config['default_block_img'];
+        return cdnurl($value, true);
+    }
+
+    public function getContentAttr($value, $data)
+    {
+        if (isset($data['parsetpl']) && $data['parsetpl']) {
+            $view = View::instance();
+            $view->engine->layout(false);
+            return $view->display($data['content']);
+        }
+        return $data['content'];
+    }
+
+    public function getHasimageAttr($value, $data)
+    {
+        return $this->getData("image") ? true : false;
+    }
+
+    /**
+     * 通过名称获取区块列表
+     * @param $name
+     * @return false|\PDOStatement|string|\think\Collection
+     */
+    public static function getBlockListByName($name)
+    {
+        return self::getBlockList(['name' => $name]);
+    }
+
+    /**
+     * 获取区块列表
+     * @param $params
+     * @return false|\PDOStatement|string|\think\Collection
+     */
+    public static function getBlockList($params)
+    {
+        $config = get_addon_config('shop');
+        $name = empty($params['name']) ? '' : $params['name'];
+        $type = empty($params['type']) ? '' : $params['type'];
+        $condition = empty($params['condition']) ? '' : $params['condition'];
+        $field = empty($params['field']) ? '*' : $params['field'];
+        $row = empty($params['row']) ? 10 : (int)$params['row'];
+        $orderby = empty($params['orderby']) ? 'id' : $params['orderby'];
+        $orderway = empty($params['orderway']) ? 'desc' : strtolower($params['orderway']);
+        $limit = empty($params['limit']) ? $row : $params['limit'];
+        $imgwidth = empty($params['imgwidth']) ? '' : $params['imgwidth'];
+        $imgheight = empty($params['imgheight']) ? '' : $params['imgheight'];
+        $orderway = in_array($orderway, ['asc', 'desc']) ? $orderway : 'desc';
+        $paginate = !isset($params['paginate']) ? false : $params['paginate'];
+
+        list($cacheKey, $cacheExpire) = Service::getCacheKeyExpire('blocklist', $params);
+
+        self::$tagCount++;
+
+        $where = ['status' => 'normal'];
+        if ($name !== '') {
+            $where['name'] = $name;
+        }
+        if (!empty($type)) {
+            $where['type'] = ['in', $type];
+        }
+        $order = $orderby == 'rand' ? Db::raw('rand()') : (preg_match("/\,|\s/", $orderby) ? $orderby : "{$orderby} {$orderway}");
+        $order = $orderby == 'weigh' ? $order . ',id DESC' : $order;
+
+        $blockModel = self::where($where)
+            ->where($condition)
+            ->field($field)
+            ->orderRaw($order);
+
+        if ($paginate) {
+            $paginateArr = explode(',', $paginate);
+            $listRows = is_numeric($paginate) ? $paginate : (is_numeric($paginateArr[0]) ? $paginateArr[0] : $row);
+            $config = [];
+            $config['var_page'] = $paginateArr[2] ?? 'bpage' . self::$tagCount;
+            $config['path'] = $paginateArr[3] ?? '';
+            $config['fragment'] = $paginateArr[4] ?? '';
+            $config['query'] = request()->get();
+            $config['type'] = '\\addons\\shop\\library\\Bootstrap';
+            $list = $blockModel->paginate($listRows, ($paginateArr[1] ?? false), $config);
+        } else {
+            $list = $blockModel->limit($limit)->cache($cacheKey, $cacheExpire, 'shop')->select();
+        }
+
+        self::render($list, $imgwidth, $imgheight);
+        return $list;
+    }
+
+    public static function render(&$list, $imgwidth, $imgheight)
+    {
+        $width = $imgwidth ? 'width="' . $imgwidth . '"' : '';
+        $height = $imgheight ? 'height="' . $imgheight . '"' : '';
+        $time = time();
+        foreach ($list as $k => &$v) {
+            if (($v['begintime'] && $time < $v['begintime']) || ($v['endtime'] && $time > $v['endtime'])) {
+                unset($list[$k]);
+                continue;
+            }
+            $v['textlink'] = '<a href="' . $v['url'] . '">' . $v['title'] . '</a>';
+            $v['imglink'] = '<a href="' . $v['url'] . '"><img src="' . $v['image'] . '" border="" ' . $width . ' ' . $height . ' /></a>';
+            $v['img'] = '<img src="' . $v['image'] . '" border="" ' . $width . ' ' . $height . ' />';
+        }
+        return $list;
+    }
+
+    /**
+     * 获取区块内容
+     * @param $params
+     * @return string
+     */
+    public static function getBlockContent($params)
+    {
+        $fieldName = isset($params['id']) ? 'id' : 'name';
+        $value = $params[$fieldName] ?? '';
+        $field = $params['field'] ?? '';
+
+        list($cacheKey, $cacheExpire) = Service::getCacheKeyExpire('blockinfo', $params);
+
+        $row = self::where($fieldName, $value)
+            ->where('status', 'normal')
+            ->cache($cacheKey, $cacheExpire, 'shop')
+            ->find();
+        $result = '';
+        if ($row) {
+            $content = $row->getData('content');
+            if ($field && isset($row[$field])) {
+                $result = $row->getData($field);
+            } else {
+                if ($content) {
+                    $result = $content;
+                } elseif ($row['image']) {
+                    $result = '<img src="' . $row['image'] . '" class="img-responsive"/>';
+                } else {
+                    $result = $row['title'];
+                }
+                if ($row['url'] && !$content) {
+                    $result = $row['url'] ? '<a href="' . (preg_match("/^https?:\/\/(.*)/i", $row['url']) ? $row['url'] : url($row['url'])) . '" target="_blank">' . $result . '</a>' : $result;
+                }
+            }
+            $row['begintime'] = (int)$row['begintime'];
+            $row['endtime'] = (int)$row['endtime'];
+            if (!$content) {
+                return $result;
+            } else {
+                if (!$row['parsetpl']) {
+                    $tagIdentify = "taglib_shop_block_content_" . $row['id'];
+                    Cache::set($tagIdentify, $result);
+                    $result = "{:cache('{$tagIdentify}')}";
+                }
+            }
+            //未开始或过期处理
+            $result = "{if (!{$row['begintime']} || time()>{$row['begintime']})&&(!{$row['endtime']} || time()<{$row['endtime']})}{$result}{/if}";
+        }
+        return $result;
+    }
+}

+ 31 - 0
application/common/model/Brand.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+
+class Brand extends Model
+{
+       
+
+    // 表名
+    protected $name = 'shop_brand';
+    
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+
+    ];
+    
+
+
+
+}

+ 47 - 0
application/common/model/Card.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+class Card extends Model
+{
+
+    // 表名
+    protected $name = 'shop_card';
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+        'status_text'
+    ];
+
+    public function getStatusList()
+    {
+        return ['normal' => __('Normal'), 'hidden' => __('Hidden')];
+    }
+
+    public function getStatusTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['status'] ?? '');
+        $list = $this->getStatusList();
+        return $list[$value] ?? '';
+    }
+
+    public function setContentAttr($value, $data)
+    {
+        return htmlentities($value);
+    }
+
+    public function getContentAttr($value, $data)
+    {
+        return html_entity_decode($value);
+    }
+}

+ 106 - 0
application/common/model/Carts.php

@@ -0,0 +1,106 @@
+<?php
+
+namespace app\common\model;
+
+use think\Cookie;
+use think\Model;
+
+/**
+ * 模型
+ */
+class Carts extends Model
+{
+
+    // 表名
+    protected $name = 'shop_carts';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = '';
+    // 追加属性
+    protected $append = [];
+
+    /**
+     * @ DateTime 2021-05-31
+     * @ 获取购物车商品列表
+     * @param string  $ids
+     * @param integer $user_id
+     * @param integer $sceneval
+     * @return array
+     */
+    public static function getGoodsList($ids, $user_id)
+    {
+        return (new self())->field("c.*,GROUP_CONCAT(sp.name,':',sv.value order by sp.id asc) sku_attr")
+            ->with([
+                'Goods' => function ($query) {
+                    $query->where('status', 'normal');
+                },
+                'Sku'
+            ])
+            ->alias('c')
+            ->join('shop_goods_sku sku', 'c.goods_sku_id=sku.id', 'LEFT')
+            ->join('shop_goods_sku_spec p', "FIND_IN_SET(p.id,sku.sku_id)", 'LEFT')
+            ->join('shop_spec sp', 'sp.id=p.spec_id', 'LEFT')
+            ->join('shop_spec_value sv', 'sv.id=p.spec_value_id', 'LEFT')
+            ->where(function ($query) use ($ids) {
+                if ($ids) {
+                    $query->where('c.id', 'in', $ids);
+                }
+            })
+            ->where('c.user_id', $user_id)
+            ->order('c.createtime desc')
+            ->group('c.id')
+            ->select();
+    }
+
+    /**
+     * 添加商品到购物车
+     *
+     * @param string $goods_id     商品ID
+     * @param string $goods_sku_id 商品SKUID
+     * @param int    $nums         数量
+     * @param int    $user_id      会员ID
+     * @return mixed
+     */
+    public static function push($goods_id, $goods_sku_id, $nums = 1, $user_id = 0)
+    {
+
+        $row = (new self)->where('goods_id', $goods_id)
+            ->where('goods_sku_id', $goods_sku_id)
+            ->where('user_id', $user_id)
+            ->find();
+        //已存在,数量加
+        if ($row) {
+            $row->setInc('nums', $nums);
+        } else {
+            $row = (new self);
+            $row->save([
+                'goods_id'     => $goods_id,
+                'goods_sku_id' => $goods_sku_id,
+                'user_id'      => $user_id,
+                'nums'         => $nums
+            ]);
+        }
+        return $row->id;
+    }
+
+    /**
+     * 清空购物车
+     */
+    public static function clear($cart_ids)
+    {
+        self::where('id', 'IN', $cart_ids)->delete();
+    }
+
+
+    public function Goods()
+    {
+        return $this->belongsTo('Goods', 'goods_id', 'id', [], 'LEFT');
+    }
+
+    public function Sku()
+    {
+        return $this->belongsTo('Sku', 'goods_sku_id', 'id', [], 'LEFT');
+    }
+}

+ 346 - 28
application/common/model/Category.php

@@ -2,6 +2,10 @@
 
 
 namespace app\common\model;
 namespace app\common\model;
 
 
+use app\common\library\Service;
+use app\common\model\Attribute as AttributeModel;
+use think\Cache;
+use think\Db;
 use think\Model;
 use think\Model;
 
 
 /**
 /**
@@ -10,6 +14,7 @@ use think\Model;
 class Category extends Model
 class Category extends Model
 {
 {
 
 
+    protected $name = 'shop_category';
     // 开启自动写入时间戳字段
     // 开启自动写入时间戳字段
     protected $autoWriteTimestamp = 'int';
     protected $autoWriteTimestamp = 'int';
     // 定义时间戳字段名
     // 定义时间戳字段名
@@ -17,16 +22,27 @@ class Category extends Model
     protected $updateTime = 'updatetime';
     protected $updateTime = 'updatetime';
     // 追加属性
     // 追加属性
     protected $append = [
     protected $append = [
-        'type_text',
+        'url',
         'flag_text',
         'flag_text',
     ];
     ];
 
 
+    protected static $config = [];
+
+    protected static $tagCount = 0;
+
+    protected static $parentIds = null;
+
+    protected static $outlinkParentIds = null;
+
+    protected static $navParentIds = null;
+
     protected static function init()
     protected static function init()
     {
     {
+        $config = get_addon_config('shop');
+        self::$config = $config;
+
         self::afterInsert(function ($row) {
         self::afterInsert(function ($row) {
-            if (!$row['weigh']) {
-                $row->save(['weigh' => $row['id']]);
-            }
+            $row->save(['weigh' => $row['id']]);
         });
         });
     }
     }
 
 
@@ -35,24 +51,44 @@ class Category extends Model
         return is_array($value) ? implode(',', $value) : $value;
         return is_array($value) ? implode(',', $value) : $value;
     }
     }
 
 
-    /**
-     * 读取分类类型
-     * @return array
-     */
-    public static function getTypeList()
+    public function getUrlAttr($value, $data)
+    {
+        return $this->buildUrl($value, $data);
+    }
+
+    public function getFullurlAttr($value, $data)
     {
     {
-        $typeList = config('site.categorytype');
-        foreach ($typeList as $k => &$v) {
-            $v = __($v);
+        return $this->buildUrl($value, $data, true);
+    }
+
+    private function buildUrl($value, $data, $domain = false)
+    {
+        $diyname = isset($data['diyname']) && $data['diyname'] ? $data['diyname'] : $data['id'];
+        $cateid = $data['id'] ?? 0;
+        $catename = isset($data['diyname']) && $data['diyname'] ? $data['diyname'] : 'all';
+        $time = $data['createtime'] ?? time();
+
+        $vars = [
+            ':id'       => $data['id'],
+            ':diyname'  => $diyname,
+            ':category' => $cateid,
+            ':catename' => $catename,
+            ':cateid'   => $cateid,
+            ':year'     => date("Y", $time),
+            ':month'    => date("m", $time),
+            ':day'      => date("d", $time)
+        ];
+        if (isset($data['type']) && isset($data['outlink']) && $data['type'] == 'link') {
+            return $this->getAttr('outlink');
         }
         }
-        return $typeList;
+        $suffix = static::$config['moduleurlsuffix']['category'] ?? static::$config['urlsuffix'];
+        return addon_url('shop/category/index', $vars, $suffix, $domain);
     }
     }
 
 
-    public function getTypeTextAttr($value, $data)
+    public function getImageAttr($value, $data)
     {
     {
-        $value = $value ? $value : $data['type'];
-        $list = $this->getTypeList();
-        return $list[$value] ?? '';
+        $value = $value ? $value : self::$config['default_category_img'];
+        return cdnurl($value, true);
     }
     }
 
 
     public function getFlagList()
     public function getFlagList()
@@ -60,30 +96,312 @@ class Category extends Model
         return ['hot' => __('Hot'), 'index' => __('Index'), 'recommend' => __('Recommend')];
         return ['hot' => __('Hot'), 'index' => __('Index'), 'recommend' => __('Recommend')];
     }
     }
 
 
+    public function getOutlinkAttr($value, $data)
+    {
+        $indexUrl = $view_replace_str = config('view_replace_str.__PUBLIC__');
+        $indexUrl = rtrim($indexUrl, '/');
+        return str_replace('__INDEX__', $indexUrl, $value ?: '');
+    }
+
     public function getFlagTextAttr($value, $data)
     public function getFlagTextAttr($value, $data)
     {
     {
-        $value = $value ? $value : $data['flag'];
+        $value = $value ?: ($data['flag'] ?? '');
         $valueArr = explode(',', $value);
         $valueArr = explode(',', $value);
         $list = $this->getFlagList();
         $list = $this->getFlagList();
         return implode(',', array_intersect_key($list, array_flip($valueArr)));
         return implode(',', array_intersect_key($list, array_flip($valueArr)));
     }
     }
 
 
     /**
     /**
-     * 读取分类列表
-     * @param string $type   指定类型
-     * @param string $status 指定状态
+     * 判断是否拥有子列表
+     * @param $value
+     * @param $data
+     * @return bool|mixed
+     */
+    public function getHasChildAttr($value, $data)
+    {
+        static $checked = [];
+        if (isset($checked[$data['id']])) {
+            return $checked[$data['id']];
+        }
+        if (is_null(self::$parentIds)) {
+            self::$parentIds = self::where('pid', '>', 0)->cache(true, null, 'shop')->where('status', 'normal')->column('pid');
+        }
+        if (self::$parentIds && in_array($data['id'], self::$parentIds)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 判断导航是否拥有子列表
+     * @param $value
+     * @param $data
+     * @return bool|mixed
+     */
+    public function getHasNavChildAttr($value, $data)
+    {
+        static $checked = [];
+        if (isset($checked[$data['id']])) {
+            return $checked[$data['id']];
+        }
+        if (is_null(self::$navParentIds)) {
+            self::$navParentIds = self::where('pid', '>', 0)->cache(true, null, 'shop')->where('status', 'normal')->where('isnav', 1)->column('pid');
+        }
+        if (self::$navParentIds && in_array($data['id'], self::$navParentIds)) {
+            return true;
+        }
+        return false;
+    }
+
+    public static function getIndexCategoryList()
+    {
+        $categoryList = self::where('status', 'normal')
+            ->where('pid', 0)
+            ->where("FIND_IN_SET('index',`flag`)")
+            ->limit(9)
+            ->order('weigh desc,id asc')
+            ->cache(false)
+            ->select();
+        $categoryList = collection($categoryList)->toArray();
+        return $categoryList;
+    }
+
+
+    /**
+     * 获取面包屑导航
+     * @param array $category
+     * @param array $goods
+     * @param array $page
      * @return array
      * @return array
      */
      */
-    public static function getCategoryArray($type = null, $status = null)
+    public static function getBreadcrumb($category, $goods = [], $page = [])
     {
     {
-        $list = collection(self::where(function ($query) use ($type, $status) {
-            if (!is_null($type)) {
-                $query->where('type', '=', $type);
+        $list = [];
+        $list[] = ['name' => __('Home'), 'url' => addon_url('shop/index/index', [], false)];
+        if ($category) {
+            if ($category['pid']) {
+                $categoryList = self::where('status', 'normal')
+                    ->order('weigh desc,id desc')
+                    ->field('id,name,pid,diyname')
+                    ->cache(true, null, 'shop')
+                    ->select();
+                //获取栏目的所有上级栏目
+                $parents = \fast\Tree::instance()->init(collection($categoryList)->toArray(), 'pid')->getParents($category['id']);
+                foreach ($parents as $k => $v) {
+                    $list[] = ['name' => $v['name'], 'url' => $v['url']];
+                }
             }
             }
-            if (!is_null($status)) {
-                $query->where('status', '=', $status);
+            $list[] = ['name' => $category['name'], 'url' => $category['url']];
+        }
+        if ($goods) {
+            //$list[] = ['name' => $goods['title'], 'url' => $goods['url']];
+        }
+        if ($page && $category['url'] != $page['url']) {
+            $list[] = ['name' => $page['title'], 'url' => $page['url']];
+        }
+        return $list;
+    }
+
+    /**
+     * 获取导航分类列表HTML
+     * @param       $category
+     * @param array $tag
+     * @return mixed|string
+     */
+    public static function getNav($category, $tag = [])
+    {
+        $config = get_addon_config('shop');
+        $condition = empty($tag['condition']) ? '' : $tag['condition'];
+        $maxLevel = !isset($tag['maxlevel']) ? 0 : $tag['maxlevel'];
+
+        list($cacheKey, $cacheExpire) = Service::getCacheKeyExpire('nav', $tag);
+
+        $cacheName = 'shop-nav-' . md5(serialize($tag));
+        $result = Cache::tag('shop')->get($cacheName);
+        if ($result === false) {
+            $categoryList = Category::where($condition)
+                ->where('status', 'normal')
+                ->order('weigh desc,id desc')
+                ->cache($cacheKey, $cacheExpire, 'shop')
+                ->select();
+            $tree = \fast\Tree::instance();
+            $tree->init(collection($categoryList)->toArray(), 'pid');
+            $result = self::getTreeUl($tree, 0, $category ? $category['id'] : '', '', 1, $maxLevel);
+            Cache::tag('shop')->set($cacheName, $result);
+        }
+        return $result;
+    }
+
+    /**
+     * 获取栏目所有子级的ID
+     * @param mixed $ids      栏目ID或集合ID
+     * @param bool  $withself 是否包含自身
+     * @return array
+     */
+    public static function getCategoryChildrenIds($ids, $withself = true)
+    {
+        $cacheName = 'shop-childrens-' . $ids . '-' . $withself;
+        $result = Cache::get($cacheName);
+        if ($result === false) {
+            $categoryList = Category::where('status', 'normal')
+                ->order('weigh desc,id desc')
+                ->cache(true, null, 'shop')
+                ->select();
+
+            $result = [];
+            $tree = \fast\Tree::instance();
+            $tree->init(collection($categoryList)->toArray(), 'pid');
+            $CategoryIds = is_array($ids) ? $ids : explode(',', $ids);
+            foreach ($CategoryIds as $index => $CategoryId) {
+                $result = array_merge($result, $tree->getChildrenIds($CategoryId, $withself));
+            }
+            Cache::set($cacheName, $result);
+        }
+        return $result;
+    }
+
+    /**
+     * 获取分类列表
+     * @param $tag
+     * @return false|\PDOStatement|string|\think\Collection
+     */
+    public static function getCategoryList($tag)
+    {
+        $config = get_addon_config('shop');
+        $type = empty($tag['type']) ? '' : $tag['type'];
+        $typeid = !isset($tag['typeid']) ? '' : $tag['typeid'];
+        $condition = empty($tag['condition']) ? '' : $tag['condition'];
+        $field = empty($tag['field']) ? '*' : $tag['field'];
+        $row = empty($tag['row']) ? 10 : (int)$tag['row'];
+        $flag = empty($tag['flag']) ? '' : $tag['flag'];
+        $orderby = empty($tag['orderby']) ? 'weigh' : $tag['orderby'];
+        $orderway = empty($tag['orderway']) ? 'desc' : strtolower($tag['orderway']);
+        $limit = empty($tag['limit']) ? $row : $tag['limit'];
+        $orderway = in_array($orderway, ['asc', 'desc']) ? $orderway : 'desc';
+        $paginate = !isset($tag['paginate']) ? false : $tag['paginate'];
+
+        list($cacheKey, $cacheExpire) = Service::getCacheKeyExpire('categorylist', $tag);
+
+        $where = ['status' => 'normal'];
+
+        self::$tagCount++;
+
+        if ($type === 'top') {
+            //顶级分类
+            $where['pid'] = 0;
+        } elseif ($type === 'brother') {
+            $subQuery = self::where('id', 'in', $typeid)->field('pid')->buildSql();
+            //同级
+            $where['pid'] = ['exp', Db::raw(' IN ' . '(' . $subQuery . ')')];
+        } elseif ($type === 'son') {
+            $subQuery = self::where('pid', 'in', $typeid)->field('id')->buildSql();
+            //子级
+            $where['id'] = ['exp', Db::raw(' IN ' . '(' . $subQuery . ')')];
+        } elseif ($type === 'sons') {
+            //所有子级
+            $where['id'] = ['in', self::getCategoryChildrenIds($typeid)];
+        } else {
+            if ($typeid !== '') {
+                $where['id'] = ['in', $typeid];
+            }
+        }
+
+        //如果有设置标志,则拆分标志信息并构造condition条件
+        if ($flag !== '') {
+            if (stripos($flag, '&') !== false) {
+                $arr = [];
+                foreach (explode('&', $flag) as $k => $v) {
+                    $arr[] = "FIND_IN_SET('{$v}', flag)";
+                }
+                if ($arr) {
+                    $condition .= "(" . implode(' AND ', $arr) . ")";
+                }
+            } else {
+                $condition .= ($condition ? ' AND ' : '');
+                $arr = [];
+                foreach (explode(',', str_replace('|', ',', $flag)) as $k => $v) {
+                    $arr[] = "FIND_IN_SET('{$v}', flag)";
+                }
+                if ($arr) {
+                    $condition .= "(" . implode(' OR ', $arr) . ")";
+                }
             }
             }
-        })->order('weigh', 'desc')->select())->toArray();
+        }
+
+        $order = $orderby == 'rand' ? Db::raw('rand()') : (preg_match("/\,|\s/", $orderby) ? $orderby : "{$orderby} {$orderway}");
+        $order = $orderby == 'weigh' ? $order . ',id DESC' : $order;
+
+        $CategoryModel = self::where($where)
+            ->where($condition)
+            ->field($field)
+            ->orderRaw($order);
+        if ($paginate) {
+            $paginateArr = explode(',', $paginate);
+            $listRows = is_numeric($paginate) ? $paginate : (is_numeric($paginateArr[0]) ? $paginateArr[0] : $row);
+            $config = [];
+            $config['var_page'] = $paginateArr[2] ?? 'cpage' . self::$tagCount;
+            $config['path'] = $paginateArr[3] ?? '';
+            $config['fragment'] = $paginateArr[4] ?? '';
+            $config['query'] = request()->get();
+            $list = $CategoryModel->paginate($listRows, ($paginateArr[1] ?? false), $config);
+        } else {
+            $list = $CategoryModel->limit($limit)->cache($cacheKey, $cacheExpire, 'shop')->select();
+        }
+
         return $list;
         return $list;
     }
     }
+
+    public static function getFilterList($category, $filter, $params = [], $multiple = false)
+    {
+        $filterList = [];
+        $attributeList = (new AttributeModel())->with(['AttributeValue'])->where('category_id', $category['id'])->where('is_search', 1)->select();
+
+        foreach ($attributeList as $k => $v) {
+            $v['title'] = $v['name'];
+            $v['name'] = 'f_' . $v['id'];
+            $content = [];
+            $valueList = ['' => __('All')];
+            foreach ($v['attribute_value'] as $index => $item) {
+                $valueList[$item['id']] = $item['name'];
+            }
+            foreach ($valueList as $m => $n) {
+                $filterArr = isset($filter[$v['name']]) && $filter[$v['name']] !== '' ? ($multiple ? explode(',', $filter[$v['name']]) : [$filter[$v['name']]]) : [];
+                $active = ($m === '' && !$filterArr) || ($m !== '' && in_array($m, $filterArr)) ? true : false;
+                if ($active) {
+                    $current = implode(',', array_diff($filterArr, [$m]));
+                } else {
+                    $current = $multiple ? implode(',', array_merge($filterArr, [$m])) : $m;
+                }
+                $prepare = $m === '' ? array_diff_key($filter, [$v['name'] => $m]) : array_merge($filter, [$v['name'] => $current]);
+                $url = '?' . str_replace(['%2C', '%3B'], [',', ';'], http_build_query(array_merge($prepare, array_intersect_key($params, array_flip(['orderby', 'orderway', 'multiple'])))));
+                $content[] = ['value' => $m, 'title' => $n, 'active' => $active, 'url' => $url];
+            }
+
+            $filterList[] = [
+                'name'   => $v['name'],
+                'title'  => $v['title'],
+                'values' => $content,
+            ];
+        }
+        foreach ($filter as $index => &$item) {
+            $item = is_array($item) ? $item : explode(',', str_replace(';', ',', $item));
+        }
+        return $filterList;
+    }
+
+    public static function getCategorySubList($id)
+    {
+        return self::where('pid', $id)->where('status', 'normal')->order('weigh desc,id asc')->select();
+    }
+
+    public static function getCategorySubIds($id)
+    {
+        return self::where('pid', $id)->where('status', 'normal')->column('id');
+    }
+
+    public function Goods()
+    {
+        return $this->hasMany('Goods', 'category_id', 'id');
+    }
+
 }
 }

+ 118 - 0
application/common/model/Collect.php

@@ -0,0 +1,118 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+
+class Collect extends Model
+{
+
+    // 表名
+    protected $name = 'shop_collect';
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [      
+        'status_text'
+    ];
+
+
+    public function getStatusList()
+    {
+        return ['0' => __('Status 0'), '1' => __('Status 1')];
+    }
+
+
+    public function getStatusTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['status'] ?? '');
+        $list = $this->getStatusList();
+        return $list[$value] ?? '';
+    }
+
+    /**
+     * 添加收藏/取消收藏
+     * @param [type] $user_id   
+     * @param [type] $goods_id
+     * @return true
+     */
+    public static function addOrCancel($user_id, $goods_id)
+    {
+        $data = self::where('user_id', $user_id)->where('goods_id', $goods_id)->find();
+        //不存在,添加收藏
+        if (!$data) {
+            return (new self)->save([
+                'user_id' => $user_id,
+                'goods_id' => $goods_id
+            ]);
+        }
+        if ($data->status == 1) { //已收藏,为取消
+            $data->status = 0;
+        } else { //已取消,添加收藏
+            $data->status = 1;
+        }
+        return $data->save();
+    }
+
+    /**
+     * 渲染收藏
+     *
+     * @param [type] $list
+     * @param [type] $user_id  
+     * @return void
+     */
+    public static function render($list, $user_id)
+    {
+        $data = self::where('user_id', $user_id)->where('status', 1)->select();
+        $newData = [];
+        foreach ($data as $item) {
+            $newData[$item['goods_id']] = $item['status'];
+        }
+        foreach ($list as $res) {
+            $res->isCollect = isset($newData[$res['id']]) ?? $newData[$res['id']]['status'];
+        }
+    }
+
+    /**
+     * 收藏列表
+     *
+     * @param [type] $param
+     * @return \think\Paginator
+     */
+    public static function tableList($param)
+    {
+        $pageNum = 15;
+        if (!empty($param['num'])) {
+            $pageNum = $param['num'];
+        }
+
+        $list = self::with(['Goods' => function ($query) {
+            $query->field('id,title,image,price,marketprice,description');
+        }])
+            ->field('id,goods_id,user_id,status,createtime')
+            ->where(function ($query) use ($param) {
+
+                $query->where('status', 1);
+
+                if (!empty($param['user_id'])) {
+                    $query->where('user_id', $param['user_id']);
+                }
+            })->order('createtime desc')->paginate($pageNum);
+
+
+        return $list;
+    }
+
+    public function Goods()
+    {
+        return $this->belongsTo('Goods', 'goods_id', 'id', [], 'LEFT');
+    }
+}

+ 117 - 0
application/common/model/Comment.php

@@ -0,0 +1,117 @@
+<?php
+
+namespace app\common\model;
+
+use app\common\library\Service;
+use app\common\exception\CommentException;
+use app\common\library\Auth;
+use fast\Tree;
+use think\Exception;
+use think\Model;
+use think\Validate;
+
+/**
+ * 模型
+ */
+class Comment extends Model
+{
+
+    // 表名
+    protected $name = 'shop_comment';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    // 追加属性
+    protected $append = [];
+
+    public function getImagesAttr($value, $data)
+    {
+        $imagesArr = explode(',', $data['images'] ?? '');
+        foreach ($imagesArr as $index => &$item) {
+            $item && $item = cdnurl($item, true);
+        }
+        return array_filter($imagesArr);
+    }
+
+    public function setImagesAttr($value, $data)
+    {
+        return is_array($value) ? implode(',', $value) : $value;
+    }
+
+    public function User()
+    {
+        return $this->belongsTo('User', 'user_id', 'id', [], 'LEFT');
+    }
+
+    public function Manage()
+    {
+        return $this->belongsTo('\app\admin\model\Admin', 'user_id', 'id', [], 'LEFT');
+    }
+
+    public function Goods()
+    {
+        return $this->belongsTo('Goods', 'goods_id', 'id', [], 'LEFT');
+    }
+
+    /**
+     * 获取评论列表
+     */
+    public static function getCommentList($params)
+    {
+        $goods_id = empty($params['goods_id']) ? 0 : $params['goods_id'];
+        $pid = empty($params['pid']) ? 0 : $params['pid'];
+        $condition = empty($params['condition']) ? '' : $params['condition'];
+        $fragment = empty($params['fragment']) ? 'comments' : $params['fragment'];
+        $row = empty($params['row']) ? 10 : (int)$params['row'];
+        $orderby = empty($params['orderby']) ? 'nums' : $params['orderby'];
+        $orderway = empty($params['orderway']) ? 'desc' : strtolower($params['orderway']);
+        $pagesize = empty($params['pagesize']) ? $row : $params['pagesize'];
+        $page = empty($params['page']) ? 1 : (int)$params['page'];
+        $orderway = in_array($orderway, ['asc', 'desc']) ? $orderway : 'desc';
+
+        $where = [
+            'status' => 'normal'
+        ];
+        if ($goods_id !== '') {
+            $where['goods_id'] = $goods_id;
+        }
+        if ($pid !== '') {
+            $where['pid'] = $pid;
+        }
+        $order = $orderby == 'rand' ? 'rand()' : (in_array($orderby, ['pid', 'id', 'createtime', 'updatetime']) ? "{$orderby} {$orderway}" : "id {$orderway}");
+        $config = [
+            'type'     => '\\addons\\shop\\library\\Bootstrap',
+            'var_page' => 'cp',
+            'fragment' => $fragment,
+            'page'     => $page
+        ];
+        $list = self::with(['user', 'reply' => function ($query) {
+            $query->with(['manage' => function ($user) {
+                $user->field('id,nickname');
+            }]);
+        }])
+            ->where($where)
+            ->where($condition)
+            ->order($order)
+            ->paginate($pagesize, false, $config);
+        return $list;
+    }
+
+    //好评度
+    public static function degree($goods_id)
+    {
+        $total = self::where('goods_id', $goods_id)->where('pid', 0)->where('status', 'normal')->sum('star');
+        $favorable = self::where('goods_id', $goods_id)->where('pid', 0)->where('status', 'normal')->where('star', '>', 3)->sum('star');
+        if (!$total || !$favorable) {
+            return 100;
+        }
+        return bcmul(bcdiv($favorable, $total, 2), 100);
+    }
+
+    public function reply()
+    {
+        return $this->hasMany('Comment', 'pid', 'id')->where('status', 'normal');
+    }
+}

+ 446 - 0
application/common/model/Coupon.php

@@ -0,0 +1,446 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+use addons\shop\model\UserCoupon;
+use addons\shop\library\coupon\ToCalculate;
+use addons\shop\library\coupon\Discount;
+use addons\shop\library\coupon\FullReduction;
+use addons\shop\model\Order;
+use addons\shop\library\IntCode;
+
+class Coupon extends Model
+{
+
+    // 表名
+    protected $name = 'shop_coupon';
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+        'condition_text',
+        'result_text',
+        'is_open_text',
+        'url',
+        'use_times_text',
+        'receive_times',
+    ];
+
+    protected static $config = [];
+
+    protected static function init()
+    {
+        $config = get_addon_config('shop');
+        self::$config = $config;
+    }
+
+    public function getUrlAttr($value, $data)
+    {
+        return $this->buildUrl($value, $data);
+    }
+
+    public function getFullurlAttr($value, $data)
+    {
+        return $this->buildUrl($value, $data, true);
+    }
+
+    private function buildUrl($value, $data, $domain = false)
+    {
+        $vars = [
+            ':coupon' => $data['id']
+        ];
+        $suffix = static::$config['moduleurlsuffix']['coupon'] ?? static::$config['urlsuffix'];
+        return addon_url('shop/coupon/show', $vars, $suffix, $domain);
+    }
+
+    public function getReceiveTimesAttr($value, $data)
+    {
+        return date("Y-m-d H:i:s", $data['begintime']) . ' - ' . date("Y-m-d H:i:s", $data['endtime']);
+    }
+
+    public function getConditionList()
+    {
+        return ['0' => __('Condition 0'), '1' => __('Condition 1'), '2' => __('Condition 2'), '3' => __('Condition 3'), '4' => __('Condition 4')];
+    }
+
+    public function getResultList()
+    {
+        return ['0' => __('Result 0'), '1' => __('Result 1')];
+    }
+
+    public function getIsOpenList()
+    {
+        return ['0' => __('Is_open 0'), '1' => __('Is_open 1')];
+    }
+
+
+    public function getConditionTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['condition'] ?? '');
+        $list = $this->getConditionList();
+        return $list[$value] ?? '';
+    }
+
+
+    public function getResultTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['result'] ?? '');
+        $list = $this->getResultList();
+        return $list[$value] ?? '';
+    }
+
+
+    public function getIsOpenTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['is_open'] ?? '');
+        $list = $this->getIsOpenList();
+        return $list[$value] ?? '';
+    }
+
+    public function getUseTimesTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['use_times'] ?? '');
+        return preg_replace('/\s\d{2}\:\d{2}\:\d{2}/i', '', $value);
+    }
+
+    public function getResultDataAttr($value)
+    {
+        return (array)json_decode($value, true);
+    }
+
+    //购买计算:0=订单满xx打x折,1=订单满xx减x元
+    public function doBuy($money)
+    {
+        $obj = null;
+        //判断是那种计算方式
+        switch ((int)$this->getData('result')) {
+            case 0: //订单满xx打x折
+                $obj = new ToCalculate(new Discount());
+                break;
+            case 1: //订单满xx减x元
+                $obj = new ToCalculate(new FullReduction());
+                break;
+        }
+        if (!$obj) {
+            throw new \Exception('未找到优惠方式');
+        }
+        return $obj->doing($this->getData('result_data'), $money);
+    }
+
+    /**
+     * @ 获取优惠券
+     * @param $id
+     * @return array|bool|Model|\PDOStatement|string|null
+     */
+    public static function getCouponInfo($id)
+    {
+        if (!is_numeric($id)) {
+            $id = IntCode::decode($id);
+        }
+        $row = self::where('id', $id)->where('is_open', 1)->find();
+        if (!empty($row)) {
+            $row->id = IntCode::encode($row->id);
+            $rd = $row->result_data;
+            $row->result_tips = '订单满' . $rd['money'] . ($row->result ? '减' : '打') . $rd['number'] . ($row->result ? '元' : '折');
+            if ($row->mode == 'fixation') {
+                $row->expire_time = "领取后 {$row['use_times']} 天内有效";
+            } else {
+                $row->expire_time = "领取后有效时间为:" . $row['use_times_text'];
+            }
+        }
+        return $row;
+    }
+
+    /**
+     * 获取优惠券
+     * @param $coupon_id
+     * @return array|false|\PDOStatement|string|Model|$this
+     */
+    public function getCoupon($coupon_id)
+    {
+        return $this->where('id', $coupon_id)->where('is_open', 1)->find();
+    }
+
+    /**
+     * 检查优惠券
+     * @return $this
+     * @throws \Exception
+     */
+    public function checkCoupon()
+    {
+        if (empty($this->origin)) {
+            throw new \Exception('该优惠券不存在!');
+        }
+        return $this;
+    }
+
+    /**
+     * 是否开启
+     * @return $this
+     * @throws \Exception
+     */
+    public function checkOpen()
+    {
+        if ($this->getData('is_open') == 0) {
+            throw new \Exception('该优惠券已关闭!');
+        }
+        return $this;
+    }
+
+    /**
+     * 判断优惠券的剩余数量
+     * @return $this
+     * @throws \Exception
+     */
+    public function checkNumber()
+    {
+        if ($this->getData('give_num') <= $this->getData('received_num')) {
+            throw new \Exception('该优惠券已被领完!');
+        }
+        return $this;
+    }
+
+    /**
+     * 判断自己的剩余数量
+     * @param $user_id
+     * @return $this
+     * @throws \think\Exception
+     */
+    public function checkMyNumber($user_id)
+    {
+        $self_num = UserCoupon::where('user_id', $user_id)->where('coupon_id', $this->getData('id'))->count();
+        if ($this->getData('allow_num') <= $self_num) {
+            throw new \Exception('您没有可领取该优惠券的数量!');
+        }
+        return $this;
+    }
+
+    /**
+     * 校验优惠券领取的时间
+     * @return $this
+     * @throws \Exception
+     */
+    public function checkReceiveTime()
+    {
+        $time = time();
+        $receive_times = $this->getAttr('receive_times');
+        $range_time = explode(' - ', $receive_times);
+        if (count($range_time) != 2) {
+            throw new \Exception('该优惠券领取时间格式错误!', 1000);
+        }
+
+        if ($time < strtotime($range_time[0])) {
+            throw new \Exception('未到领取优惠券时间,请过后再来!', 1002);
+        }
+        if ($time > strtotime($range_time[1])) {
+            throw new \Exception('该优惠券已经失效!', 1000);
+        }
+        return $this;
+    }
+
+    /**
+     * 校验优惠券使用的时间
+     * @param $use_coupon_time
+     * @return $this
+     * @throws \Exception
+     */
+    public function checkUseTime($use_coupon_time)
+    {
+        $time = time();
+        $use_times = $this->getData('use_times');
+        if ($this->getData('mode') == 'fixation') { //固定日期
+
+            if (!empty($use_times) && is_numeric($use_times) && ($time - $use_coupon_time >= $use_times * 24 * 60 * 60)) {
+                throw new \Exception('该优惠券已经失效!', 1000);
+            }
+        } else { //时间范围
+
+            $range_time = explode(' - ', $use_times);
+            if (count($range_time) != 2) {
+                throw new \Exception('该优惠券使用时间格式错误!', 1000);
+            }
+
+            if ($time < strtotime($range_time[0])) {
+                throw new \Exception('未到使用优惠券时间,请换个优惠券试试!', 1001);
+            }
+
+            if ($time > strtotime($range_time[1])) {
+                throw new \Exception('该优惠券已经失效!', 1000);
+            }
+        }
+        return $this;
+    }
+
+    /**
+     * @ 获取生效和失效时间
+     * @return array
+     */
+    public function getUseTime()
+    {
+        $timeArr = [];
+        $time = time();
+        $use_times = $this->getData('use_times');
+        if ($this->getData('mode') == 'fixation') { //固定日期
+            $timeArr[0] = $time;
+            $timeArr[1] = \fast\Date::unixtime('day', $use_times, 'end');
+        } else { //时间范围
+            $range_time = explode(' - ', $use_times);
+            if (count($range_time) != 2) {
+                throw new \Exception('该优惠券使用时间格式错误!');
+            }
+            $timeArr[0] = strtotime($range_time[0]);
+            $timeArr[1] = strtotime($range_time[1]);
+        }
+        return $timeArr;
+    }
+
+    /**
+     * 优惠券是否已经领取
+     * @param $user_id
+     * @return $this
+     */
+    public function checkReceive($user_id)
+    {
+        if (empty($this->origin)) {
+            throw new \Exception('该优惠券不存在!');
+        }
+        $row = UserCoupon::where('coupon_id', $this->getData('id'))->where('user_id', $user_id)->find();
+        if ($row) {
+            throw new \Exception('您已领取该优惠券!');
+        }
+        return $this;
+    }
+
+    /**
+     * 判断条件:1指定商品
+     * @param       $goods_ids
+     * @param       $user_id
+     * @param array $category_ids
+     * @param array $brand_ids
+     * @return $this
+     * @throws \think\Exception
+     */
+    public function checkConditionGoods($goods_ids, $user_id, $category_ids = [], $brand_ids = [])
+    {
+        if (!empty($this->getData('condition_ids'))) {
+
+            $conditions = CouponCondition::where('id', 'IN', $this->getData('condition_ids'))->select();
+
+            foreach ($conditions as $item) {
+                switch ($item['type']) {
+                    case 1: //指定商品
+                        $ids = explode(',', $item['content']);
+                        $result = array_intersect($goods_ids, $ids);
+                        //要全等
+                        if (count($goods_ids) != count($result)) {
+                            throw new \Exception('该优惠券必须在指定的商品使用!');
+                        }
+                        break;
+                    case 2: //新用户专享
+                        $order = (new Order())->where('user_id', $user_id)->where('paytime', '>', 0)->count();
+                        if ($order) {
+                            throw new \Exception('该优惠券限定新用户专享!');
+                        }
+                        break;
+                    case 3: //老用户专享
+                        $order = (new Order())->where('user_id', $user_id)->where('paytime', '>', 0)->count();
+                        if (!$order) {
+                            throw new \Exception('该优惠券限定老用户专享!');
+                        }
+                        break;
+                    case 4: //指定分类
+                        $ids = explode(',', $item['content']);
+                        $result = array_intersect($category_ids, $ids);
+                        //要全等
+                        if (count($category_ids) != count($result)) {
+                            throw new \Exception('该优惠券必须在指定分类的商品使用!');
+                        }
+                        break;
+                    case 5: //指定品牌
+                        $ids = explode(',', $item['content']);
+                        $result = array_intersect($brand_ids, $ids);
+                        //要全等
+                        if (count($brand_ids) != count($result)) {
+                            throw new \Exception('该优惠券必须在指定品牌的商品使用!');
+                        }
+                        break;
+                }
+            }
+        }
+        return $this;
+    }
+
+    /**
+     * 渲染优惠券
+     * @param       $row
+     * @param array $coupon_ids
+     * @return mixed
+     */
+    public static function render(&$row, $coupon_ids = [])
+    {
+        //失效的优惠券状态
+        if (!empty($coupon_ids) && in_array($row['id'], $coupon_ids)) {
+            $num = 0;
+            foreach ($coupon_ids as $res) {
+                if ($res == $row['id']) {
+                    $num++;
+                }
+            }
+            $row['is_received'] = ($row['allow_num'] - $num) == 0;
+        } else {
+            $row['is_received'] = false;
+        }
+        $row->expired = false;
+        $row->online = true;
+        try {
+            $row->checkReceiveTime();
+        } catch (\Exception $e) {
+            if ($e->getCode() == 1000) {
+                $row->expired = true;
+            } elseif ($e->getCode() == 1002) {
+                $row->online = false;
+            };
+            $row->message = $e->getMessage();
+        }
+        $row->has_more = $row->received_num >= $row->give_num;
+        $row->id = IntCode::encode($row->id);
+        return $row;
+    }
+
+    /**
+     * 获取列表
+     * @param $param
+     * @return \think\Paginator
+     */
+    public static function tableList($param)
+    {
+
+        $pageNum = 15;
+        if (!empty($param['num'])) {
+            $pageNum = $param['num'];
+        }
+
+        return self::where(function ($query) use ($param) {
+
+            if (!empty($param['is_private'])) {
+                $query->where('is_private', $param['is_private']);
+            }
+
+            if (!empty($param['is_open'])) {
+                $query->where('is_open', $param['is_open']);
+            }
+
+            if (isset($param['result']) && $param['result'] != '') {
+                $query->where('result', $param['result']);
+            }
+        })->where('endtime', '>', time())->order('createtime desc')->paginate($pageNum);
+    }
+}

+ 89 - 0
application/common/model/CouponCondition.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+
+class CouponCondition extends Model
+{
+
+
+    // 表名
+    protected $name = 'shop_coupon_condition';
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+        'type_text'
+    ];
+
+
+
+    public function getTypeList()
+    {
+        return ['1' => __('Type 1'), '2' => __('Type 2'), '3' => __('Type 3'), '4' => __('Type 4')];
+    }
+
+
+    public function getTypeTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['type'] ?? '');
+        $list = $this->getTypeList();
+        return $list[$value] ?? '';
+    }
+
+    /**
+     * @ 获取指定商品的优惠券条件id
+     * @param $goods_ids
+     * @param $category_ids
+     * @param $brand_ids
+     * @param $type
+     * @return bool|\PDOStatement|string|\think\Collection
+     */
+    public static function getGoodsCondition($goods_ids, $category_ids = [], $brand_ids = [], $type = 2)
+    {
+        return self::where('type', $type)
+            ->whereOr(function ($query) use ($goods_ids) {
+                $sql = '1=2';
+                if (is_array($goods_ids)) {
+                    foreach ($goods_ids as $id) {
+                        $sql .= " OR FIND_IN_SET('{$id}',content)";
+                    }
+                } else {
+                    $sql .= " OR FIND_IN_SET('{$goods_ids}',content)";
+                }
+                $query->where('type', 1)->where($sql);
+            })
+            ->whereOr(function ($query) use ($category_ids) {
+                $sql = '1=2';
+                if (is_array($category_ids)) {
+                    foreach ($category_ids as $id) {
+                        $sql .= " OR FIND_IN_SET('{$id}',content)";
+                    }
+                } else {
+                    $sql .= " OR FIND_IN_SET('{$category_ids}',content)";
+                }
+                $query->where('type', 4)->where($sql);
+            })
+            ->whereOr(function ($query) use ($brand_ids) {
+                $sql = '1=2';
+                if (is_array($brand_ids)) {
+                    foreach ($brand_ids as $id) {
+                        $sql .= " OR FIND_IN_SET('{$id}',content)";
+                    }
+                } else {
+                    $sql .= " OR FIND_IN_SET('{$brand_ids}',content)";
+                }
+                $query->where('type', 5)->where($sql);
+            })
+            ->select();
+    }
+}

+ 32 - 0
application/common/model/ElectronicsOrder.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+
+class ElectronicsOrder extends Model
+{
+
+    // 表名
+    protected $name = 'shop_electronics_order';
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+
+    ];
+
+    public function Shipper()
+    {
+        return $this->hasOne('Shipper', 'id', 'shipper_id', [], 'LEFT');
+    }
+
+}

+ 126 - 0
application/common/model/Exchange.php

@@ -0,0 +1,126 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+
+class Exchange extends Model
+{
+
+
+    // 表名
+    protected $name = 'shop_exchange';
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    protected static $config = [];
+
+    // 追加属性
+    protected $append = [
+        'type_text',
+        'status_text',
+        'url'
+    ];
+
+    public function getUrlAttr($value, $data)
+    {
+        return $this->buildUrl($value, $data);
+    }
+
+    public function getFullurlAttr($value, $data)
+    {
+        return $this->buildUrl($value, $data, true);
+    }
+
+    private function buildUrl($value, $data, $domain = false)
+    {
+        $vars = [
+            ':id' => $data['id']
+        ];
+        $suffix = static::$config['moduleurlsuffix']['exchange'] ?? static::$config['urlsuffix'];
+        return addon_url('shop/exchange/show', $vars, $suffix, $domain);
+    }
+
+
+    protected static function init()
+    {
+        $config = get_addon_config('shop');
+        self::$config = $config;
+
+        self::afterInsert(function ($row) {
+            $pk = $row->getPk();
+            $row->getQuery()->where($pk, $row[$pk])->update(['weigh' => $row[$pk]]);
+        });
+    }
+
+
+    public function getTypeList()
+    {
+        return ['virtual' => __('Type virtual'), 'reality' => __('Type reality')];
+    }
+
+    public function getStatusList()
+    {
+        return ['normal' => __('Normal'), 'hidden' => __('Hidden')];
+    }
+
+
+    public function getTypeTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['type'] ?? '');
+        $list = $this->getTypeList();
+        return $list[$value] ?? '';
+    }
+
+
+    public function getStatusTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['status'] ?? '');
+        $list = $this->getStatusList();
+        return $list[$value] ?? '';
+    }
+
+    public function getImageAttr($value)
+    {
+        return cdnurl($value, true);
+    }
+
+    //获取列表
+    public static function tableList($param)
+    {
+        $pageNum = 15;
+        if (!empty($param['num'])) {
+            $pageNum = $param['num'];
+        }
+
+        $orderby = 'createtime';
+        $orderway = 'desc';
+
+        if (!empty($param['orderby']) && in_array($param['orderby'], ['createtime', 'score', 'sales', 'weigh'])) {
+            $orderby = $param['orderby'];
+        }
+        if (!empty($param['orderway']) && in_array($param['orderway'], ['asc', 'desc'])) {
+            $orderway = $param['orderway'];
+        }
+
+        return self::where(function ($query) use ($param) {
+
+            $query->where('stocks', '>', 0)->where('status', 'normal');
+
+            if (!empty($param['type'])) {
+                $query->where('type', $param['type']);
+            }
+
+            if (isset($param['keyword']) && $param['keyword'] != '') {
+                $query->where('title', 'like', '%' . $param['keyword'] . '%');
+            }
+        })->order("$orderby $orderway")->paginate($pageNum);
+    }
+}

+ 89 - 0
application/common/model/ExchangeOrder.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+
+class ExchangeOrder extends Model
+{
+
+    // 表名
+    protected $name = 'shop_exchange_order';
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+        'type_text',
+        'status_text'
+    ];
+
+    public function getTypeList()
+    {
+        return ['virtual' => __('Type virtual'), 'reality' => __('Type reality')];
+    }
+
+    public function getStatusList()
+    {
+        return ['created' => __('待兑换'), 'inprogress' => __('处理中'), 'rejected' => __('已拒绝'), 'delivered' => __('已发货'), 'completed' => __('已完成')];
+    }
+
+    public function getTypeTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['type'] ?? '');
+        $list = $this->getTypeList();
+        return $list[$value] ?? '';
+    }
+
+
+    public function getStatusTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['status'] ?? '');
+        $list = $this->getStatusList();
+        return $list[$value] ?? '';
+    }
+
+    //常见兑换订单
+    public static function createOrder($data)
+    {
+        $data['orderid'] = date("Ymdhis") . sprintf("%06d", $data['user_id']) . mt_rand(1000, 9999);
+        $data['ip'] = request()->ip();
+        $data['useragent'] = substr(request()->server('HTTP_USER_AGENT'), 0, 255);
+        return (new self)->save($data);
+    }
+
+
+    //获取列表
+    public static function tableList($param)
+    {
+        $pageNum = 10;
+        if (!empty($param['num'])) {
+            $pageNum = $param['num'];
+        }
+        return self::with(['Exchange' => function ($query) {
+            $query->field('id,title,image');
+        }])->field('id,status,nums,score,type,exchange_id,reason,expressname,expressno,createtime')->where(function ($query) use ($param) {
+            if (!empty($param['type'])) {
+                $query->where('type', $param['type']);
+            }
+            if (!empty($param['status'])) {
+                $query->where('status', $param['status']);
+            }
+            if (!empty($param['user_id'])) {
+                $query->where('user_id', $param['user_id']);
+            }
+        })->order('createtime desc')->paginate($pageNum);
+    }
+
+    public function Exchange()
+    {
+        return $this->hasOne('Exchange', 'id', 'exchange_id');
+    }
+}

+ 64 - 0
application/common/model/Freight.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+use app\common\model\FreightItems;
+
+/**
+ * 模型
+ */
+class Freight extends Model
+{
+
+    // 表名
+    protected $name = 'shop_freight';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    // 追加属性
+    protected $append = [];
+
+    public function getFirstNumAttr($value, $data)
+    {
+        return $data['first_num'] ?? $data['num'];
+    }
+
+    public function getFirstPriceAttr($value, $data)
+    {
+        return $data['first_price'] ?? $data['price'];
+    }
+
+    /**
+     * @ DateTime 2021-05-28
+     * @ 计算邮费
+     * @param $freight_id
+     * @param $area_id
+     * @param $nums
+     * @param $weight
+     * @param $amount
+     * @return int
+     */
+    public static function calculate($freight_id, $area_id, $nums, $weight, $amount)
+    {
+        //模板id
+        $freight = self::where('id', $freight_id)->where('switch', 1)->find();
+        $shippingfee = 0;
+        if (empty($freight)) {
+            return $shippingfee;
+        }
+        //当前模板 计费类型 ,1=计件 ,2=计重
+        $FreightItems = new FreightItems();
+        switch ($freight['type']) {
+            case 1:
+                $shippingfee = $FreightItems->numPostage($freight, $area_id, $nums, $amount);
+                break;
+            case 2:
+                $shippingfee = $FreightItems->weightPostage($freight, $area_id, $nums, $weight, $amount);
+                break;
+        }
+        return $shippingfee;
+    }
+}

+ 188 - 0
application/common/model/FreightItems.php

@@ -0,0 +1,188 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+/**
+ * 运费条件模型
+ */
+class FreightItems extends Model
+{
+
+    // 表名
+    protected $name = 'shop_freight_items';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    // 追加属性
+    protected $append = [];
+
+    /**
+     * @ DateTime 2021-05-31
+     * @ 计件费用
+     * @param Freight $freight
+     * @param int     $area_id
+     * @param int     $nums
+     * @param float   $amount
+     * @return float
+     */
+    public function numPostage($freight, $area_id, $nums, $amount)
+    {
+        //计件
+        $shippingfee = 0; //邮费
+        $is_free = false; //包邮的
+        $result = []; //计费的
+        $freightItems = $this->where('freight_id', $freight->id)->select();
+        if (empty($freightItems)) {
+            // 默认计件费用
+            //首件
+            $money = $freight['price'];
+            //续费
+            if ($freight['num'] < $nums && $freight['continue_num'] > 0 && $freight['continue_price'] > 0) {
+                $money = bcadd($money, bcmul(bcdiv($freight['continue_price'], $freight['continue_num'], 2), bcsub($nums, $freight['num'], 2), 2), 2);
+            }
+            return $money;
+        }
+        // 是否指定地区包邮 -> 是否有在包邮地区里 ->
+        foreach ($freightItems as $item) {
+            $postage_area_ids = explode(',', $item['postage_area_ids']); //包邮限制地区
+            $area_ids = explode(',', $item['area_ids']); //邮费限制地区
+            //首先判断是否满足包邮
+            if (in_array($item['type'], [1, 2])) {
+                $is_free = $this->getWeightNumsFree($item, $area_id, $postage_area_ids, $nums, $amount);
+                if ($is_free) {
+                    break;
+                }
+            }
+            //走到这里,则包邮未满足,追加邮费结果
+            if (in_array($area_id, $area_ids)) {
+                $result[] = $item;
+            }
+        }
+        //包邮
+        if ($is_free) {
+            return $shippingfee;
+        }
+
+        if (!$result) {
+            //如果未找到指定地区的运费模板,则使用主运费模板
+            $result = $freight;
+        }
+
+        //计算邮费
+        $monies = [];
+        foreach ($result as $res) {
+            //首件
+            $money = $res['first_price'];
+            //续费
+            if ($res['first_num'] < $nums && $res['continue_num'] > 0 && $res['continue_price'] > 0) {
+                $money = bcadd($money, bcmul(bcdiv($res['continue_price'], $res['continue_num'], 2), bcsub($nums, $res['first_num'], 2), 2), 2);
+            }
+            $monies[] = $money;
+        }
+        $config = get_addon_config('shop');
+        $shippingfee = isset($config['freightitemfee']) || $config['freightitemfee'] == 'max' ? max($monies) : min($monies);
+        return $shippingfee;
+    }
+
+    /**
+     * @ DateTime 2021-05-31
+     * @ 计重费用
+     * @param Freight $freight
+     * @param int     $area_id
+     * @param int     $nums
+     * @param float   $weight
+     * @param float   $amount
+     * @return float
+     */
+    public function weightPostage($freight, $area_id, $nums, $weight, $amount)
+    {
+        //计重
+        $shippingfee = 0; //邮费
+        $is_free = false; //包邮的
+        $result = []; //计费的
+        $totalWeight = bcmul($nums, $weight, 2); //总重量
+        $freightItems = $this->where('freight_id', $freight->id)->select();
+        if (empty($freightItems)) {
+            // 默认计件费用
+            //首重
+            $money = $freight['price'];
+            //续费
+            if ($freight['num'] < $totalWeight && $freight['continue_num'] > 0 && $freight['continue_price'] > 0) {
+                $money = bcadd($money, bcmul(bcdiv($freight['continue_price'], $freight['continue_num'], 2), bcsub($totalWeight, $freight['num'], 2), 2), 2);
+            }
+            return $money;
+        }
+        // 是否指定地区包邮 -> 是否有在包邮地区里 ->
+        foreach ($freightItems as $item) {
+            $postage_area_ids = explode(',', $item['postage_area_ids']); //包邮限制地区
+            $area_ids = explode(',', $item['area_ids']); //邮费限制地区
+            if (in_array($item['type'], [1, 2])) {
+                $is_free = $this->getWeightNumsFree($item, $area_id, $postage_area_ids, $nums, $amount);
+                if ($is_free) {
+                    break;
+                }
+            }
+            //走到这里,则包邮未满足,追加邮费结果
+            if (in_array($area_id, $area_ids)) {
+                $result[] = $item;
+            }
+        }
+        //包邮
+        if ($is_free) {
+            return $shippingfee;
+        }
+
+        if (!$result) {
+            //如果未找到指定地区的运费模板,则使用主运费模板
+            $result = [$freight];
+        }
+
+        //计算邮费
+        $monies = [];
+        foreach ($result as $res) {
+            //首重
+            $money = $res['first_price'];
+            //续费
+            if ($res['first_num'] < $totalWeight && $res['continue_num'] > 0 && $res['continue_price'] > 0) {
+                $money = bcadd($money, bcmul(bcdiv($res['continue_price'], $res['continue_num'], 2), bcsub($totalWeight, $res['first_num'], 2), 2), 2);
+            }
+            $monies[] = $money;
+        }
+        $config = get_addon_config('shop');
+        $shippingfee = isset($config['freightitemfee']) || $config['freightitemfee'] == 'max' ? max($monies) : min($monies);
+        return $shippingfee;
+    }
+
+    /**
+     * @ DateTime 2021-05-31
+     * @ 包邮判断
+     * @param $item
+     * @param $area_id
+     * @param $postage_area_ids
+     * @param $nums
+     * @param $amount
+     * @return boolean
+     */
+    protected function getWeightNumsFree($item, $area_id, $postage_area_ids, $nums, $amount)
+    {
+        //判断条件
+        if (empty($postage_area_ids)) {
+            //开启包邮,没有限制地区,则全部包邮
+            return true;
+        }
+        if ($item['type'] == 1 && in_array($area_id, $postage_area_ids) && $item['postage_num'] <= $nums) { //计件的开启包邮
+            //在包邮地区里
+            //满足包邮条件
+            return true;
+        } elseif ($item['type'] == 2 && in_array($area_id, $postage_area_ids) && $item['postage_price'] <= $amount) { //金额开启包邮
+            //在包邮地区里
+            //满足包邮条件
+            return true;
+        }
+        return false;
+    }
+}

+ 245 - 0
application/common/model/Goods.php

@@ -0,0 +1,245 @@
+<?php
+
+namespace app\common\model;
+
+use app\common\library\Service;
+use think\Cache;
+use think\Db;
+use think\Model;
+use traits\model\SoftDelete;
+
+/**
+ * 商品模型
+ */
+class Goods extends Model
+{
+
+    use SoftDelete;
+
+    // 表名
+    protected $name = 'shop_goods';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = 'deletetime';
+    // 追加属性
+    protected $append = [
+        'url'
+    ];
+    protected static $config = [];
+
+    protected static $tagCount = 0;
+
+    protected static function init()
+    {
+        $config = get_addon_config('shop');
+        self::$config = $config;
+    }
+
+    public function getUrlAttr($value, $data)
+    {
+        return $this->buildUrl($value, $data);
+    }
+
+    public function getFullurlAttr($value, $data)
+    {
+        return $this->buildUrl($value, $data, true);
+    }
+
+    private function buildUrl($value, $data, $domain = false)
+    {
+        $diyname = isset($data['diyname']) && $data['diyname'] ? $data['diyname'] : $data['id'];
+        $catename = isset($this->category) && $this->category ? $this->category->diyname : 'all';
+        $cateid = isset($this->category) && $this->category ? $this->category->id : 0;
+        $time = $data['publishtime'] ?? time();
+        $vars = [
+            ':id'       => $data['id'],
+            ':diyname'  => $diyname,
+            ':category' => $cateid,
+            ':catename' => $catename,
+            ':cateid'   => $cateid,
+            ':year'     => date("Y", $time),
+            ':month'    => date("m", $time),
+            ':day'      => date("d", $time),
+        ];
+        $suffix = static::$config['moduleurlsuffix']['goods'] ?? static::$config['urlsuffix'];
+        return addon_url('shop/goods/index', $vars, $suffix, $domain);
+    }
+
+    public function getImageAttr($value, $data)
+    {
+        $value = $value ?: self::$config['default_goods_img'];
+        return cdnurl($value, true);
+    }
+
+    public function getImagesAttr($value, $data)
+    {
+        $images = explode(',', $data['images'] ?? '');
+        foreach ($images as $index => &$image) {
+            $image && $image = cdnurl($image, true);
+        }
+        return array_filter($images);
+    }
+
+    public function setImagesAttr($value, $data)
+    {
+        return is_array($value) ? implode(',', $value) : $value;
+    }
+
+    public function getContentAttr($value, $data)
+    {
+        //组装卡片信息
+        return \addons\shop\library\Service::formatSourceTpl($value);
+    }
+
+    public static function getIndexGoodsList()
+    {
+        return self::where('status', 'normal')
+            ->where("FIND_IN_SET('recommend',`flag`)")
+            ->order('weigh desc')
+            ->limit(12)
+            ->cache(false)
+            ->select();
+    }
+
+    /**
+     * 获取SQL查询结果
+     */
+    public static function getQueryList($params)
+    {
+        $config = get_addon_config('shop');
+        $sql = $params['sql'] ?? '';
+        $bind = isset($params['bind']) ? explode(',', $params['bind']) : [];
+        list($cacheKey, $cacheExpire) = Service::getCacheKeyExpire('sql', $params);
+        $list = Cache::tag('shop')->get($cacheKey);
+        if (!$list) {
+            $list = Db::query($sql, $bind);
+            Cache::tag('shop')->set($cacheKey, $list, $cacheExpire);
+        }
+        return $list;
+    }
+
+    public static function refreshStar($goods_id)
+    {
+        $goods = self::get($goods_id);
+        if ($goods) {
+            $one = Comment::where('goods_id', $goods_id)->field("COUNT(*) as nums,SUM(star) as amount")->find();
+            if ($one) {
+                $goods->star = floor($one['amount'] / $one['nums']);
+                $goods->save();
+            }
+        }
+    }
+
+
+    /**
+     * 获取商品列表
+     * @param $params
+     * @return array|false|\PDOStatement|string|\think\Collection
+     */
+    public static function getGoodsList($params)
+    {
+        $config = get_addon_config('shop');
+        $type = empty($params['type']) ? '' : $params['type'];
+        $category = !isset($params['category']) ? '' : $params['category'];
+        $condition = empty($params['condition']) ? '' : $params['condition'];
+        $field = empty($params['field']) ? '*' : $params['field'];
+        $flag = empty($params['flag']) ? '' : $params['flag'];
+        $row = empty($params['row']) ? 10 : (int)$params['row'];
+        $orderby = empty($params['orderby']) ? 'createtime' : $params['orderby'];
+        $orderway = empty($params['orderway']) ? 'desc' : strtolower($params['orderway']);
+        $limit = empty($params['limit']) ? $row : $params['limit'];
+        $orderway = in_array($orderway, ['asc', 'desc']) ? $orderway : 'desc';
+        $paginate = !isset($params['paginate']) ? false : $params['paginate'];
+        $page = !isset($params['page']) ? 1 : (int)$params['page'];
+        $with = !isset($params['with']) ? 'category' : $params['with'];
+
+        list($cacheKey, $cacheExpire) = Service::getCacheKeyExpire('goodslist', $params);
+
+        $where = ['status' => 'normal'];
+
+        $where['deletetime'] = ['exp', Db::raw('IS NULL')];
+
+        self::$tagCount++;
+
+        $goodsModel = self::with($with)->alias('a');
+        if ($category !== '') {
+            if ($type === 'son') {
+                $subQuery = Category::where('parent_id', 'in', $category)->field('id')->buildSql();
+                //子级
+                $where['category_id'] = ['exp', Db::raw(' IN ' . '(' . $subQuery . ')')];
+            } elseif ($type === 'sons') {
+                //所有子级
+                $where['category_id'] = ['in', Category::getCategoryChildrenIds($category)];
+            } else {
+                $where['category_id'] = ['in', $category];
+            }
+        }
+        //如果有设置标志,则拆分标志信息并构造condition条件
+        if ($flag !== '') {
+            if (stripos($flag, '&') !== false) {
+                $arr = [];
+                foreach (explode('&', $flag) as $k => $v) {
+                    $arr[] = "FIND_IN_SET('{$v}', flag)";
+                }
+                if ($arr) {
+                    $condition .= "(" . implode(' AND ', $arr) . ")";
+                }
+            } else {
+                $condition .= ($condition ? ' AND ' : '');
+                $arr = [];
+                foreach (explode(',', str_replace('|', ',', $flag)) as $k => $v) {
+                    $arr[] = "FIND_IN_SET('{$v}', flag)";
+                }
+                if ($arr) {
+                    $condition .= "(" . implode(' OR ', $arr) . ")";
+                }
+            }
+        }
+
+        $order = $orderby == 'rand' ? Db::raw('rand()') : (preg_match("/\,|\s/", $orderby) ? $orderby : "{$orderby} {$orderway}");
+        $order = $orderby == 'weigh' ? $order . ',id DESC' : $order;
+
+        $modelInfo = null;
+        $prefix = config('database.prefix');
+        $goodsModel
+            ->where($where)
+            ->where($condition)
+            ->field($field, false, $prefix . "shop_goods", "a")
+            ->orderRaw($order);
+
+        if ($paginate) {
+            $paginateArr = explode(',', $paginate);
+            $listRows = is_numeric($paginate) ? $paginate : (is_numeric($paginateArr[0]) ? $paginateArr[0] : $row);
+            $config = [];
+            $config['var_page'] = $paginateArr[2] ?? (isset($params['page']) ? 'page' : 'apage' . self::$tagCount);
+            $config['path'] = $paginateArr[3] ?? '';
+            $config['fragment'] = $paginateArr[4] ?? '';
+            $config['query'] = request()->get();
+            $config['page'] = $page;
+            $list = $goodsModel->paginate($listRows, ($paginateArr[1] ?? false), $config);
+        } else {
+            $list = $goodsModel->limit($limit)->cache($cacheKey, $cacheExpire, 'shop')->select();
+        }
+
+        return $list;
+    }
+
+
+    public function Category()
+    {
+        return $this->belongsTo('Category', 'category_id', 'id');
+    }
+
+    public function Sku()
+    {
+        return $this->hasMany('Sku', 'goods_id', 'id');
+    }
+
+    public function Comment()
+    {
+        return $this->hasMany('Comment', 'goods_id', 'id');
+    }
+}

+ 65 - 0
application/common/model/GoodsAttr.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+use think\Db;
+
+class GoodsAttr extends Model
+{
+
+    // 表名
+    protected $name = 'shop_goods_attr';
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [];
+
+    //查询符合的goods_id
+    public static function getGoodsIds($attributes)
+    {
+        if (!is_array($attributes) && empty($attributes)) {
+            return [];
+        }
+        $attributes = array_values($attributes);
+        $attrSql = '';
+        $whereSql = '1=1';
+        foreach ($attributes as $key => $item) {
+            $sql = self::field('goods_id')->where('attribute_id', $item['attribute_id'])->where('value_id', 'IN', $item['value_id'])->fetchSql(true)->select();
+            if (!$key) {
+                $attrSql .= "({$sql}) AS A{$key}";
+            } else {
+                $attrSql .= " INNER JOIN ({$sql}) AS A{$key}";
+                $k = $key - 1;
+                $whereSql .= " AND A{$k}.goods_id = A{$key}.goods_id";
+            }
+        }
+        $sql = "SELECT * FROM {$attrSql} WHERE {$whereSql}";
+        $list = Db::query($sql);
+        return array_unique(array_column($list, 'goods_id'));
+
+        $valueIds = [];
+        foreach ($attributes as $key => $item) {
+            $valueIds = array_merge($valueIds, explode(',', $item['value_id']));
+        }
+        $goodsAttrList = GoodsAttr::field('goods_id')->where('value_id', 'in', $valueIds)->column('value_id,attribute_id,goods_id');
+        $goodsArr = [];
+        foreach ($goodsAttrList as $index => $attr) {
+            $goodsArr[$attr['goods_id']][$attr['attribute_id']] = $attr['value_id'];
+        }
+        $goodsIds = [];
+        foreach ($goodsArr as $index => $item) {
+            if (count($item) >= count($attributes)) {
+                $goodsIds[] = $index;
+            }
+        }
+        return array_unique($goodsIds);
+    }
+}

+ 55 - 0
application/common/model/Guarantee.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+
+class Guarantee extends Model
+{
+
+    // 表名
+    protected $name = 'shop_guarantee';
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+        'status_text'
+    ];
+
+    protected static $config = [];
+
+    protected static $tagCount = 0;
+
+    protected static function init()
+    {
+        $config = get_addon_config('shop');
+        self::$config = $config;
+    }
+
+    public function getStatusList()
+    {
+        return ['normal' => __('Normal'), 'hidden' => __('Hidden')];
+    }
+
+    public function getStatusTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['status'] ?? '');
+        $list = $this->getStatusList();
+        return $list[$value] ?? '';
+    }
+
+    public function getImageAttr($value, $data)
+    {
+        $value = $value ? $value : "/assets/addons/shop/img/checked.png";
+        return cdnurl($value, true);
+    }
+
+}

+ 187 - 0
application/common/model/Menu.php

@@ -0,0 +1,187 @@
+<?php
+
+namespace app\common\model;
+
+use app\common\library\Service;
+use fast\Tree;
+use think\Cache;
+use think\Db;
+use think\Model;
+
+/**
+ * 模型
+ */
+class Menu extends Model
+{
+
+    // 表名
+    protected $name = 'shop_menu';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = '';
+    // 追加属性
+    protected $append = [
+    ];
+
+    protected static $tagCount = 0;
+
+    protected static $parentIds = null;
+
+    protected static $outlinkParentIds = null;
+
+    protected static $navParentIds = null;
+
+    /**
+     * 获取分类列表
+     * @param $params
+     * @return false|\PDOStatement|string|\think\Collection
+     */
+    public static function getMenuList($params)
+    {
+        $config = get_addon_config('shop');
+        $type = empty($params['type']) ? '' : $params['type'];
+        $typeid = !isset($params['typeid']) ? '' : $params['typeid'];
+        $condition = empty($params['condition']) ? '' : $params['condition'];
+        $field = empty($params['field']) ? '*' : $params['field'];
+        $row = empty($params['row']) ? 10 : (int)$params['row'];
+        $flag = empty($params['flag']) ? '' : $params['flag'];
+        $orderby = empty($params['orderby']) ? 'weigh' : $params['orderby'];
+        $orderway = empty($params['orderway']) ? 'desc' : strtolower($params['orderway']);
+        $limit = empty($params['limit']) ? $row : $params['limit'];
+        $orderway = in_array($orderway, ['asc', 'desc']) ? $orderway : 'desc';
+        $paginate = !isset($params['paginate']) ? false : $params['paginate'];
+
+        list($cacheKey, $cacheExpire) = Service::getCacheKeyExpire('menulist', $params);
+
+        $where = ['status' => 'normal'];
+
+        self::$tagCount++;
+
+        if ($type === 'top') {
+            //顶级分类
+            $where['pid'] = 0;
+        } elseif ($type === 'brother') {
+            $subQuery = self::where('id', 'in', $typeid)->field('pid')->buildSql();
+            //同级
+            $where['pid'] = ['exp', Db::raw(' IN ' . '(' . $subQuery . ')')];
+        } elseif ($type === 'son') {
+            $subQuery = self::where('pid', 'in', $typeid)->field('id')->buildSql();
+            //子级
+            $where['id'] = ['exp', Db::raw(' IN ' . '(' . $subQuery . ')')];
+        } elseif ($type === 'sons') {
+            //所有子级
+            $where['id'] = ['in', self::getMenuChildrenIds($typeid)];
+        } else {
+            if ($typeid !== '') {
+                $where['id'] = ['in', $typeid];
+            }
+        }
+
+        //如果有设置标志,则拆分标志信息并构造condition条件
+        if ($flag !== '') {
+            if (stripos($flag, '&') !== false) {
+                $arr = [];
+                foreach (explode('&', $flag) as $k => $v) {
+                    $arr[] = "FIND_IN_SET('{$v}', flag)";
+                }
+                if ($arr) {
+                    $condition .= "(" . implode(' AND ', $arr) . ")";
+                }
+            } else {
+                $condition .= ($condition ? ' AND ' : '');
+                $arr = [];
+                foreach (explode(',', str_replace('|', ',', $flag)) as $k => $v) {
+                    $arr[] = "FIND_IN_SET('{$v}', flag)";
+                }
+                if ($arr) {
+                    $condition .= "(" . implode(' OR ', $arr) . ")";
+                }
+            }
+        }
+
+        $order = $orderby == 'rand' ? Db::raw('rand()') : (preg_match("/\,|\s/", $orderby) ? $orderby : "{$orderby} {$orderway}");
+        $order = $orderby == 'weigh' ? $order . ',id DESC' : $order;
+
+        $MenuModel = self::where($where)
+            ->where($condition)
+            ->field($field)
+            ->orderRaw($order);
+        if ($paginate) {
+            $paginateArr = explode(',', $paginate);
+            $listRows = is_numeric($paginate) ? $paginate : (is_numeric($paginateArr[0]) ? $paginateArr[0] : $row);
+            $config = [];
+            $config['var_page'] = $paginateArr[2] ?? 'mpage' . self::$tagCount;
+            $config['path'] = $paginateArr[3] ?? '';
+            $config['fragment'] = $paginateArr[4] ?? '';
+            $config['query'] = request()->get();
+            $list = $MenuModel->paginate($listRows, ($paginateArr[1] ?? false), $config);
+        } else {
+            $list = $MenuModel->limit($limit)->cache($cacheKey, $cacheExpire, 'shop')->select();
+        }
+
+        return $list;
+    }
+
+    /**
+     * 获取菜单列表HTML
+     * @param array $params
+     * @return mixed|string
+     */
+    public static function getMenu($params = [])
+    {
+        $config = get_addon_config('shop');
+        $condition = empty($params['condition']) ? '' : $params['condition'];
+        $maxLevel = !isset($params['maxlevel']) ? 0 : $params['maxlevel'];
+
+        list($cacheKey, $cacheExpire) = Service::getCacheKeyExpire('menu', $params);
+
+        $menuList = Menu::where($condition)
+            ->where('status', 'normal')
+            ->field('id,pid,name,url')
+            ->order('weigh desc,id desc')
+            ->cache($cacheKey, $cacheExpire, 'shop')
+            ->select();
+        $tree = \fast\Tree::instance();
+        $tree->init(collection($menuList)->toArray(), 'pid');
+        $result = self::getTreeUl($tree, 0, '', '', 1, $maxLevel);
+
+        return $result;
+    }
+
+    public static function getTreeUl($tree, $myid, $selectedids = '', $disabledids = '', $level = 1, $maxlevel = 0)
+    {
+        $str = '';
+        $childs = $tree->getChild($myid);
+        if ($childs) {
+            foreach ($childs as $value) {
+                $id = $value['id'];
+                unset($value['child']);
+                $selected = $selectedids && in_array($id, (is_array($selectedids) ? $selectedids : explode(',', $selectedids))) ? 'active' : '';
+                $disabled = $disabledids && in_array($id, (is_array($disabledids) ? $disabledids : explode(',', $disabledids))) ? 'disabled' : '';
+                if (!$selected && request()->url(substr($value['url'], 0, 1) !== '/' ? true : null) == $value['url']) {
+                    $selected = 'active';
+                }
+                $value = array_merge($value, array('selected' => $selected, 'disabled' => $disabled));
+                $value = array_combine(array_map(function ($k) {
+                    return '@' . $k;
+                }, array_keys($value)), $value);
+                $itemtpl = '<li class="@dropdown @selected" value=@id @disabled><a data-toggle="@toggle" data-target="#" href="@url">@name@caret</a> @childlist</li>';
+                $nstr = strtr($itemtpl, $value);
+                $childlist = '';
+                if (!$maxlevel || $level < $maxlevel) {
+                    $childdata = self::getTreeUl($tree, $id, $selectedids, $disabledids, $level + 1, $maxlevel);
+                    $childlist = $childdata ? '<ul class="dropdown-menu" role="menu">' . $childdata . '</ul>' : "";
+                }
+                $str .= strtr($nstr, [
+                    '@childlist' => $childlist,
+                    '@caret'     => $childlist ? ($level == 1 ? '<span class="indicator"><i class="fa fa-angle-down"></i></span>' : '<span class="indicator"><i class="fa fa-angle-right"></i></span>') : '',
+                    '@dropdown'  => $childlist ? ($level == 1 ? 'dropdown' : 'dropdown-submenu') : '',
+                    '@toggle'    => $childlist ? 'dropdown' : ''
+                ]);
+            }
+        }
+        return $str;
+    }
+}

+ 2 - 1
application/common/model/MoneyLog.php

@@ -5,7 +5,7 @@ namespace app\common\model;
 use think\Model;
 use think\Model;
 
 
 /**
 /**
- * 会员余额日志模型
+ * 模型
  */
  */
 class MoneyLog extends Model
 class MoneyLog extends Model
 {
 {
@@ -20,4 +20,5 @@ class MoneyLog extends Model
     // 追加属性
     // 追加属性
     protected $append = [
     protected $append = [
     ];
     ];
+
 }
 }

+ 41 - 0
application/common/model/Navigation.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+
+class Navigation extends Model
+{
+
+    // 表名
+    protected $name = 'shop_navigation';
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+
+    ];
+
+    public function getImageAttr($value)
+    {
+        if (strpos($value, '/') === false) {
+            return $value;
+        } else {
+            return cdnurl($value, true);
+        }
+    }
+
+    public static function tableList()
+    {
+        return self::where('switch', 1)->order('weigh desc')->select();
+    }
+
+}

+ 615 - 0
application/common/model/Order.php

@@ -0,0 +1,615 @@
+<?php
+
+namespace app\common\model;
+
+use think\Db;
+use think\Exception;
+use think\Model;
+use addons\shop\model\Freight;
+use addons\shop\model\Carts;
+use addons\shop\model\Address;
+use Yansongda\Pay\Exceptions\GatewayException;
+use addons\epay\library\Service;
+use addons\shop\model\OrderAction;
+use addons\shop\model\TemplateMsg;
+use traits\model\SoftDelete;
+
+/**
+ * 模型
+ */
+class Order extends Model
+{
+    use SoftDelete;
+
+    // 表名
+    protected $name = 'shop_order';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = 'deletetime';
+    // 追加属性
+    protected $append = [
+        'url'
+    ];
+    protected static $config = [];
+
+    protected static $tagCount = 0;
+
+    protected static function init()
+    {
+        $config = get_addon_config('shop');
+        self::$config = $config;
+    }
+
+    public function getUrlAttr($value, $data)
+    {
+        return url('shop.order/detail', ['orderid' => $data['order_sn']]);
+    }
+
+    public function getPayurlAttr($value, $data)
+    {
+        return addon_url('shop/payment/index') . '?orderid=' . $data['order_sn'];
+    }
+
+    public function getCommenturlAttr($value, $data)
+    {
+        return url('shop.comment/post') . '?orderid=' . $data['order_sn'];
+    }
+
+    /**
+     * 获取快递查询URL
+     */
+    public function getLogisticsurlAttr($value, $data)
+    {
+        $url = self::$config['logisticstype'] == 'kdnapi' ? url('shop.order/logistics') . '?orderid=' . $data['order_sn'] : "https://www.kuaidi100.com/chaxun?com={$data['expressname']}&nu={$data['expressno']}";
+        return $url;
+    }
+
+    public function getOrderstateList()
+    {
+        return ['0' => __('Orderstate 0'), '1' => __('Orderstate 1'), '2' => __('Orderstate 2'), '3' => __('Orderstate 3'), '4' => __('Orderstate 4'), '5' => __('Orderstate 5')];
+    }
+
+    public function getShippingstateList()
+    {
+        return ['0' => __('Shippingstate 0'), '1' => __('Shippingstate 1'), '2' => __('Shippingstate 2'), '3' => __('Shippingstate 3')];
+    }
+
+    public function getPaystateList()
+    {
+        return ['0' => __('Paystate 0'), '1' => __('Paystate 1')];
+    }
+
+    public function getOrderstateTextAttr($value, $data)
+    {
+        $value = $value ? $value : $data['orderstate'];
+        $list = $this->getOrderstateList();
+        return $list[$value] ?? '';
+    }
+
+    public function getShippingstateTextAttr($value, $data)
+    {
+        $value = $value ? $value : $data['shippingstate'];
+        $list = $this->getShippingstateList();
+        return $list[$value] ?? '';
+    }
+
+    public function getPaystateTextAttr($value, $data)
+    {
+        $value = $value ? $value : $data['paystate'];
+        $list = $this->getPaystateList();
+        return $list[$value] ?? '';
+    }
+
+    public function getStatusTextAttr($value, $data)
+    {
+        if ($data['orderstate'] == 0) {
+            if ($data['paystate'] == 0) {
+                return '待付款';
+            }
+            if ($data['shippingstate'] == 0) {
+                return '待发货';
+            } elseif ($data['shippingstate'] == 1) {
+                return '待收货';
+            } elseif ($data['shippingstate'] == 2) {
+                return '待评价';
+            }
+        } elseif ($data['orderstate'] == 1) {
+            return '已取消';
+        } elseif ($data['orderstate'] == 2) {
+            return '已失效';
+        } elseif ($data['orderstate'] == 3) {
+            return '已完成';
+        } elseif ($data['orderstate'] == 4) {
+            return '退货/退款中';
+        }
+        return '未知';
+    }
+
+    //获取订单剩余有效时长
+    public function getRemainsecondsAttr($value, $data)
+    {
+        return max(0, $data['expiretime'] - time());
+    }
+
+    //计算购物车商品
+    public static function computeCarts(&$orderInfo, $cart_ids, $user_id, $area_id, $user_coupon_id = '')
+    {
+        $config = get_addon_config('shop');
+        $goodsList = Carts::getGoodsList($cart_ids, $user_id);
+        if (empty($goodsList)) {
+            throw new \Exception("未找到商品");
+        }
+        $orderInfo['amount'] = 0;
+        $orderInfo['goodsprice'] = 0;
+        $orderInfo['shippingfee'] = 0;
+        $orderInfo['discount'] = 0;
+        $orderItem = [];
+        $shippingTemp = [];
+        $userCoupon = null;
+        //校验领取和是否可使用
+        if ($user_coupon_id) {
+            $userCouponModel = new UserCoupon();
+            $userCoupon = $userCouponModel->checkUserOrUse($user_coupon_id, $user_id);
+            $orderInfo['user_coupon_id'] = $user_coupon_id;
+        }
+        //判断商品库存和状态
+        foreach ($goodsList as $item) {
+            $goodsItem = [];
+            if (empty($item->goods) && empty($item->sku)) {
+                throw new \Exception("商品已下架");
+            }
+            //规格
+            if ($item->goods_sku_id && empty($item->sku)) {
+                throw new \Exception("商品规格不存在");
+            }
+            if (!empty($item->sku)) { //规格计算
+                if ($item->sku->stocks < $item->nums) {
+                    throw new \Exception("有商品库存不足,请返回购物车重新修改");
+                }
+                $goodsItem['image'] = !empty($item->sku->image) ? $item->sku->image : $item->goods->image;
+                $goodsItem['price'] = $item->sku->price;
+                $goodsItem['marketprice'] = $item->sku->marketprice;
+                $goodsItem['goods_sn'] = $item->sku->goods_sn;
+                $amount = bcmul($item->sku->price, $item->nums, 2);
+            } else { //商品默认计算
+                if ($item->goods->stocks < $item->nums) {
+                    throw new \Exception("有商品库存不足,请返回购物车重新修改");
+                }
+                $goodsItem['image'] = !empty($item->sku->image) ? $item->sku->image : $item->goods->image;
+                $goodsItem['price'] = $item->goods->price;
+                $goodsItem['marketprice'] = $item->goods->marketprice;
+                $goodsItem['goods_sn'] = $item->goods->goods_sn;
+                $amount = bcmul($item->goods->price, $item->nums, 2);
+            }
+            $goodsItem['amount'] = $amount;
+            //订单总价
+            $orderInfo['amount'] = bcadd($orderInfo['amount'], $amount, 2);
+            //商品总价
+            $orderInfo['goodsprice'] = bcadd($orderInfo['goodsprice'], $amount, 2);
+
+            $freight_id = $item->goods->freight_id;
+            //计算邮费【合并运费模板】
+            if (!isset($shippingTemp[$freight_id])) {
+                $shippingTemp[$freight_id] = [
+                    'nums'   => $item->nums,
+                    'weight' => $item->goods->weight,
+                    'amount' => $amount
+                ];
+            } else {
+                $shippingTemp[$freight_id] = [
+                    'nums'   => bcadd($shippingTemp[$freight_id]['nums'], $item->nums, 2),
+                    'weight' => bcadd($shippingTemp[$freight_id]['weight'], $item->goods->weight, 2),
+                    'amount' => bcadd($shippingTemp[$freight_id]['amount'], $amount, 2)
+                ];
+            }
+            //创建订单商品数据
+            $orderItem[] = array_merge($goodsItem, [
+                'order_sn'     => $orderInfo['order_sn'],
+                'goods_id'     => $item->goods_id,
+                'title'        => $item->goods->title,
+                'url'          => $item->goods->url,
+                'nums'         => $item->nums,
+                'goods_sku_id' => $item->goods_sku_id,
+                'attrdata'     => $item->sku_attr,
+                'weight'       => $item->goods->weight,
+                'category_id'  => $item->goods->category_id,
+                'brand_id'     => $item->goods->brand_id,
+            ]);
+        }
+        //按运费模板计算
+        foreach ($shippingTemp as $key => $item) {
+            $shippingfee = Freight::calculate($key, $area_id, $item['nums'], $item['weight'], $item['amount']);
+            $orderInfo['shippingfee'] = bcadd($orderInfo['shippingfee'], $shippingfee, 2);
+        }
+
+        //订单总价(含邮费)
+        $orderInfo['amount'] = bcadd($orderInfo['goodsprice'], $orderInfo['shippingfee'], 2);
+
+        if (!empty($userCoupon)) {
+            //校验优惠券
+            $goods_ids = array_column($orderItem, 'goods_id');
+            $category_ids = array_column($orderItem, 'category_id');
+            $brand_ids = array_column($orderItem, 'brand_id');
+            $couponModel = new Coupon();
+            $coupon = $couponModel->getCoupon($userCoupon['coupon_id'])
+                ->checkCoupon()
+                ->checkOpen()
+                ->checkUseTime($userCoupon['createtime'])
+                ->checkConditionGoods($goods_ids, $user_id, $category_ids, $brand_ids);
+
+            //计算折扣金额,判断是使用不含运费,还是含运费的金额
+            $amount = !isset($config['shippingfeecoupon']) || $config['shippingfeecoupon'] == 0 ? $orderInfo['goodsprice'] : $orderInfo['amount'];
+            list($new_money, $coupon_money) = $coupon->doBuy($amount);
+
+            //判断优惠金额是否超出总价,超出则直接设定优惠金额为总价
+            $orderInfo['discount'] = $coupon_money > $amount ? $amount : $coupon_money;
+        }
+
+        //计算订单的应付金额【减去折扣】
+        $orderInfo['saleamount'] = max(0, bcsub($orderInfo['amount'], $orderInfo['discount'], 2));
+        $orderInfo['discount'] = bcadd($orderInfo['discount'], 0, 2);
+
+        return [
+            $orderItem,
+            $goodsList,
+            $userCoupon
+        ];
+    }
+
+    /**
+     * @ DateTime 2021-05-28
+     * @ 创建订单
+     * @param int    $address_id
+     * @param int    $user_id
+     * @param mixed  $cart_ids
+     * @param string $memo
+     * @return Order|null
+     */
+    public static function createOrder($address_id, $user_id, $cart_ids, $user_coupon_id, $memo)
+    {
+        $address = Address::get($address_id);
+        if (!$address || $address['user_id'] != $user_id) {
+            throw new \Exception("地址未找到");
+        }
+        $config = get_addon_config('shop');
+        $order_sn = date("Ymdhis") . sprintf("%08d", $user_id) . mt_rand(1000, 9999);
+        //订单主表
+        $orderInfo = [
+            'user_id'     => $user_id,
+            'order_sn'    => $order_sn,
+            'address_id'  => $address->id,
+            'province_id' => $address->province_id,
+            'city_id'     => $address->city_id,
+            'area_id'     => $address->area_id,
+            'receiver'    => $address->receiver,
+            'mobile'      => $address->mobile,
+            'address'     => $address->address,
+            'zipcode'     => $address->zipcode,
+            'goodsprice'  => 0, //商品金额 (不含运费)
+            'amount'      => 0, //总金额 (含运费)
+            'shippingfee' => 0, //运费
+            'discount'    => 0, //优惠金额
+            'saleamount'  => 0,
+            'memo'        => $memo,
+            'expiretime'  => time() + $config['order_timeout'], //订单失效
+            'status'      => 'normal'
+        ];
+
+        //订单详细表
+        list($orderItem, $goodsList, $userCoupon) = self::computeCarts($orderInfo, $cart_ids, $user_id, $address->area_id, $user_coupon_id);
+        $order = null;
+        Db::startTrans();
+        try {
+            //创建订单
+            $order = Order::create($orderInfo, true);
+            //减库存
+            foreach ($goodsList as $index => $item) {
+                if ($item->sku) {
+                    $item->sku->setDec('stocks', $item->nums);
+                }
+                $item->goods->setDec("stocks", $item->nums);
+            }
+            //计算单个商品折扣后的价格
+            $saleamount = bcsub($order['saleamount'], $order['shippingfee'], 2);
+            $saleratio = $order['goodsprice'] > 0 ? bcdiv($saleamount, $order['goodsprice'], 10) : 1;
+            $saleremains = $saleamount;
+            foreach ($orderItem as $index => &$item) {
+                if (!isset($orderItem[$index + 1])) {
+                    $saleprice = $saleremains;
+                } else {
+                    $saleprice = $order['discount'] == 0 ? bcmul($item['price'], $item['nums'], 2) : bcmul(bcmul($item['price'], $item['nums'], 2), $saleratio, 2);
+                }
+                $saleremains = bcsub($saleremains, $saleprice, 2);
+                $item['realprice'] = $saleprice;
+            }
+            unset($item);
+            //创建订单商品数据
+            foreach ($orderItem as $index => $item) {
+                OrderGoods::create($item, true);
+            }
+            //修改地址使用次数
+            $address->setInc('usednums');
+            //优惠券已使用
+            if (!empty($userCoupon)) {
+                $userCoupon->save(['is_used' => 2]);
+            }
+            //提交事务
+            Db::commit();
+        } catch (Exception $e) {
+            Db::rollback();
+            throw new Exception($e->getMessage());
+        }
+        //清空购物车
+        Carts::clear($cart_ids);
+        //记录操作
+        OrderAction::push($order_sn, '系统', '订单创建成功');
+        //订单应付金额为0时直接结算
+        if ($order['saleamount'] == 0) {
+            self::settle($order->order_sn, 0);
+            $order = Order::get($order->id);
+        }
+        return $order;
+    }
+
+    /**
+     * @ DateTime 2021-05-31
+     * @ 订单信息
+     * @param $order_sn
+     * @param $user_id
+     * @return array|false|\PDOStatement|string|Model
+     */
+    public static function getDetail($order_sn, $user_id)
+    {
+        return self::with(['orderGoods'])->where('order_sn', $order_sn)->where('user_id', $user_id)->find();
+    }
+
+    /**
+     * 判断订单是否失效
+     * @param $order_sn
+     * @return bool
+     */
+    public static function isExpired($order_sn)
+    {
+        $orderInfo = Order::getByOrderSn($order_sn);
+        //订单过期
+        if (!$orderInfo['orderstate'] && !$orderInfo['paystate'] && time() > $orderInfo['expiretime']) {
+            // 启动事务
+            Db::startTrans();
+            try {
+                $orderInfo->save(['orderstate' => 2]);
+                //库存恢复
+                OrderGoods::setGoodsStocksInc($orderInfo->order_sn);
+                //恢复优惠券
+                UserCoupon::resetUserCoupon($orderInfo->user_coupon_id, $orderInfo->order_sn);
+                // 提交事务
+                Db::commit();
+            } catch (\Exception $e) {
+                // 回滚事务
+                Db::rollback();
+            }
+            return true;
+        }
+        return false;
+    }
+
+
+    /**
+     * @ 支付
+     * @param string $orderid
+     * @param int    $user_id
+     * @param string $paytype
+     * @param string $method
+     * @param string $openid
+     * @param string $notifyurl
+     * @param string $returnurl
+     * @return \addons\epay\library\Collection|\addons\epay\library\RedirectResponse|\addons\epay\library\Response|null
+     * @throws \Exception
+     */
+    public static function pay($orderid, $user_id, $paytype, $method = 'web', $openid = '', $notifyurl = null, $returnurl = null)
+    {
+        $request = \think\Request::instance();
+        $order = self::getDetail($orderid, $user_id);
+        if (!$order) {
+            throw new \Exception('订单不存在!');
+        }
+        if ($order->paystate) {
+            throw new \Exception('订单已支付!');
+        }
+        if ($order->orderstate) {
+            throw new \Exception('订单已失效!');
+        }
+        //支付金额为0,无需支付
+        if ($order->saleamount == 0) {
+            throw new \Exception('无需支付!');
+        }
+        $order_sn = $order->order_sn;
+        // 启动事务
+        Db::startTrans();
+        try {
+            //支付方式变更
+            if (($order['paytype'] == $paytype && $order['method'] != $method)) {
+                $order_sn = date("Ymdhis") . sprintf("%08d", $user_id) . mt_rand(1000, 9999);
+                //更新电子面单
+                $electronics = $order->order_electronics;
+                foreach ($electronics as $aftersales) {
+                    $aftersales->order_sn = $order_sn;
+                    $aftersales->save();
+                }
+                //更新操作日志
+                $orderAction = $order->order_action;
+                foreach ($orderAction as $action) {
+                    $action->order_sn = $order_sn;
+                    $action->save();
+                }
+                $order->save(['order_sn' => $order_sn]);
+                //更新订单明细
+                foreach ($order->order_goods as $item) {
+                    $item->order_sn = $order_sn;
+                    $item->save();
+                }
+            }
+            //更新支付类型和方法
+            $order->allowField(true)->save(['paytype' => $paytype, 'method' => $method, 'openid' => $openid]);
+            //提交事务
+            Db::commit();
+        } catch (\Exception $e) {
+            // 回滚事务
+            Db::rollback();
+            throw new \Exception($e->getMessage());
+        }
+        $response = null;
+        $epay = get_addon_info('epay');
+
+        if ($epay && $epay['state']) {
+            $notifyurl = $notifyurl ? $notifyurl : $request->root(true) . '/addons/shop/order/epay/type/notify/paytype/' . $paytype;
+            $returnurl = $returnurl ? $returnurl : $request->root(true) . '/addons/shop/order/epay/type/return/paytype/' . $paytype . '/order_sn/' . $order_sn;
+
+            //保证取出的金额一致,不一致将导致订单重复错误
+            $amount = sprintf("%.2f", $order->saleamount);
+
+            $params = [
+                'amount'    => $amount,
+                'orderid'   => $order_sn,
+                'type'      => $paytype,
+                'title'     => "支付{$amount}元",
+                'notifyurl' => $notifyurl,
+                'returnurl' => $returnurl,
+                'method'    => $method,
+                'openid'    => $openid
+            ];
+            try {
+                $response = Service::submitOrder($params);
+            } catch (GatewayException $e) {
+                throw new \Exception(config('app_debug') ? $e->getMessage() : "支付失败,请稍后重试");
+            }
+        } else {
+            throw new \Exception("请在后台安装配置微信支付宝整合插件");
+        }
+        return $response;
+    }
+
+
+    /**
+     * 订单列表
+     *
+     * @param $param
+     * @return \think\Paginator
+     */
+    public static function tableList($param)
+    {
+        $pageNum = 15;
+        if (!empty($param['num'])) {
+            $pageNum = $param['num'];
+        }
+        return self::with(['orderGoods'])
+            ->where(function ($query) use ($param) {
+                $query->where('status', 'normal');
+
+                if (!empty($param['user_id'])) {
+                    $query->where('user_id', $param['user_id']);
+                }
+
+                if (isset($param['orderstate']) && $param['orderstate'] != '') {
+                    $query->where('orderstate', $param['orderstate']);
+                }
+                if (isset($param['shippingstate']) && $param['shippingstate'] != '') {
+                    $query->where('shippingstate', $param['shippingstate']);
+                }
+                if (isset($param['paystate']) && $param['paystate'] != '') {
+                    $query->where('paystate', $param['paystate']);
+                }
+                if (isset($param['q']) && $param['q'] != '') {
+                    $query->where('order_sn', 'in', function ($query) use ($param) {
+                        return $query->name('shop_order_goods')->where('order_sn|title', 'like', '%' . $param['q'] . '%')->field('order_sn');
+                    });
+                }
+            })
+            ->order('createtime desc')->paginate($pageNum, false, ['query' => request()->get()]);
+    }
+
+
+    /**
+     * @ DateTime 2021-06-01
+     * @ 订单结算
+     * @param string $order_sn      订单号
+     * @param float  $payamount     支付金额
+     * @param string $transactionid 流水号
+     * @return bool
+     */
+    public static function settle($order_sn, $payamount, $transactionid = '')
+    {
+        $order = self::with(['orderGoods'])->where('order_sn', $order_sn)->find();
+        if (!$order || $order->paystate == 1) {
+            return false;
+        }
+
+        if ($payamount != $order->saleamount) {
+            \think\Log::write("[shop][pay][{$order_sn}][订单支付金额不一致]");
+            return false;
+        }
+
+        // 启动事务
+        Db::startTrans();
+        try {
+            $order->paystate = 1;
+            $order->transactionid = $transactionid;
+            $order->payamount = $payamount;
+            $order->paytime = time();
+            $order->paytype = !$order->paytype ? 'system' : $order->paytype;
+            $order->method = !$order->method ? 'system' : $order->method;
+            $order->save();
+            if ($order->payamount == $order->saleamount) {
+                //支付完成后,商品销量+1
+                foreach ($order->order_goods as $item) {
+                    $goods = $item->goods;
+                    $sku = $item->sku;
+                    if ($goods) {
+                        $goods->setInc('sales', $item->nums);
+                    }
+                    if ($sku) {
+                        $sku->setInc('sales', $item->nums);
+                    }
+                }
+            }
+            // 提交事务
+            Db::commit();
+        } catch (\Exception $e) {
+            // 回滚事务
+            Db::rollback();
+            return false;
+        }
+        //记录操作
+        OrderAction::push($order_sn, '系统', '订单支付成功');
+        //发送通知
+        TemplateMsg::sendTempMsg(0, $order->order_sn);
+        return true;
+    }
+
+    public function user()
+    {
+        return $this->belongsTo('User', 'user_id', 'id', [], 'LEFT')->setEagerlyType(0);
+    }
+
+    public function address()
+    {
+        return $this->belongsTo('Address', 'address_id', 'id', [], 'LEFT')->setEagerlyType(0);
+    }
+
+    public function orderGoods()
+    {
+        return $this->hasMany('OrderGoods', 'order_sn', 'order_sn');
+    }
+
+    public function orderElectronics()
+    {
+        return $this->hasMany('OrderElectronics', 'order_sn', 'order_sn');
+    }
+
+    public function orderAction()
+    {
+        return $this->hasMany('OrderAction', 'order_sn', 'order_sn');
+    }
+}

+ 33 - 0
application/common/model/OrderAction.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+/**
+ * 模型
+ */
+class OrderAction extends Model
+{
+
+    // 表名
+    protected $name = 'shop_order_action';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = '';
+    // 追加属性
+    protected $append = [];
+
+
+    public static function push($order_sn, $operator, $memo)
+    {
+        self::create([
+            'order_sn' => $order_sn,
+            'operator' => $operator,
+            'memo' => $memo
+        ]);
+        return true;
+    }
+}

+ 67 - 0
application/common/model/OrderAftersales.php

@@ -0,0 +1,67 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+/**
+ * 模型
+ */
+class OrderAftersales extends Model
+{
+
+    // 表名
+    protected $name = 'shop_order_aftersales';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    // 追加属性
+    protected $append = [
+        'status_text',
+        'type_text'
+    ];
+
+    public function getStatusList()
+    {
+        return ['1' => '等待审核', '2' => '审核通过', '3' => '审核拒绝'];
+    }
+
+    public function getStatusTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['status'] ?? '');
+        $list = $this->getStatusList();
+        return $list[$value] ?? '';
+    }
+
+    public function getTypeList()
+    {
+        return ['1' => '仅退款', '2' => '退货退款'];
+    }
+
+    public function getTypeTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['type'] ?? '');
+        $list = $this->getTypeList();
+        return $list[$value] ?? '';
+    }
+
+    public function getImagesAttr($value, $data)
+    {
+        $value = $value ?: ($data['images'] ?? '');
+        if (empty($value)) {
+            return [];
+        }
+        $value = explode(',', $value);
+        foreach ($value as &$img) {
+            $img = cdnurl($img, true);
+        }
+        return $value;
+    }
+
+    public function OrderGoods()
+    {
+        return $this->hasOne('OrderGoods', 'id', 'order_goods_id', [], 'LEFT');
+    }
+}

+ 25 - 0
application/common/model/OrderElectronics.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+
+class OrderElectronics extends Model
+{
+
+    // 表名
+    protected $name = 'shop_order_electronics';
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [];
+
+}

+ 171 - 0
application/common/model/OrderGoods.php

@@ -0,0 +1,171 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+use think\Db;
+
+/**
+ * 模型
+ */
+class OrderGoods extends Model
+{
+
+    // 表名
+    protected $name = 'shop_order_goods';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = '';
+    // 追加属性
+    protected $append = [
+        'url'
+    ];
+    protected static $config = [];
+
+    protected static $tagCount = 0;
+
+    protected static function init()
+    {
+        $config = get_addon_config('shop');
+        self::$config = $config;
+    }
+
+    public function getUrlAttr($value, $data)
+    {
+        $suffix = static::$config['moduleurlsuffix']['goods'] ?? static::$config['urlsuffix'];
+        return $this->goods ? $this->goods->url : addon_url('shop/goods/index', ['id' => $data['goods_id']], $suffix);
+    }
+
+    public function getFullurlAttr($value, $data)
+    {
+        $suffix = static::$config['moduleurlsuffix']['goods'] ?? static::$config['urlsuffix'];
+        return $this->goods ? $this->goods->fullurl : addon_url('shop/goods/index', ['id' => $data['goods_id']], $suffix, true);
+    }
+
+    public function getImageAttr($value, $data)
+    {
+        $value = $value ?: ($data['image'] ?? '/assets/addons/shop/img/noimage.jpg');
+        return cdnurl($value, true);
+    }
+
+    //销量增
+    public static function setGoodsSalesInc($order_sn)
+    {
+        $list = (new self)->where('order_sn', $order_sn)->select();
+        // 启动事务
+        Db::startTrans();
+        try {
+            foreach ($list as $item) {
+                $goods = $item->goods;
+                $sku = $item->sku;
+                if ($goods) {
+                    $goods->setInc('sales', $item->nums);
+                }
+                if ($sku) {
+                    $sku->setInc('sales', $item->nums);
+                }
+            }
+            // 提交事务
+            Db::commit();
+        } catch (\Exception $e) {
+            // 回滚事务
+            Db::rollback();
+        }
+        return true;
+    }
+
+    //销量减
+    public static function setGoodsSalesDec($order_sn)
+    {
+        $list = (new self)->where('order_sn', $order_sn)->select();
+        // 启动事务
+        Db::startTrans();
+        try {
+            foreach ($list as $item) {
+                $goods = $item->goods;
+                $sku = $item->sku;
+                if ($goods) {
+                    $goods->setDec('sales', $item->nums);
+                }
+                if ($sku) {
+                    $sku->setDec('sales', $item->nums);
+                }
+            }
+            // 提交事务
+            Db::commit();
+        } catch (\Exception $e) {
+            // 回滚事务
+            Db::rollback();
+        }
+        return true;
+    }
+
+    //库存增
+    public static function setGoodsStocksInc($order_sn)
+    {
+        $list = (new self)->where('order_sn', $order_sn)->select();
+        // 启动事务
+        Db::startTrans();
+        try {
+            foreach ($list as $item) {
+                $goods = $item->goods;
+                $sku = $item->sku;
+                if ($sku) {
+                    $sku->setInc('stocks', $item->nums);
+                }
+                if ($goods) {
+                    $goods->setInc('stocks', $item->nums);
+                }
+            }
+            // 提交事务
+            Db::commit();
+        } catch (\Exception $e) {
+            // 回滚事务
+            Db::rollback();
+        }
+        return true;
+    }
+
+    //库存减
+    public static function setGoodsStocksDec($order_sn)
+    {
+        $list = (new self)->where('order_sn', $order_sn)->select();
+        // 启动事务
+        Db::startTrans();
+        try {
+            foreach ($list as $item) {
+                $goods = $item->goods;
+                $sku = $item->sku;
+                if ($sku) {
+                    $sku->setDec('stocks', $item->nums);
+                }
+                if ($goods) {
+                    $goods->setDec('stocks', $item->nums);
+                }
+            }
+            // 提交事务
+            Db::commit();
+        } catch (\Exception $e) {
+            // 回滚事务
+            Db::rollback();
+        }
+        return true;
+    }
+
+    public function goods()
+    {
+        return $this->belongsTo('Goods', 'goods_id', 'id', [], 'LEFT')->setEagerlyType(1);
+    }
+
+    public function Sku()
+    {
+        return $this->belongsTo('Sku', 'goods_sku_id', 'id', [], 'LEFT');
+    }
+
+    public function Order()
+    {
+        return $this->hasOne('Order', 'order_sn', 'order_sn', [], 'LEFT');
+    }
+}

+ 23 - 0
application/common/model/OrderRegion.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+/**
+ * 模型
+ */
+class Region extends Model
+{
+
+    // 表名
+    protected $name = 'shop_region';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = '';
+    // 追加属性
+    protected $append = [
+    ];
+}

+ 199 - 0
application/common/model/Page.php

@@ -0,0 +1,199 @@
+<?php
+
+namespace app\common\model;
+
+use app\common\library\Service;
+use fast\Tree;
+use think\Db;
+use think\Model;
+use think\View;
+use traits\model\SoftDelete;
+
+/**
+ * 模型
+ */
+class Page extends Model
+{
+
+    use SoftDelete;
+
+    // 表名
+    protected $name = 'shop_page';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = 'deletetime';
+    // 追加属性
+    protected $append = [
+        'url'
+    ];
+    protected static $config = [];
+
+    protected static $tagCount = 0;
+
+    protected static function init()
+    {
+        $config = get_addon_config('shop');
+        self::$config = $config;
+    }
+
+    public function getImageAttr($value, $data)
+    {
+        $value = $value ? $value : self::$config['default_page_img'];
+        return cdnurl($value, true);
+    }
+
+    public function getUrlAttr($value, $data)
+    {
+        return $this->buildUrl($value, $data);
+    }
+
+    public function getFullurlAttr($value, $data)
+    {
+        return $this->buildUrl($value, $data, true);
+    }
+
+    private function buildUrl($value, $data, $domain = false)
+    {
+        $diyname = isset($data['diyname']) && $data['diyname'] ? $data['diyname'] : $data['id'];
+        $time = $data['createtime'] ?? time();
+
+        $vars = [
+            ':id'      => $data['id'],
+            ':diyname' => $diyname,
+            ':year'    => date("Y", $time),
+            ':month'   => date("m", $time),
+            ':day'     => date("d", $time)
+        ];
+        $suffix = static::$config['moduleurlsuffix']['page'] ?? static::$config['urlsuffix'];
+        return addon_url('shop/page/index', $vars, $suffix, $domain);
+    }
+
+    public function getContentAttr($value, $data)
+    {
+        if (isset($data['parsetpl']) && $data['parsetpl']) {
+            $view = View::instance();
+            $view->engine->layout(false);
+            return $view->display($data['content']);
+        }
+        //组装卡片信息
+        return \addons\shop\library\Service::formatSourceTpl($data['content']);
+    }
+
+    public function getHasimageAttr($value, $data)
+    {
+        return $this->getData("image") ? true : false;
+    }
+
+    public function getLikeratioAttr($value, $data)
+    {
+        return bcmul(($data['dislikes'] > 0 ? min(1, $data['likes'] / ($data['dislikes'] + $data['likes'])) : ($data['likes'] ? 1 : 0.5)), 100);
+    }
+
+    /**
+     * 获取单页列表
+     * @param $params
+     * @return false|\PDOStatement|string|\think\Collection
+     */
+    public static function getPageList($params)
+    {
+        $type = empty($params['type']) ? '' : $params['type'];
+        $condition = empty($params['condition']) ? '' : $params['condition'];
+        $field = empty($params['field']) ? '*' : $params['field'];
+        $row = empty($params['row']) ? 10 : (int)$params['row'];
+        $orderby = empty($params['orderby']) ? 'createtime' : $params['orderby'];
+        $orderway = empty($params['orderway']) ? 'desc' : strtolower($params['orderway']);
+        $limit = empty($params['limit']) ? $row : $params['limit'];
+        $imgwidth = empty($params['imgwidth']) ? '' : $params['imgwidth'];
+        $imgheight = empty($params['imgheight']) ? '' : $params['imgheight'];
+        $orderway = in_array($orderway, ['asc', 'desc']) ? $orderway : 'desc';
+        $paginate = !isset($params['paginate']) ? false : $params['paginate'];
+
+        list($cacheKey, $cacheExpire) = Service::getCacheKeyExpire('pagelist', $params);
+
+        self::$tagCount++;
+
+        $where = ['status' => 'normal'];
+        if ($type !== '') {
+            $where['type'] = $type;
+        }
+        $order = $orderby == 'rand' ? Db::raw('rand()') : (preg_match("/\,|\s/", $orderby) ? $orderby : "{$orderby} {$orderway}");
+
+        $pageModel = self::where($where)
+            ->where($condition)
+            ->field($field)
+            ->orderRaw($order);
+
+        if ($paginate) {
+            $paginateArr = explode(',', $paginate);
+            $listRows = is_numeric($paginate) ? $paginate : (is_numeric($paginateArr[0]) ? $paginateArr[0] : $row);
+            $config = [];
+            $config['var_page'] = $paginateArr[2] ?? 'ppage' . self::$tagCount;
+            $config['path'] = $paginateArr[3] ?? '';
+            $config['fragment'] = $paginateArr[4] ?? '';
+            $config['query'] = request()->get();
+            $config['type'] = '\\addons\\shop\\library\\Bootstrap';
+            $list = $pageModel->paginate($listRows, ($paginateArr[1] ?? false), $config);
+        } else {
+            $list = $pageModel->limit($limit)->cache($cacheKey, $cacheExpire, 'shop')->select();
+        }
+
+        self::render($list, $imgwidth, $imgheight);
+        return $list;
+    }
+
+    public static function getPageInfo($params)
+    {
+        $config = get_addon_config('shop');
+        $sid = empty($params['sid']) ? '' : $params['sid'];
+        $condition = empty($params['condition']) ? '' : $params['condition'];
+        $field = empty($params['field']) ? '*' : $params['field'];
+        $row = empty($params['row']) ? 10 : (int)$params['row'];
+        $orderby = empty($params['orderby']) ? 'weigh' : $params['orderby'];
+        $orderway = empty($params['orderway']) ? 'desc' : strtolower($params['orderway']);
+        $limit = empty($params['limit']) ? $row : $params['limit'];
+        $imgwidth = empty($params['imgwidth']) ? '' : $params['imgwidth'];
+        $imgheight = empty($params['imgheight']) ? '' : $params['imgheight'];
+        $orderway = in_array($orderway, ['asc', 'desc']) ? $orderway : 'desc';
+
+        list($cacheKey, $cacheExpire) = Service::getCacheKeyExpire('pageinfo', $params);
+
+        $where = [];
+
+        if ($sid !== '') {
+            $where['id'] = $sid;
+        }
+        $order = $orderby == 'rand' ? Db::raw('rand()') : (preg_match("/\,|\s/", $orderby) ? $orderby : "{$orderby} {$orderway}");
+        $order = $orderby == 'weigh' ? $order . ',id DESC' : $order;
+
+        $data = self::where($where)
+            ->where($condition)
+            ->field($field)
+            ->order($order)
+            ->limit($limit)
+            ->cache($cacheKey, $cacheExpire, 'shop')
+            ->find();
+        if ($data) {
+            $list = [$data];
+            self::render($list, $imgwidth, $imgheight);
+            return reset($list);
+        } else {
+            return false;
+        }
+    }
+
+    public static function render(&$list, $imgwidth, $imgheight)
+    {
+        $width = $imgwidth ? 'width="' . $imgwidth . '"' : '';
+        $height = $imgheight ? 'height="' . $imgheight . '"' : '';
+        foreach ($list as $k => &$v) {
+            $v['textlink'] = '<a href="' . $v['url'] . '">' . $v['title'] . '</a>';
+            $v['imglink'] = '<a href="' . $v['url'] . '"><img src="' . $v['image'] . '" border="" ' . $width . ' ' . $height . ' /></a>';
+            $v['img'] = '<img src="' . $v['image'] . '" border="" ' . $width . ' ' . $height . ' />';
+        }
+        return $list;
+    }
+
+}

+ 73 - 0
application/common/model/SearchLog.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace app\common\model;
+
+use app\common\library\Service;
+use think\Db;
+use think\Model;
+
+/**
+ * 搜索记录
+ */
+class SearchLog extends Model
+{
+
+    // 表名
+    protected $name = 'shop_search_log';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = '';
+    // 追加属性
+    protected $append = [
+    ];
+    protected static $tagCount = 0;
+
+    /**
+     * 获取标签列表
+     * @param $tag
+     * @return false|\PDOStatement|string|\think\Collection
+     */
+    public static function getSearchlogList($tag)
+    {
+        $config = get_addon_config('shop');
+        $condition = empty($tag['condition']) ? '' : $tag['condition'];
+        $field = empty($tag['field']) ? '*' : $tag['field'];
+        $row = empty($tag['row']) ? 10 : (int)$tag['row'];
+        $orderby = empty($tag['orderby']) ? 'nums' : $tag['orderby'];
+        $orderway = empty($tag['orderway']) ? 'desc' : strtolower($tag['orderway']);
+        $limit = empty($tag['limit']) ? $row : $tag['limit'];
+        $orderway = in_array($orderway, ['asc', 'desc']) ? $orderway : 'desc';
+        $paginate = !isset($tag['paginate']) ? false : $tag['paginate'];
+
+        list($cacheKey, $cacheExpire) = Service::getCacheKeyExpire('searchloglist', $tag);
+
+        self::$tagCount++;
+
+        $where = [];
+
+        $order = $orderby == 'rand' ? Db::raw('rand()') : (preg_match("/\,|\s/", $orderby) ? $orderby : "{$orderby} {$orderway}");
+
+        $tagModel = self::where($where)
+            ->where('status', 'normal')
+            ->where($condition)
+            ->field($field)
+            ->orderRaw($order);
+
+        if ($paginate) {
+            $paginateArr = explode(',', $paginate);
+            $listRows = is_numeric($paginate) ? $paginate : (is_numeric($paginateArr[0]) ? $paginateArr[0] : $row);
+            $config = [];
+            $config['var_page'] = $paginateArr[2] ?? 'tpage' . self::$tagCount;
+            $config['path'] = $paginateArr[3] ?? '';
+            $config['fragment'] = $paginateArr[4] ?? '';
+            $config['query'] = request()->get();
+            $list = $tagModel->paginate($listRows, ($paginateArr[1] ?? false), $config);
+        } else {
+            $list = $tagModel->limit($limit)->cache($cacheKey, $cacheExpire, 'shop')->select();
+        }
+
+        return $list;
+    }
+}

+ 29 - 0
application/common/model/Shipper.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+
+class Shipper extends Model
+{
+
+
+    // 表名
+    protected $name = 'shop_shipper';
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+
+    ];
+
+
+}

+ 48 - 0
application/common/model/Sku.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+/**
+ * 模型
+ */
+class Sku extends Model
+{
+
+    // 表名
+    protected $name = 'shop_goods_sku';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    // 追加属性
+    protected $append = [
+    ];
+
+    protected static $config = [];
+
+    protected static $tagCount = 0;
+
+    protected static function init()
+    {
+        $config = get_addon_config('shop');
+        self::$config = $config;
+    }
+
+    public function getImageAttr($value, $data)
+    {
+        if (!$value) {
+            return $value;
+        }
+        $value = $value ? $value : '';
+        return cdnurl($value, true);
+    }
+
+    public function goods()
+    {
+        return $this->belongsTo("Goods", "goods_id", "id", [], 'LEFT');
+    }
+
+}

+ 56 - 0
application/common/model/SkuSpec.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+/**
+ * 模型
+ */
+class SkuSpec extends Model
+{
+
+    // 表名
+    protected $name = 'shop_goods_sku_spec';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    // 追加属性
+    protected $append = [];
+
+    /**
+     * 获取指定商品的SKU信息
+     * @param int $goods_id 商品ID
+     * @return array
+     */
+    public static function getGoodsSkuSpec($goods_id)
+    {
+        $list = (new self)->field('MIN(`id`) AS `id`, MIN(`goods_id`) AS `goods_id`, `spec_id`')->where('goods_id', $goods_id)
+            ->with([
+                'Spec',
+                'SkuValue' => function ($query) use ($goods_id) {
+                    $query->where('goods_id', $goods_id)->field('id,goods_id,spec_id,spec_value_id')->with(['SpecValue']);
+                }
+            ])->group('spec_id')->select();
+
+        $list = collection($list)->toArray();
+        return $list;
+    }
+
+    public function SkuValue()
+    {
+        return $this->hasMany('SkuSpec', 'spec_id', 'spec_id');
+    }
+
+    public function Spec()
+    {
+        return $this->hasOne('Spec', 'id', 'spec_id', [], 'LEFT')->bind(['title' => 'name']);
+    }
+
+    public function SpecValue()
+    {
+        return $this->hasOne('SpecValue', 'id', 'spec_value_id', [], 'LEFT')->bind(['title' => 'value']);
+    }
+}

+ 24 - 0
application/common/model/Spec.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+/**
+ * 规格模型
+ */
+class Spec extends Model
+{
+
+    // 表名
+    protected $name = 'shop_spec';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    // 追加属性
+    protected $append = [
+    ];
+
+}

+ 24 - 0
application/common/model/SpecValue.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+/**
+ * 规格数据模型
+ */
+class SpecValue extends Model
+{
+
+    // 表名
+    protected $name = 'shop_spec_value';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    // 追加属性
+    protected $append = [
+    ];
+
+}

+ 25 - 0
application/common/model/SubscribeLog.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+/**
+ * 模型
+ */
+class SubscribeLog extends Model
+{
+
+    // 表名
+    protected $name = 'shop_subscribe_log';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = false;
+    protected $updateTime = false;
+    // 追加属性
+    protected $append = [
+    ];
+
+   
+}

+ 193 - 0
application/common/model/TemplateMsg.php

@@ -0,0 +1,193 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+use app\common\model\Order;
+use app\common\model\SubscribeLog;
+use app\common\library\message\Service;
+use think\Queue;
+
+class TemplateMsg extends Model
+{
+
+    // 表名
+    protected $name = 'shop_template_msg';
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [];
+
+    public static function getTplIds()
+    {
+        return self::where('switch', 1)->where('type', 2)->column('tpl_id');
+    }
+
+    //获取发送模板消息的数据【付款成功】【商城发货通知】【退款通知】【售后回复】
+    public static function sendTempMsg($event, $order_sn)
+    {
+        try {
+            $config = get_addon_config('shop');
+            if ($config['sendnoticemode'] == 'queue') {
+                if (extension_loaded('redis') && class_exists('\think\Queue') && config('queue.connector') == 'redis') {
+                    //使用队列发送
+                    Queue::push('addons\shop\controller\queue\Subscribe', ['event' => $event, 'order_sn' => $order_sn], 'shopSubscribeQueue');
+                }
+            } elseif ($config['sendnoticemode'] == 'async') {
+                //异步并发发送
+                self::getSendOrderData($event, $order_sn);
+            }
+        } catch (\Exception $e) {
+        }
+        return true;
+    }
+
+    /**
+     * @ 获取数据发送
+     * @param $event
+     * @param $order_sn
+     * @return bool
+     */
+    public static function getSendOrderData($event, $order_sn)
+    {
+        try {
+            //type 1=公众号,2=小程序,3=邮箱,4=短信
+            $temps = self::where('event', $event)->where('switch', 1)->order('id asc')->column('*', 'type');
+            //找订单
+            $order = Order::field('o.*,u.mobile,u.email,u.nickname')
+                ->alias('o')
+                ->join('user u', 'u.id=o.user_id')
+                ->where('order_sn', $order_sn)
+                ->find();
+
+            self::toSend($order, $temps);
+        } catch (\Exception $e) {
+            if (config('app_debug')) {
+                \think\Log::write("Line:" . $e->getLine() . " Code:" . $e->getCode() . " Message:" . $e->getMessage() . " File:" . $e->getFile());
+            }
+            return false;
+        }
+        return true;
+    }
+
+    //去发送
+    protected static function toSend($order, $temps)
+    {
+        $result = false;
+        foreach ($temps as $tp) {
+            switch ($tp['type']) {
+                case 1:
+                    //是否有openID
+                    if (!empty($order['openid'])) {
+                        $result = self::assembleMpData($order, $tp);
+                    }
+                    break;
+                case 2:
+                    //是否有openID
+                    if (!empty($order['openid'])) {
+                        //是否订阅有
+                        $subscribe = SubscribeLog::where('order_sn', $order['order_sn'])->where('tpl_id', $tp['tpl_id'])->where('status', 0)->find();
+                        if (!empty($subscribe)) {
+                            $result = self::assembleMiniData($order, $tp);
+                            $subscribe->status = 1;
+                            $subscribe->save();
+                        }
+                    }
+                    break;
+                case 3:
+                    if (!empty($order['email'])) {
+                        $result = self::assembleMEData($order, $tp);
+                    }
+                    break;
+                case 4:
+                    if (!empty($order['mobile'])) {
+                        $result = self::assembleMEData($order, $tp);
+                    }
+                    break;
+            }
+            \think\Log::record($result);
+            $result && Service::send($tp['type'], $result);
+        }
+    }
+
+    //组装公众号模板数据
+    protected static function assembleMpData($param, $temp)
+    {
+        $data = self::prepareData($param, $temp);
+        $templateData = [
+            'touser'      => $param['openid'],
+            'template_id' => $temp['tpl_id'],
+            'data'        => $data
+        ];
+        if (strpos($temp['page'], 'http') !== false) {
+            $templateData['url'] = $temp['page'];
+        } else {
+            $config = get_addon_config('shop');
+            $templateData['miniprogram'] = [
+                "appid"    => $config['wx_appid'],
+                "pagepath" => $temp['page']
+            ];
+        }
+        return $templateData;
+    }
+
+    //组装小程序模板数据
+    protected static function assembleMiniData($param, $temp)
+    {
+        $data = self::prepareData($param, $temp);
+        return [
+            'touser'      => $param['openid'],
+            'template_id' => $temp['tpl_id'],
+            'page'        => $temp['page'],
+            'data'        => $data
+        ];
+    }
+
+    //组装邮箱,短信模板数据
+    protected static function assembleMEData($param, $temp)
+    {
+        $data = self::prepareData($param, $temp);
+
+        $msg = $temp['extend'];
+        //替换内容中的变量 ${变量名}
+        $msg = preg_replace_callback('/\$\{(.*?)\}/i', function ($matches) use ($data) {
+            return $data[$matches[1]] ?? '';
+        }, $msg);
+        return [
+            'template_id' => $temp['tpl_id'],
+            'mobile'      => $param['mobile'],
+            'email'       => $param['email'],
+            'nickname'    => $param['nickname'],
+            'title'       => $temp['title'],
+            'message'     => $msg,
+            'data'        => $data,
+        ];
+    }
+
+    //准备数据
+    protected static function prepareData($param, $temp)
+    {
+        $msg = $temp['extend'];
+        $temp['content'] = is_array($temp['content']) ? $temp['content'] : (array)json_decode($temp['content'], true);
+        $data = [];
+        foreach ($temp['content'] as $res) {
+            $value = $res['value'];
+            $value = str_replace('.DATA}}', '', str_replace('{{', '', $value));
+            if ($value) {
+                $data[$value] = $res['key'] != 'diy_text' && isset($param[$res['key']]) ? $param[$res['key']] : $res['def_val'];
+                //如果为时间字段且为数字则做转换
+                if (is_numeric($data[$value]) && $data[$value] && preg_match("/([a-z]+)time\$/", $res['key'])) {
+                    $data[$value] = date("Y-m-d H:i:s", $data[$value]);
+                }
+            }
+        }
+        return $data;
+    }
+}

+ 6 - 87
application/common/model/User.php

@@ -2,15 +2,10 @@
 
 
 namespace app\common\model;
 namespace app\common\model;
 
 
-use think\Db;
 use think\Model;
 use think\Model;
 
 
 /**
 /**
  * 会员模型
  * 会员模型
- * @method static mixed getByUsername($str) 通过用户名查询用户
- * @method static mixed getByNickname($str) 通过昵称查询用户
- * @method static mixed getByMobile($str) 通过手机查询用户
- * @method static mixed getByEmail($str) 通过邮箱查询用户
  */
  */
 class User extends Model
 class User extends Model
 {
 {
@@ -25,6 +20,11 @@ class User extends Model
         'url',
         'url',
     ];
     ];
 
 
+    public static function init()
+    {
+        parent::init();
+    }
+
     /**
     /**
      * 获取个人URL
      * 获取个人URL
      * @param string $value
      * @param string $value
@@ -44,21 +44,9 @@ class User extends Model
      */
      */
     public function getAvatarAttr($value, $data)
     public function getAvatarAttr($value, $data)
     {
     {
-        if (!$value) {
-            //如果不需要启用首字母头像,请使用
-            //$value = '/assets/img/avatar.png';
-            $value = letter_avatar($data['nickname']);
-        }
-        return $value;
+        return $value ? $value : '/assets/img/avatar.png';
     }
     }
 
 
-    /**
-     * 获取会员的组别
-     */
-    public function getGroupAttr($value, $data)
-    {
-        return UserGroup::get($data['group_id']);
-    }
 
 
     /**
     /**
      * 获取验证字段数组值
      * 获取验证字段数组值
@@ -83,73 +71,4 @@ class User extends Model
         $value = is_object($value) || is_array($value) ? json_encode($value) : $value;
         $value = is_object($value) || is_array($value) ? json_encode($value) : $value;
         return $value;
         return $value;
     }
     }
-
-    /**
-     * 变更会员余额
-     * @param int    $money   余额
-     * @param int    $user_id 会员ID
-     * @param string $memo    备注
-     */
-    public static function money($money, $user_id, $memo)
-    {
-        Db::startTrans();
-        try {
-            $user = self::lock(true)->find($user_id);
-            if ($user && $money != 0) {
-                $before = $user->money;
-                //$after = $user->money + $money;
-                $after = function_exists('bcadd') ? bcadd($user->money, $money, 2) : $user->money + $money;
-                //更新会员信息
-                $user->save(['money' => $after]);
-                //写入日志
-                MoneyLog::create(['user_id' => $user_id, 'money' => $money, 'before' => $before, 'after' => $after, 'memo' => $memo]);
-            }
-            Db::commit();
-        } catch (\Exception $e) {
-            Db::rollback();
-        }
-    }
-
-    /**
-     * 变更会员积分
-     * @param int    $score   积分
-     * @param int    $user_id 会员ID
-     * @param string $memo    备注
-     */
-    public static function score($score, $user_id, $memo)
-    {
-        Db::startTrans();
-        try {
-            $user = self::lock(true)->find($user_id);
-            if ($user && $score != 0) {
-                $before = $user->score;
-                $after = $user->score + $score;
-                $level = self::nextlevel($after);
-                //更新会员信息
-                $user->save(['score' => $after, 'level' => $level]);
-                //写入日志
-                ScoreLog::create(['user_id' => $user_id, 'score' => $score, 'before' => $before, 'after' => $after, 'memo' => $memo]);
-            }
-            Db::commit();
-        } catch (\Exception $e) {
-            Db::rollback();
-        }
-    }
-
-    /**
-     * 根据积分获取等级
-     * @param int $score 积分
-     * @return int
-     */
-    public static function nextlevel($score = 0)
-    {
-        $lv = array(1 => 0, 2 => 30, 3 => 100, 4 => 500, 5 => 1000, 6 => 2000, 7 => 3000, 8 => 5000, 9 => 8000, 10 => 10000);
-        $level = 1;
-        foreach ($lv as $key => $value) {
-            if ($score >= $value) {
-                $level = $key;
-            }
-        }
-        return $level;
-    }
 }
 }

+ 141 - 0
application/common/model/UserCoupon.php

@@ -0,0 +1,141 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+use app\common\library\IntCode;
+use app\common\model\OrderAction;
+
+class UserCoupon extends Model
+{
+
+    // 表名
+    protected $name = 'shop_user_coupon';
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+        'is_used_text'
+    ];
+
+
+    public function getIsUsedList()
+    {
+        return ['1' => __('Is_used 1'), '2' => __('Is_used 2')];
+    }
+
+
+    public function getIsUsedTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['is_used'] ?? '');
+        $list = $this->getIsUsedList();
+        return $list[$value] ?? '';
+    }
+
+    /**
+     * 检查优惠券的归属用户和是否已使用
+     *
+     * @param [type] $user_coupon_id
+     * @param [type] $user_id
+     * @return Object
+     */
+    public function checkUserOrUse($user_coupon_id, $user_id)
+    {
+        $row = $this->where('id', $user_coupon_id)->where('user_id', $user_id)->find();
+        if (!$row) {
+            throw new \Exception('未领取该优惠券或不可用!');
+        }
+        if ($row['is_used'] == 2) {
+            throw new \Exception('该优惠券已使用!');
+        }
+        return $row;
+    }
+
+    //我的优惠券列表
+    public static function tableList($param)
+    {
+        $pageNum = 10;
+        if (!empty($param['num'])) {
+            $pageNum = $param['num'];
+        }
+        return self::field('id,coupon_id,user_id,is_used,expire_time,begin_time,createtime')->with(['Coupon' => function ($query) {
+            $query->field('id,name,result,result_data,allow_num,begintime,endtime,mode,use_times');
+        }])->where(function ($query) use ($param) {
+
+            $time = time();
+            if (!empty($param['is_used'])) {
+                $query->where('is_used', $param['is_used'])->where('begin_time', '<', $time);
+            }
+
+            if (isset($param['user_id']) && $param['user_id'] != '') {
+                $query->where('user_id', $param['user_id']);
+            }
+            if (!empty($param['begin_time'])) {
+                $query->where('begin_time', '>', $time);
+            }
+            if (!empty($param['expire_time'])) {
+                $query->where('expire_time', '<', $time);
+            }
+        })->order('createtime desc')->paginate($pageNum);
+    }
+
+
+    //我的可以使用的优惠券【过滤掉条件不符合的】
+    public static function myGoodsCoupon($user_id, $goods_ids, $category_ids, $brand_ids)
+    {
+        $order = (new Order())->where('user_id', $user_id)->where('paytime', '>', 0)->count();
+        if ($order) {
+            $type = 3; //老用户
+        } else {
+            $type = 2; //新用户
+        }
+        $conditions = CouponCondition::getGoodsCondition($goods_ids, $category_ids, $brand_ids, $type);
+        $sql = "condition_ids IS NULL OR condition_ids=''";
+        foreach ($conditions as $key => $item) {
+            $sql .= " OR FIND_IN_SET('{$item['id']}',condition_ids)";
+        }
+        $time = time();
+        //我的所有未使用的优惠券
+        $list = self::field('*')->with(['Coupon' => function ($query) use ($sql) {
+            $query->where($sql);
+        }])->where('user_id', $user_id)
+            ->where('is_used', 1)
+            ->where('begin_time', '<', $time)
+            ->where('expire_time', '>', $time)
+            ->select();
+        $data = [];
+        foreach ($list as $item) {
+            if (!empty($item['coupon'])) {
+                $coupon = $item['coupon'];
+                $coupon['id'] = is_numeric($coupon['id']) ? IntCode::encode($coupon['id']) : $coupon['id'];
+                $coupon['user_coupon_id'] = $item['id'];
+                $coupon['expire_time'] = $item['expire_time'];
+                $data[] = $coupon->toArray();
+            }
+        }
+        return $data;
+    }
+
+    //恢复优惠券
+    public static function resetUserCoupon($user_coupon_id, $order_sn)
+    {
+        if ($user_coupon_id) {
+            self::where('id', $user_coupon_id)->update(['is_used' => 1]);
+            //记录操作
+            OrderAction::push($order_sn, '系统', '订单取消恢复优惠券');
+        }
+        return true;
+    }
+
+    public function Coupon()
+    {
+        return $this->hasOne('Coupon', 'id', 'coupon_id');
+    }
+}

+ 8 - 4
application/common/model/Version.php

@@ -25,14 +25,17 @@ class Version extends Model
     public static function check($version)
     public static function check($version)
     {
     {
         $versionlist = self::where('status', 'normal')->cache('__version__')->order('weigh desc,id desc')->select();
         $versionlist = self::where('status', 'normal')->cache('__version__')->order('weigh desc,id desc')->select();
-        foreach ($versionlist as $k => $v) {
+        foreach ($versionlist as $k => $v)
+        {
             // 版本正常且新版本号不等于验证的版本号且找到匹配的旧版本
             // 版本正常且新版本号不等于验证的版本号且找到匹配的旧版本
-            if ($v['status'] == 'normal' && $v['newversion'] !== $version && \fast\Version::check($version, $v['oldversion'])) {
+            if ($v['status'] == 'normal' && $v['newversion'] !== $version && \fast\Version::check($version, $v['oldversion']))
+            {
                 $updateversion = $v;
                 $updateversion = $v;
                 break;
                 break;
             }
             }
         }
         }
-        if (isset($updateversion)) {
+        if (isset($updateversion))
+        {
             $search = ['{version}', '{newversion}', '{downloadurl}', '{url}', '{packagesize}'];
             $search = ['{version}', '{newversion}', '{downloadurl}', '{url}', '{packagesize}'];
             $replace = [$version, $updateversion['newversion'], $updateversion['downloadurl'], $updateversion['downloadurl'], $updateversion['packagesize']];
             $replace = [$version, $updateversion['newversion'], $updateversion['downloadurl'], $updateversion['downloadurl'], $updateversion['packagesize']];
             $upgradetext = str_replace($search, $replace, $updateversion['content']);
             $upgradetext = str_replace($search, $replace, $updateversion['content']);
@@ -45,6 +48,7 @@ class Version extends Model
                 "upgradetext" => $upgradetext
                 "upgradetext" => $upgradetext
             ];
             ];
         }
         }
-        return null;
+        return NULL;
     }
     }
+
 }
 }