Selaa lähdekoodia

安装shopro,第二步

lizhen_gitee 5 kuukautta sitten
vanhempi
commit
386a683bad
100 muutettua tiedostoa jossa 13849 lisäystä ja 0 poistoa
  1. 0 0
      addons/shopro/.addonrc
  2. 155 0
      addons/shopro/Shopro.php
  3. 231 0
      addons/shopro/application/admin/controller/shopro/Category.php
  4. 31 0
      addons/shopro/application/admin/controller/shopro/Common.php
  5. 320 0
      addons/shopro/application/admin/controller/shopro/Config.php
  6. 312 0
      addons/shopro/application/admin/controller/shopro/Coupon.php
  7. 241 0
      addons/shopro/application/admin/controller/shopro/Dashboard.php
  8. 112 0
      addons/shopro/application/admin/controller/shopro/Feedback.php
  9. 223 0
      addons/shopro/application/admin/controller/shopro/PayConfig.php
  10. 30 0
      addons/shopro/application/admin/controller/shopro/Share.php
  11. 126 0
      addons/shopro/application/admin/controller/shopro/Withdraw.php
  12. 321 0
      addons/shopro/application/admin/controller/shopro/activity/Activity.php
  13. 124 0
      addons/shopro/application/admin/controller/shopro/activity/Groupon.php
  14. 323 0
      addons/shopro/application/admin/controller/shopro/app/ScoreShop.php
  15. 163 0
      addons/shopro/application/admin/controller/shopro/app/mplive/Goods.php
  16. 81 0
      addons/shopro/application/admin/controller/shopro/app/mplive/Index.php
  17. 201 0
      addons/shopro/application/admin/controller/shopro/app/mplive/Room.php
  18. 129 0
      addons/shopro/application/admin/controller/shopro/chat/CommonWord.php
  19. 205 0
      addons/shopro/application/admin/controller/shopro/chat/CustomerService.php
  20. 38 0
      addons/shopro/application/admin/controller/shopro/chat/Index.php
  21. 133 0
      addons/shopro/application/admin/controller/shopro/chat/Question.php
  22. 37 0
      addons/shopro/application/admin/controller/shopro/chat/Record.php
  23. 62 0
      addons/shopro/application/admin/controller/shopro/chat/User.php
  24. 250 0
      addons/shopro/application/admin/controller/shopro/commission/Agent.php
  25. 89 0
      addons/shopro/application/admin/controller/shopro/commission/Goods.php
  26. 144 0
      addons/shopro/application/admin/controller/shopro/commission/Level.php
  27. 50 0
      addons/shopro/application/admin/controller/shopro/commission/Log.php
  28. 323 0
      addons/shopro/application/admin/controller/shopro/commission/Order.php
  29. 108 0
      addons/shopro/application/admin/controller/shopro/commission/Reward.php
  30. 170 0
      addons/shopro/application/admin/controller/shopro/data/Area.php
  31. 156 0
      addons/shopro/application/admin/controller/shopro/data/Express.php
  32. 222 0
      addons/shopro/application/admin/controller/shopro/data/FakeUser.php
  33. 138 0
      addons/shopro/application/admin/controller/shopro/data/Faq.php
  34. 156 0
      addons/shopro/application/admin/controller/shopro/data/Page.php
  35. 170 0
      addons/shopro/application/admin/controller/shopro/data/Richtext.php
  36. 48 0
      addons/shopro/application/admin/controller/shopro/decorate/Designer.php
  37. 76 0
      addons/shopro/application/admin/controller/shopro/decorate/Page.php
  38. 286 0
      addons/shopro/application/admin/controller/shopro/decorate/Template.php
  39. 232 0
      addons/shopro/application/admin/controller/shopro/dispatch/Dispatch.php
  40. 247 0
      addons/shopro/application/admin/controller/shopro/goods/Comment.php
  41. 444 0
      addons/shopro/application/admin/controller/shopro/goods/Goods.php
  42. 157 0
      addons/shopro/application/admin/controller/shopro/goods/Service.php
  43. 57 0
      addons/shopro/application/admin/controller/shopro/goods/SkuPrice.php
  44. 48 0
      addons/shopro/application/admin/controller/shopro/goods/StockLog.php
  45. 120 0
      addons/shopro/application/admin/controller/shopro/goods/StockWarning.php
  46. 227 0
      addons/shopro/application/admin/controller/shopro/notification/Config.php
  47. 128 0
      addons/shopro/application/admin/controller/shopro/notification/Notification.php
  48. 183 0
      addons/shopro/application/admin/controller/shopro/notification/traits/Notification.php
  49. 338 0
      addons/shopro/application/admin/controller/shopro/order/Aftersale.php
  50. 62 0
      addons/shopro/application/admin/controller/shopro/order/Invoice.php
  51. 968 0
      addons/shopro/application/admin/controller/shopro/order/Order.php
  52. 199 0
      addons/shopro/application/admin/controller/shopro/trade/Order.php
  53. 303 0
      addons/shopro/application/admin/controller/shopro/traits/SkuPrice.php
  54. 33 0
      addons/shopro/application/admin/controller/shopro/user/Coupon.php
  55. 27 0
      addons/shopro/application/admin/controller/shopro/user/Group.php
  56. 152 0
      addons/shopro/application/admin/controller/shopro/user/User.php
  57. 90 0
      addons/shopro/application/admin/controller/shopro/user/WalletLog.php
  58. 137 0
      addons/shopro/application/admin/controller/shopro/wechat/Admin.php
  59. 30 0
      addons/shopro/application/admin/controller/shopro/wechat/Config.php
  60. 178 0
      addons/shopro/application/admin/controller/shopro/wechat/Material.php
  61. 223 0
      addons/shopro/application/admin/controller/shopro/wechat/Menu.php
  62. 143 0
      addons/shopro/application/admin/controller/shopro/wechat/Reply.php
  63. 53 0
      addons/shopro/application/admin/model/shopro/Admin.php
  64. 75 0
      addons/shopro/application/admin/model/shopro/Cart.php
  65. 35 0
      addons/shopro/application/admin/model/shopro/Category.php
  66. 80 0
      addons/shopro/application/admin/model/shopro/Common.php
  67. 192 0
      addons/shopro/application/admin/model/shopro/Config.php
  68. 320 0
      addons/shopro/application/admin/model/shopro/Coupon.php
  69. 36 0
      addons/shopro/application/admin/model/shopro/Feedback.php
  70. 81 0
      addons/shopro/application/admin/model/shopro/Pay.php
  71. 46 0
      addons/shopro/application/admin/model/shopro/PayConfig.php
  72. 38 0
      addons/shopro/application/admin/model/shopro/Refund.php
  73. 15 0
      addons/shopro/application/admin/model/shopro/SearchHistory.php
  74. 142 0
      addons/shopro/application/admin/model/shopro/Share.php
  75. 9 0
      addons/shopro/application/admin/model/shopro/ThirdOauth.php
  76. 83 0
      addons/shopro/application/admin/model/shopro/Withdraw.php
  77. 13 0
      addons/shopro/application/admin/model/shopro/WithdrawLog.php
  78. 407 0
      addons/shopro/application/admin/model/shopro/activity/Activity.php
  79. 73 0
      addons/shopro/application/admin/model/shopro/activity/GiftLog.php
  80. 98 0
      addons/shopro/application/admin/model/shopro/activity/Groupon.php
  81. 43 0
      addons/shopro/application/admin/model/shopro/activity/GrouponLog.php
  82. 93 0
      addons/shopro/application/admin/model/shopro/activity/Order.php
  83. 21 0
      addons/shopro/application/admin/model/shopro/activity/Signin.php
  84. 128 0
      addons/shopro/application/admin/model/shopro/activity/SkuPrice.php
  85. 63 0
      addons/shopro/application/admin/model/shopro/app/ScoreSkuPrice.php
  86. 53 0
      addons/shopro/application/admin/model/shopro/app/mplive/Goods.php
  87. 99 0
      addons/shopro/application/admin/model/shopro/app/mplive/Room.php
  88. 23 0
      addons/shopro/application/admin/model/shopro/chat/CommonWord.php
  89. 57 0
      addons/shopro/application/admin/model/shopro/chat/CustomerService.php
  90. 62 0
      addons/shopro/application/admin/model/shopro/chat/CustomerServiceUser.php
  91. 23 0
      addons/shopro/application/admin/model/shopro/chat/Question.php
  92. 94 0
      addons/shopro/application/admin/model/shopro/chat/Record.php
  93. 22 0
      addons/shopro/application/admin/model/shopro/chat/ServiceLog.php
  94. 33 0
      addons/shopro/application/admin/model/shopro/chat/User.php
  95. 31 0
      addons/shopro/application/admin/model/shopro/chat/traits/ChatCommon.php
  96. 83 0
      addons/shopro/application/admin/model/shopro/commission/Agent.php
  97. 47 0
      addons/shopro/application/admin/model/shopro/commission/CommissionGoods.php
  98. 20 0
      addons/shopro/application/admin/model/shopro/commission/Level.php
  99. 232 0
      addons/shopro/application/admin/model/shopro/commission/Log.php
  100. 116 0
      addons/shopro/application/admin/model/shopro/commission/Order.php

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
addons/shopro/.addonrc


+ 155 - 0
addons/shopro/Shopro.php

@@ -0,0 +1,155 @@
+<?php
+
+namespace addons\shopro;
+
+use think\Addons;
+use app\common\library\Menu;
+use app\admin\model\AuthRule;
+use addons\shopro\library\Hook;
+
+/**
+ * Shopro插件 v3.0.0
+ */
+class Shopro extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        // 创建菜单
+        $menu = self::getMenu();
+        Menu::create($menu['new']);
+
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        // 删除菜单
+        Menu::delete('shopro');
+
+        return true;
+    }
+
+    /**
+     * 插件启用方法
+     */
+    public function enable()
+    {
+        // 启用菜单
+        Menu::enable('shopro');
+
+        return true;
+    }
+
+    /**
+     * 插件更新方法
+     */
+    public function upgrade()
+    {
+        // 更新菜单
+        $menu = self::getMenu();
+        Menu::upgrade('shopro', $menu['new']);
+
+        return true;
+    }
+
+    /**
+     * 插件禁用方法
+     */
+    public function disable()
+    {
+        // 禁用菜单
+        Menu::disable('shopro');
+
+        return true;
+    }
+
+
+    /**
+     * 应用初始化
+     */
+    public function appInit()
+    {
+        // 公共方法
+        require_once __DIR__ . '/helper/helper.php';
+
+        // 覆盖队列 redis 参数
+        $queue = \think\Config::get('queue');
+        $redis = \think\Config::get('redis');
+        if ($queue && strtolower($queue['connector']) == 'redis' && $redis) {
+            $queue = array_merge($redis, $queue);       // queue.php 中的配置,覆盖 redis.php 中的配置
+            \think\Config::set('queue', $queue);
+        }
+
+        // database 增加断线重连参数
+        $database = \think\Config::get('database');
+        $database['break_reconnect'] = true;        // 断线重连
+        \think\Config::set('database', $database);
+
+        // 全局注册行为事件
+        Hook::register();
+
+        if (request()->isCli()) {
+            \think\Console::addDefaultCommands([
+                'addons\shopro\console\ShoproChat',
+                'addons\shopro\console\ShoproHelp'
+            ]);
+        }
+
+        // 全局共享 暗色类型 变量
+        \think\View::share('DARK_TYPE', $this->getDarkType());
+    }
+
+
+
+    public function configInit(&$config)
+    {
+        // 全局 js共享 暗色类型 变量
+        $config['dark_type'] = $this->getDarkType();
+    }
+
+
+    private static function getMenu()
+    {
+        $newMenu = [];
+        $config_file = ADDON_PATH . "shopro" . DS . 'config' . DS . "menu.php";
+        if (is_file($config_file)) {
+            $newMenu = include $config_file;
+        }
+        $oldMenu = AuthRule::where('name', 'like', "shopro%")->select();
+        $oldMenu = array_column($oldMenu, null, 'name');
+        return ['new' => $newMenu, 'old' => $oldMenu];
+    }
+
+
+
+    /**
+     * 获取暗黑类型
+     *
+     * @return string
+     */
+    private function getDarkType()
+    {
+        $dark_type = 'none';
+        if (in_array('darktheme', get_addonnames())) {
+            // 有暗黑主题
+            $darkthemeConfig = get_addon_config('darktheme');
+            $dark_type = $darkthemeConfig['mode'] ?? 'none';
+
+            $thememode = cookie("thememode");
+            if ($thememode && in_array($thememode, ['dark', 'light'])) {
+                $dark_type = $thememode;
+            }
+        }
+
+        return $dark_type;
+    }
+}

+ 231 - 0
addons/shopro/application/admin/controller/shopro/Category.php

@@ -0,0 +1,231 @@
+<?php
+
+namespace app\admin\controller\shopro;
+
+use think\Db;
+use addons\shopro\library\Tree;
+use app\admin\model\shopro\Category as CategoryModel;
+
+/**
+ * 商品分类
+ */
+class Category extends Common
+{
+
+    protected $noNeedRight = ['select', 'goodsSelect'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new CategoryModel;
+    }
+
+
+    /**
+     * 服务保障列表
+     *
+     * @return \think\Response
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+        
+        $categories = $this->model->sheepFilter()->where('parent_id', 0)->order('weigh', 'desc')->order('id', 'desc')->select();
+
+        $this->success('获取成功', null, $categories);
+    }
+
+
+
+
+    /**
+     * 添加服务保障
+     *
+     * @return \think\Response
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only([
+            'name', 'style', 'description', 'weigh', 'categories'
+        ]);
+        $this->svalidate($params, ".add");
+        $categories = json_decode($params['categories'], true);
+
+        Db::transaction(function () use ($params, $categories) {
+            $this->model->allowField(true)->save($params);
+
+            //递归处理分类数据
+            $this->createOrEditCategory($categories, $this->model->id);
+        });
+        $this->success('保存成功');
+    }
+
+
+    /**
+     * 服务保障详情
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function detail($id)
+    {
+        $category = $this->model->where('parent_id', 0)->where('id', $id)->find();
+        if (!$category) {
+            $this->error(__('No Results were found'));
+        }
+        
+        $categories = $this->model->with('children.children')->where('parent_id', $category->id)->order('weigh', 'desc')->order('id', 'desc')->select();
+
+        $this->success('获取成功', null, ['category' => $category, 'categories' => $categories]);
+    }
+
+
+
+    /**
+     * 修改商品分类
+     *
+     * @return \think\Response
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isPost()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->only([
+            'name', 'style', 'description', 'weigh', 'categories'
+        ]);
+        $this->svalidate($params, ".edit");
+        $categories = json_decode($params['categories'], true);
+        $category = $this->model->where('parent_id', 0)->where('id', $id)->find();
+        if (!$category) {
+            $this->error(__('No Results were found'));
+        }
+        Db::transaction(function () use ($category, $params, $categories) {
+            $category->allowField(true)->save($params);
+
+            //递归处理分类数据
+            $this->createOrEditCategory($categories, $category->id);
+        });
+        $this->success('更新成功');
+    }
+
+
+
+    /**
+     * 删除服务标签
+     *
+     * @param string $id 要删除的服务保障列表
+     * @return void
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $list = $this->model->with('children')->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list) {
+            $count = 0;
+            foreach ($list as $item) {
+                if ($item->children) {
+                    $this->error('请先删除子分类');
+                }
+                $count += $item->delete();
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('删除成功', null, $result);
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+
+
+    /**
+     * 获取所有服务列表
+     *
+     * @return \think\Response
+     */
+    public function select()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $categories = (new Tree(function () {
+            // 组装搜索条件,排序等
+            return $this->model->field("id, name, parent_id, status, weigh")->normal()
+                ->order('weigh', 'desc')->order('id', 'asc');
+        }))->getTree(0);
+
+        $this->success('获取成功', null, $categories);
+    }
+
+
+
+    /**
+     * 商品列表左边分类
+     *
+     * @return void
+     */
+    public function goodsSelect()
+    {
+        $categories = $this->model->with(['children'])
+            ->where('parent_id', 0)->order('weigh', 'desc')->order('id', 'desc')->select();
+
+        $this->success('获取成功', null, $categories);
+    }
+
+
+
+    private function createOrEditCategory($categories, $parent_id)
+    {
+        foreach ($categories as $key => $data) {
+            $data['parent_id'] = $parent_id;
+
+            if (isset($data['id']) && $data['id']) {
+                $category = $this->model->find($data['id']);
+                if (!$category) {
+                    $this->error(__('No Results were found'));
+                }
+                if (isset($data['deleted']) && $data['deleted'] == 1) {
+                    $category->delete();
+                } else {
+                    $category->name = $data['name'];
+                    $category->parent_id = $data['parent_id'];
+                    $category->image = $data['image'];
+                    $category->description = $data['description'] ?? null;
+                    $category->status = $data['status'];
+                    $category->weigh = $data['weigh'];
+                    $category->save();
+                }
+            } else {
+                if (!isset($data['deleted']) || !$data['deleted']) {
+                    $category = new CategoryModel;
+                    $category->name = $data['name'];
+                    $category->parent_id = $data['parent_id'];
+                    $category->image = $data['image'];
+                    $category->description = $data['description'] ?? null;
+                    $category->status = $data['status'];
+                    $category->weigh = $data['weigh'];
+                    $category->save();
+                    $data['id'] = $category->id;
+                }
+            }
+
+            if (isset($data['children']) && !empty($data['children']) && isset($data['id'])) {
+                $this->createOrEditCategory($data['children'], $data['id']);
+            }
+        }
+    }
+}

+ 31 - 0
addons/shopro/application/admin/controller/shopro/Common.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace app\admin\controller\shopro;
+
+use app\common\controller\Backend;
+use addons\shopro\controller\traits\Util;
+use addons\shopro\controller\traits\UnifiedToken;
+
+/**
+ * shopro 基础控制器
+ */
+class Common extends Backend
+{
+
+    use Util, UnifiedToken;
+
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+
+        if (check_env('yansongda', false)) {
+            set_addon_config('epay', ['version' => 'v3'], false);
+        }
+
+        $is_pro = check_env('commission', false);
+        \think\View::share('is_pro', $is_pro);
+        $this->assignconfig("is_pro", $is_pro);
+    }
+}

+ 320 - 0
addons/shopro/application/admin/controller/shopro/Config.php

@@ -0,0 +1,320 @@
+<?php
+
+namespace app\admin\controller\shopro;
+
+use app\admin\model\shopro\Config as ShoproConfig;
+
+class Config extends Common
+{
+    protected $noNeedRight = ['index', 'platformStatus', 'getPlatformUrl'];
+
+    public function index()
+    {
+        $configList = [
+            [
+                'label' => '基本信息',
+                'name' => 'shopro/config/basic',
+                'status' => $this->auth->check('shopro/config/basic')
+            ],
+            [
+                'label' => '用户配置',
+                'name' => 'shopro/config/user',
+                'status' => $this->auth->check('shopro/config/user')
+            ],
+            [
+                'label' => '平台配置',
+                'name' => 'shopro/config/platform',
+                'status' => $this->auth->check('shopro/config/platform')
+            ],
+            [
+                'label' => '订单配置',
+                'name' => 'shopro/config/order',
+                'status' => $this->auth->check('shopro/config/order')
+            ],
+            [
+                'label' => '商品配置',
+                'name' => 'shopro/config/goods',
+                'status' => $this->auth->check('shopro/config/goods')
+            ],
+            [
+                'label' => '物流配置',
+                'name' => 'shopro/config/dispatch',
+                'status' => $this->auth->check('shopro/config/dispatch')
+            ],
+            [
+                'label' => '充值提现',
+                'name' => 'shopro/config/rechargewithdraw',
+                'status' => $this->auth->check('shopro/config/rechargewithdraw')
+            ],
+            [
+                'label' => '分销配置',
+                'name' => 'shopro/config/commission',
+                'status' => $this->auth->check('shopro/config/commission')
+            ],
+            [
+                'label' => '支付配置',
+                'name' => 'shopro/pay_config',
+                'status' => $this->auth->check('shopro/pay_config')
+            ],
+            [
+                'label' => '客服配置',
+                'name' => 'shopro/config/chat',
+                'status' => $this->auth->check('shopro/config/chat')
+            ],
+            [
+                'label' => 'Redis配置',
+                'name' => 'shopro/config/redis',
+                'status' => $this->auth->check('shopro/config/redis')
+            ]
+        ];
+        $this->assignconfig("configList", $configList);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 基本配置
+     */
+    public function basic()
+    {
+        if ('GET' === $this->request->method()) {
+
+            $configs = ShoproConfig::getConfigs('shop.basic', false);
+        } elseif ('POST' === $this->request->method()) {
+
+            $configs = ShoproConfig::setConfigs('shop.basic', $this->request->param());
+        }
+        $this->success('操作成功', null, $configs);
+    }
+
+
+    /**
+     * 用户默认配置
+     */
+    public function user()
+    {
+        if ('GET' === $this->request->method()) {
+
+            $configs = ShoproConfig::getConfigs('shop.user', false);
+        } elseif ('POST' === $this->request->method()) {
+
+            $configs = ShoproConfig::setConfigs('shop.user', $this->request->param());
+        }
+        $this->success('操作成功', null, $configs);
+    }
+
+
+    /**
+     * 物流配置
+     */
+    public function dispatch()
+    {
+        if ('GET' === $this->request->method()) {
+
+            $configs = ShoproConfig::getConfigs('shop.dispatch', false);
+            $configs['callback'] = $this->request->domain() . '/addons/shopro/order.express/push';
+        } elseif ('POST' === $this->request->method()) {
+
+            $configs = ShoproConfig::setConfigs('shop.dispatch', $this->request->param());
+        }
+        $this->success('操作成功', null, $configs);
+    }
+
+
+    /**
+     * 平台状态
+     */
+    public function platformStatus()
+    {
+        $status = [
+            'H5' => ShoproConfig::getConfigs('shop.platform.H5.status', false),
+            'App' => ShoproConfig::getConfigs('shop.platform.App.status', false),
+            'WechatMiniProgram' => ShoproConfig::getConfigs('shop.platform.WechatMiniProgram.status', false),
+            'WechatOfficialAccount' => ShoproConfig::getConfigs('shop.platform.WechatOfficialAccount.status', false),
+        ];
+
+        $this->success('操作成功', null, $status);
+    }
+
+
+
+    /**
+     * 平台配置
+     */
+    public function platform($platform)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        if (!in_array($platform, ['App', 'H5', 'WechatMiniProgram', 'WechatOfficialAccount'])) {
+            $this->error('平台不支持');
+        }
+
+        if ('GET' === $this->request->method()) {
+
+            $configs = ShoproConfig::getConfigs('shop.platform.' . $platform, false);
+        } elseif ('POST' === $this->request->method()) {
+            $params = $this->request->param();
+            if (!isset($params['share']['methods'])) {
+                $params['share']['methods'] = [];
+            }
+            if (!isset($params['payment']['methods'])) {
+                $params['payment']['methods'] = [];
+            }
+            $configs = ShoproConfig::setConfigs('shop.platform.' . $platform, $params);
+        }
+        $this->success('操作成功', null, $configs);
+    }
+
+
+    public function commission()
+    {
+        if ('GET' === $this->request->method()) {
+
+            $configs = ShoproConfig::getConfigs('shop.commission', false);
+        } elseif ('POST' === $this->request->method()) {
+            check_env('commission');
+            $params = $this->request->param();
+
+            $configs = ShoproConfig::setConfigs('shop.commission', $params);
+        }
+        $this->success('操作成功', null, $configs);
+    }
+
+
+    /**
+     * 订单配置
+     */
+    public function order()
+    {
+        if ('GET' === $this->request->method()) {
+
+            $configs = ShoproConfig::getConfigs('shop.order', false);
+        } elseif ('POST' === $this->request->method()) {
+
+            $configs = ShoproConfig::setConfigs('shop.order', $this->request->param());
+        }
+        $this->success('操作成功', null, $configs);
+    }
+
+
+    /**
+     * 商品配置
+     */
+    public function goods()
+    {
+        if ('GET' === $this->request->method()) {
+
+            $configs = ShoproConfig::getConfigs('shop.goods', false);
+        } elseif ('POST' === $this->request->method()) {
+
+            $configs = ShoproConfig::setConfigs('shop.goods', $this->request->param());
+        }
+        $this->success('操作成功', null, $configs);
+    }
+
+
+    /**
+     * 充值提现配置
+     */
+    public function rechargeWithdraw()
+    {
+        if ('GET' === $this->request->method()) {
+
+            $configs = ShoproConfig::getConfigs('shop.recharge_withdraw', false);
+        } elseif ('POST' === $this->request->method()) {
+            $params = $this->request->param();
+            if (!isset($params['recharge']['methods'])) {
+                $params['recharge']['methods'] = [];
+            }
+            if (!isset($params['recharge']['quick_amounts'])) {
+                $params['recharge']['quick_amounts'] = [];
+            }
+            if (!isset($params['withdraw']['methods'])) {
+                $params['withdraw']['methods'] = [];
+            }
+            $configs = ShoproConfig::setConfigs('shop.recharge_withdraw', $params);
+        }
+        $this->success('操作成功', null, $configs);
+    }
+
+
+
+    /**
+     * 客服配置
+     */
+    public function chat()
+    {
+        if ('GET' === $this->request->method()) {
+
+            $configs = ShoproConfig::getConfigs('chat', false);
+        } elseif ('POST' === $this->request->method()) {
+            $configs = ShoproConfig::setConfigs('chat', $this->request->param());
+
+            // 存文件
+            file_put_contents(
+                ROOT_PATH . 'application' . DS . 'extra' . DS . 'chat.php',
+                '<?php' . "\n\nreturn " . short_var_export($this->request->param(), true) . ";"
+            );
+        }
+        $this->success('操作成功', null, $configs);
+    }
+
+
+
+    /**
+     * redis 配置
+     */
+    public function redis()
+    {
+        if ('GET' === $this->request->method()) {
+            $default = [
+                'host' => '127.0.0.1',              // redis 主机地址
+                'password' => '',                   // redis 密码
+                'port' => 6379,                     // redis 端口
+                'select' => 1,                      // redis 数据库
+                'timeout' => 0,                     // redis 超时时间
+                'persistent' => false,              // redis 持续性,连接复用
+            ];
+            $redis = \think\Config::get('redis');
+            $redis['empty_password'] = 0;
+            $redis['password'] = '';                // 隐藏密码
+            $configs = $redis ? array_merge($default, $redis) : $default;
+        } elseif ('POST' === $this->request->method()) {
+            operate_filter();
+            $configs = $this->request->param();
+            $empty_password = (int)$configs['empty_password'];      // 是否设置空密码
+            unset($configs['empty_password']);
+
+            if (isset($configs['password']) && empty($configs['password'])) {
+                $redis = \think\Config::get('redis');
+                // 不修改密码,保持为原始值
+                $configs['password'] = $redis['password'] ?? '';
+            } elseif ($empty_password) {
+                $configs['password'] = '';
+            }
+
+            $configs['persistent'] = (isset($configs['persistent']) && ($configs['persistent'] === true || $configs['persistent'] == 'true')) ? true : false;
+
+            // 存文件
+            file_put_contents(
+                ROOT_PATH . 'application' . DS . 'extra' . DS . 'redis.php',
+                '<?php' . "\n\nreturn " . short_var_export($configs, true) . ";"
+            );
+        }
+        $this->success('操作成功', null, $configs);
+    }
+
+
+
+    public function getPlatformUrl()
+    {
+        $h5Url = ShoproConfig::getConfigField('shop.basic.domain');
+        $wechatMpAppid =  ShoproConfig::getConfigField('shop.platform.WechatMiniProgram.app_id');
+
+        $this->success('', null, [
+            'url' => $h5Url,
+            'appid' => $wechatMpAppid
+        ]);
+    }
+}

+ 312 - 0
addons/shopro/application/admin/controller/shopro/Coupon.php

@@ -0,0 +1,312 @@
+<?php
+
+namespace app\admin\controller\shopro;
+
+use think\Db;
+use app\admin\model\shopro\Coupon as CouponModel;
+use app\admin\model\shopro\user\Coupon as UserCouponModel;
+use addons\shopro\traits\CouponSend;
+use app\admin\model\shopro\user\User;
+
+/**
+ * 优惠券
+ */
+class Coupon extends Common
+{
+
+    use CouponSend;
+
+    protected $noNeedRight = ['select'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new CouponModel;
+    }
+
+
+
+    /**
+     * 优惠券列表
+     *
+     * @return \think\Response
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $coupons = $this->model->sheepFilter()->paginate($this->request->param('list_rows', 10))->each(function ($coupon) {
+            // 优惠券领取和使用数量
+            $coupon->get_num = $coupon->get_num;
+            $coupon->use_num = $coupon->use_num;
+        });
+
+        $result = [
+            'coupons' => $coupons,
+        ];
+
+        $result['total_num'] = UserCouponModel::count();
+        $result['expire_num'] = UserCouponModel::expired()->count();
+        $result['use_num'] = UserCouponModel::used()->count();
+        $result['use_percent'] = 0 . '%';
+        if ($result['total_num']) {
+            $result['use_percent'] = bcdiv(bcmul((string)$result['use_num'], '100'), (string)$result['total_num'], 1) . '%';
+        }
+
+        $this->success('获取成功', null, $result);
+    }
+
+
+
+
+    /**
+     * 添加优惠券
+     *
+     * @return \think\Response
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only([
+            'name', 'type', 'use_scope', 'items', 'amount', 'max_amount', 'enough',
+            'stock', 'limit_num', 'get_time', 
+            'use_time_type', 'use_time', 'start_days', 'days',
+            'is_double_discount', 'description', 'status'
+        ]);
+        $this->svalidate($params, ".add");
+
+        // 时间转换
+        $getTime = explode(' - ', $params['get_time']);
+        $useTime = explode(' - ', $params['use_time']);
+        unset($params['get_time'], $params['use_time']);
+        $params['get_start_time'] = $getTime[0] ?? 0;
+        $params['get_end_time'] = $getTime[1] ?? 0;
+        $params['use_start_time'] = $useTime[0] ?? 0;
+        $params['use_end_time'] = $useTime[1] ?? 0;
+
+        $this->model->save($params);
+        
+        $this->success('保存成功');
+    }
+
+
+    /**
+     * 优惠券详情
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function detail($id)
+    {
+        $coupon = $this->model->where('id', $id)->find();
+        if (!$coupon) {
+            $this->error(__('No Results were found'));
+        }
+
+        $coupon->items_value = $coupon->items_value;       // 可用范围值
+
+        $this->success('获取成功', null, $coupon);
+    }
+
+
+
+    /**
+     * 修改优惠券
+     *
+     * @return \think\Response
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->only([
+            'name', 'use_scope', 'items', 'amount', 'max_amount', 'enough',
+            'stock', 'limit_num', 'get_time',
+            'use_time_type', 'use_time', 'start_days', 'days',
+            'is_double_discount', 'description', 'status'
+        ]);
+        $this->svalidate($params);
+
+        // 时间转换
+        if (isset($params['get_time'])) {
+            $getTime = explode(' - ', $params['get_time']);
+            
+            $params['get_start_time'] = $getTime[0] ?? 0;
+            $params['get_end_time'] = $getTime[1] ?? 0;
+            unset($params['get_time']);
+        }
+        
+        if (isset($params['use_time'])) {
+            $useTime = explode(' - ', $params['use_time']);
+
+            $params['use_start_time'] = $useTime[0] ?? 0;
+            $params['use_end_time'] = $useTime[1] ?? 0;
+            unset($params['use_time']);
+        }
+
+        $coupon = $this->model->where('id', $id)->find();
+        if (!$coupon) {
+            $this->error(__('No Results were found'));
+        }
+
+        $coupon->save($params);
+        $this->success('更新成功');
+    }
+
+
+
+    /**
+     * 删除优惠券
+     *
+     * @param string $id 要删除的优惠券
+     * @return void
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $id = explode(',', $id);
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list) {
+            $count = 0;
+            foreach ($list as $item) {
+                $count += $item->delete();
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('删除成功', null, $result);
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+
+
+    /**
+     * 优惠券列表
+     */
+    public function select()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $type = $this->request->param('type', 'page');
+        
+        $coupons = $this->model->sheepFilter();
+
+        if ($type == 'select') {
+            // 普通结果
+            $coupons = $coupons->select();
+        } elseif ($type == 'find') {
+            $coupons = $coupons->find();
+        } else {
+            // 分页结果
+            $coupons = $coupons->paginate($this->request->param('list_rows', 10));
+        }
+
+        $this->success('获取成功', null, $coupons);
+    }
+
+
+
+    public function send($id)
+    {
+        $user_ids = $this->request->post('user_ids/a');
+
+        $users = User::whereIn('id', $user_ids)->select();
+
+        Db::transaction(function () use ($users, $id) {
+            $this->manualSend($users, $id);
+        });
+
+        $this->success('发放成功');
+    }
+
+
+
+    public function recyclebin()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $coupons = $this->model->onlyTrashed()->sheepFilter()->paginate($this->request->param('list_rows', 10));
+
+        $this->success('获取成功', null, $coupons);
+    }
+
+
+    /**
+     * 还原(支持批量)
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function restore($id = null)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $items = $this->model->onlyTrashed()->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($items) {
+            $count = 0;
+            foreach ($items as $item) {
+                $count += $item->restore();
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('还原成功', null, $result);
+        } else {
+            $this->error(__('No rows were updated'));
+        }
+    }
+
+
+    /**
+     * 销毁(支持批量)
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function destroy($id = null)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        if ($id !== 'all') {
+            $items = $this->model->onlyTrashed()->whereIn('id', $id)->select();
+        } else {
+            $items = $this->model->onlyTrashed()->select();
+        }
+        $result = Db::transaction(function () use ($items) {
+            $count = 0;
+            foreach ($items as $item) {
+                // 删除商品
+                $count += $item->delete(true);
+            }
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('销毁成功', null, $result);
+        }
+        $this->error('销毁失败');
+    }
+}

+ 241 - 0
addons/shopro/application/admin/controller/shopro/Dashboard.php

@@ -0,0 +1,241 @@
+<?php
+
+namespace app\admin\controller\shopro;
+
+use app\admin\model\shopro\order\Order;
+use app\admin\model\shopro\order\OrderItem;
+use app\admin\model\shopro\goods\Goods;
+use app\admin\model\shopro\user\User;
+use app\admin\model\shopro\Share;
+use addons\shopro\service\SearchHistory;
+
+class Dashboard extends Common
+{
+
+    public function index()
+    {
+        $cardList = [
+            [
+                'name' => 'total',
+                'card' => 'shopro/dashboard/total',
+                'status' => $this->auth->check('shopro/dashboard/total')
+            ],
+            [
+                'name' => 'chart',
+                'card' => 'shopro/dashboard/chart',
+                'status' => $this->auth->check('shopro/dashboard/chart')
+            ],
+            [
+                'name' => 'ranking',
+                'card' => 'shopro/dashboard/ranking',
+                'status' => $this->auth->check('shopro/dashboard/ranking')
+            ],
+        ];
+        $this->assignconfig('cardList', $cardList);
+        return $this->view->fetch();
+    }
+
+
+    public function total()
+    {
+        // 用户数据
+        $userData = [
+            'total' => User::count(),
+            'today' => User::whereTime('createtime', 'today')->count(),
+            'week' => User::whereTime('createtime', 'week')->count(),
+            'array' => collection(User::field('id,createtime')->whereTime('createtime', 'today')->select())->each(function ($user) {
+                $user->counter = 1;
+                $user->createtime_unix = $user->getData('createtime') * 1000;
+            })
+        ];
+
+        // -- commission code start --
+        $agentData = [
+            'total' => \app\admin\model\shopro\commission\Agent::count(),
+            'today' => \app\admin\model\shopro\commission\Agent::whereTime('createtime', 'today')->count(),
+            'week' => \app\admin\model\shopro\commission\Agent::whereTime('createtime', 'week')->count(),
+            'array' => collection(\app\admin\model\shopro\commission\Agent::field('user_id,createtime')->whereTime('createtime', 'today')->select())->each(function ($agent) {
+                $agent->counter = 1;
+                $agent->createtime_unix = $agent->getData('createtime') * 1000;
+            })
+        ];
+        // -- commission code end --
+
+        // 分享数据
+        $shareData = [
+            'total' => Share::count(),
+            'today' => Share::whereTime('createtime', 'today')->count(),
+            'week' => Share::whereTime('createtime', 'week')->count(),
+            'array' => collection(Share::field('id,createtime')->whereTime('createtime', 'today')->select())->each(function ($share) {
+                $share->counter = 1;
+                $share->createtime_unix = $share->getData('createtime') * 1000;
+            })
+        ];
+
+        $this->success('获取成功', null, [
+            'user_data' => $userData,
+            'agent_data' => $agentData ?? null,
+            'share_data' => $shareData
+        ]);
+    }
+
+
+
+    public function chart()
+    {
+        $date = $this->request->param('date', '');
+        $date = array_values(array_filter(explode(' - ', $date)));
+
+        $orders = Order::with(['items'])->whereTime('createtime', 'between', $date)
+            ->order('id', 'asc')->select();
+
+        // 订单数
+        $data['orderNum'] = count($orders);
+        $data['orderArr'] = [];
+
+        // 支付订单(包含退款的订单, 不包含货到付款还未收货(未支付)订单)
+        $data['payOrderNum'] = 0;
+        $data['payOrderArr'] = [];
+        //支付金额(包含退款的订单, 不包含货到付款还未收货(未支付)订单)
+        $data['payAmountNum'] = 0;
+        $data['payAmountArr'] = [];
+
+        // 支付用户(一个人不管下多少单,都算一个,包含退款的订单, 不包含货到付款还未收货(未支付)订单)
+        $userIds = [];
+        $data['payUserNum'] = 0;
+        $data['payUserArr'] = [];
+
+        // 代发货(包含货到付款)
+        $data['noSendNum'] = 0;
+        $data['noSendArr'] = [];
+        //售后维权
+        $data['aftersaleNum'] = 0;
+        $data['aftersaleArr'] = [];
+        //退款订单
+        $data['refundNum'] = 0;
+        $data['refundArr'] = [];
+
+        foreach ($orders as $key => $order) {
+            $data['orderArr'][] = [
+                'counter' => 1,
+                'createtime' => $order->getData('createtime') * 1000,
+                'user_id' => $order->user_id
+            ];
+
+            // 已支付的,不包含,货到付款未支付的
+            if (in_array($order->status, [Order::STATUS_PAID, Order::STATUS_COMPLETED])) {
+                // 支付订单数
+                $data['payOrderNum']++;
+
+                $data['payOrderArr'][] = [
+                    'counter' => 1,
+                    'createtime' => $order->getData('createtime') * 1000,
+                    'user_id' => $order->user_id
+                ];
+
+                // 支付金额
+                $data['payAmountNum'] = bcadd((string)$data['payAmountNum'], $order->pay_fee, 2);
+
+                $data['payAmountArr'][] = [
+                    'counter' => $order->pay_fee,
+                    'createtime' => $order->getData('createtime') * 1000,
+                ];
+
+                // 下单用户
+                if (!in_array($order->user_id, $userIds)) {
+                    $data['payUserNum']++;
+                    $data['payUserArr'][] = [
+                        'counter' => 1,
+                        'createtime' => $order->getData('createtime') * 1000,
+                        'user_id' => $order->user_id
+                    ];
+                }
+            }
+
+            // 已支付的,和 货到付款未支付的
+            if (in_array($order->status, [Order::STATUS_PAID, Order::STATUS_COMPLETED]) || $order->isOffline($order)) {
+                $flagnoSend = false;
+                $flagaftersale = false;
+                $flagrefund = false;
+                $aftersaleIng = false;
+                foreach ($order->items as $k => $item) {
+                    if (
+                        !$flagnoSend
+                        && $item->dispatch_status == OrderItem::DISPATCH_STATUS_NOSEND
+                        && $item->refund_status == OrderItem::REFUND_STATUS_NOREFUND
+                        && in_array($order->apply_refund_status, [
+                            Order::APPLY_REFUND_STATUS_NOAPPLY,
+                            Order::APPLY_REFUND_STATUS_REFUSE
+                        ])
+                    ) {
+                        $flagnoSend = true;
+                    }
+
+                    if (
+                        $item->aftersale_status == OrderItem::AFTERSALE_STATUS_ING
+                        && $item->dispatch_status == OrderItem::DISPATCH_STATUS_NOSEND
+                        && $item->refund_status == OrderItem::REFUND_STATUS_NOREFUND
+                    ) {
+                        $aftersaleIng = true;
+                    }
+
+                    if (!$flagaftersale && $item->aftersale_status != OrderItem::AFTERSALE_STATUS_NOAFTER) {
+                        $data['aftersaleNum']++;
+                        // $data['aftersaleArr'][] = [
+                        //     'counter' => 1,
+                        //     'createtime' => $order->getData('createtime') * 1000,
+                        // ];
+                        $flagaftersale = true;
+                    }
+
+                    if (!$flagrefund && $item->refund_status > OrderItem::REFUND_STATUS_NOREFUND) {
+                        $data['refundNum']++;
+                        // $data['refundArr'][] = [
+                        //     'counter' => 1,
+                        //     'createtime' => $order->getData('createtime') * 1000,
+                        // ];
+                        $flagrefund = true;
+                    }
+                }
+
+                if (!$aftersaleIng && $flagnoSend) {
+                    // 存在正在售后中的订单,不算待发货(和订单列表保持一直)
+                    $data['noSendNum']++;
+                    // $data['noSendArr'][] = [
+                    //     'counter' => 1,
+                    //     'createtime' => $order->getData('createtime') * 1000,
+                    // ];
+                }
+            }
+        }
+
+        $this->success('获取成功', null, $data);
+    }
+
+
+
+    public function ranking()
+    {
+        $goods = Goods::limit(5)->order('sales', 'desc')->select();
+        foreach ($goods as $key => $gd) {
+            $gd->append(['real_sales']);
+            $result = OrderItem::field('sum(goods_num * goods_price) as sale_total_money')->where('goods_id', $gd['id'])
+                ->whereExists(function ($query) use ($gd) {
+                    $order_table_name = (new Order())->getQuery()->getTable();
+                    $table_name = (new OrderItem())->getQuery()->getTable();
+
+                    $query->table($order_table_name)->where($table_name . '.order_id=' . $order_table_name . '.id')
+                        ->whereIn('status', [Order::STATUS_PAID, Order::STATUS_COMPLETED]);       // 已支付的订单
+                })->find();
+
+            $gd['sale_total_money'] = $result['sale_total_money'] ?: 0;
+        }
+
+        $searchHistory = new SearchHistory();
+
+        $this->success('获取成功', null, [
+            'goods' => $goods,
+            'hot_search' => $searchHistory->hotSearch()
+        ]);
+    }
+}

+ 112 - 0
addons/shopro/application/admin/controller/shopro/Feedback.php

@@ -0,0 +1,112 @@
+<?php
+
+namespace app\admin\controller\shopro;
+
+use think\Db;
+use app\admin\model\shopro\Feedback as FeedbackModel;
+
+class Feedback extends Common
+{
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new FeedbackModel();
+    }
+
+
+    /**
+     * 查看
+     *
+     * @return Response
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $feedbacks = $this->model->sheepFilter()->with('user')->paginate($this->request->param('list_rows', 10));
+
+        $this->success('获取成功', null, $feedbacks);
+    }
+
+
+
+    /**
+     * 详情
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function detail($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $detail = $this->model->with('user')->where('id', $id)->find();
+        if (!$detail) {
+            $this->error(__('No Results were found'));
+        }
+
+        $this->success('获取成功', null, $detail);
+    }
+
+
+
+    /**
+     * 编辑(支持批量)
+     */
+    public function edit($id = null)
+    {
+        $params = $this->request->only(['status', 'remark']);
+
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list, $params) {
+            $count = 0;
+            foreach ($list as $item) {
+                $count += $item->save($params);
+            }
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('更新成功', null, $result);
+        } else {
+            $this->error('更新失败,未改变任何记录');
+        }
+    }
+
+
+    /**
+     * 删除优惠券
+     *
+     * @param string $id 要删除的意见反馈
+     * @return void
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $id = explode(',', $id);
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list) {
+            $count = 0;
+            foreach ($list as $item) {
+                $count += $item->delete();
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('删除成功', null, $result);
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+}

+ 223 - 0
addons/shopro/application/admin/controller/shopro/PayConfig.php

@@ -0,0 +1,223 @@
+<?php
+
+namespace app\admin\controller\shopro;
+
+use think\Db;
+use app\admin\model\shopro\PayConfig as PayConfigModel;
+
+/**
+ * 支付配置
+ */
+class PayConfig extends Common
+{
+
+    protected $noNeedRight = ['select'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new PayConfigModel;
+    }
+
+
+    /**
+     * 支付配置列表
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $payConfigs = $this->model->sheepFilter()->paginate($this->request->param('list_rows', 10));
+
+        $this->success('获取成功', null, $payConfigs);
+    }
+
+
+
+
+    /**
+     * 添加支付配置
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only([
+            'name', 'type', 'params', 'status'
+        ]);
+        $this->svalidate($params, ".add");
+        $this->svalidate($params['params'], '.' . $params['type']);     // 验证对应的支付参数是否设置完整
+
+        $this->model->save($params);
+        
+        $this->success('保存成功');
+    }
+
+
+    /**
+     * 支付配置详情
+     *
+     * @param  $id
+     */
+    public function detail($id)
+    {
+        $payConfig = $this->model->where('id', $id)->find();
+        if (!$payConfig) {
+            $this->error(__('No Results were found'));
+        }
+
+        $payConfig->append(['params']);
+        $this->success('获取成功', null, pay_config_show($payConfig));
+    }
+
+
+
+    /**
+     * 修改支付配置
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->only([
+            'name', 'params', 'status'
+        ]);
+        $this->svalidate($params);
+
+        $payConfig = $this->model->where('id', $id)->find();
+        if (!$payConfig) {
+            $this->error(__('No Results were found'));
+        }
+
+        if (isset($params['params'])) {
+            $this->svalidate($params['params'], '.' . $payConfig['type']);     // 验证对应的支付参数是否设置完整
+        }
+
+        $payConfig->save($params);
+        $this->success('更新成功');
+    }
+
+
+
+    /**
+     * 删除支付配置
+     *
+     * @param string $id 要删除的商品分类列表
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list) {
+            $count = 0;
+            foreach ($list as $item) {
+                $count += $item->delete();
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('删除成功', null, $result);
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+
+
+    /**
+     * 获取所有支付配置
+     */
+    public function select()
+    {
+        $payConfigs = $this->model->sheepFilter()->normal()
+            ->field('id, name, type,status')
+            ->select();
+
+        $this->success('获取成功', null, $payConfigs);
+    }
+
+
+
+    public function recyclebin()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $goods = $this->model->onlyTrashed()->sheepFilter()->paginate($this->request->param('list_rows', 10));
+        $this->success('获取成功', null, $goods);
+    }
+
+
+    /**
+     * 还原(支持批量)
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function restore($id = null)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $items = $this->model->onlyTrashed()->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($items) {
+            $count = 0;
+            foreach ($items as $item) {
+                $count += $item->restore();
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('还原成功', null, $result);
+        } else {
+            $this->error(__('No rows were updated'));
+        }
+    }
+
+
+    /**
+     * 销毁(支持批量)
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function destroy($id = null)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        if ($id !== 'all') {
+            $items = $this->model->onlyTrashed()->whereIn('id', $id)->select();
+        } else {
+            $items = $this->model->onlyTrashed()->select();
+        }
+        $result = Db::transaction(function () use ($items) {
+            $count = 0;
+            foreach ($items as $config) {
+                // 删除商品
+                $count += $config->delete(true);
+            }
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('销毁成功', null, $result);
+        }
+        $this->error('销毁失败');
+    }
+}

+ 30 - 0
addons/shopro/application/admin/controller/shopro/Share.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace app\admin\controller\shopro;
+
+use app\admin\model\shopro\Share as ShareModel;
+
+class Share extends Common
+{
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new ShareModel();
+    }
+
+    /**
+     * 查看用户分享记录
+     */
+    public function index()
+    {
+        $share_id = $this->request->param('id');
+
+        $data = ShareModel::with(['user' => function ($query) {
+            return $query->field(['id', 'nickname', 'avatar']);
+        }])->where('share_id', $share_id)->sheepFilter()->paginate($this->request->param('list_rows', 8));
+
+        $this->success('', null, $data);
+    }
+}

+ 126 - 0
addons/shopro/application/admin/controller/shopro/Withdraw.php

@@ -0,0 +1,126 @@
+<?php
+
+namespace app\admin\controller\shopro;
+
+use think\Db;
+use think\exception\HttpResponseException;
+use addons\shopro\exception\ShoproException;
+use app\admin\model\shopro\Withdraw as WithdrawModel;
+use app\admin\model\shopro\WithdrawLog as WithdrawLogModel;
+use app\admin\model\shopro\user\User as UserModel;
+use app\admin\model\Admin as AdminModel;
+use addons\shopro\service\Withdraw as WithdrawLibrary;
+use addons\shopro\library\Operator;
+
+/**
+ * 提现
+ */
+class Withdraw extends Common
+{
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new WithdrawModel;
+        $this->logModel = new WithdrawLogModel;
+    }
+
+
+
+    /**
+     * 提现列表
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $withdraws = $this->model->sheepFilter()->with(['user'])->paginate($this->request->param('list_rows', 10));
+
+        $this->success('获取成功', null, $withdraws);
+    }
+
+
+    /**
+     * 提现日志
+     */
+    public function log($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $logs = $this->logModel->where('withdraw_id', $id)->order('id desc')->select();
+        $morphs = [
+            'user' => UserModel::class,
+            'admin' => AdminModel::class,
+            'system' => AdminModel::class
+        ];
+        $logs = morph_to($logs, $morphs, ['oper_type', 'oper_id']);
+        $logs = $logs->toArray();
+
+        // 解析操作人信息
+        foreach ($logs as &$log) {
+            $log['oper'] = Operator::info($log['oper_type'], $log['oper'] ?? null);
+        }
+        $this->success('获取成功', null, $logs);
+    }
+
+
+    public function handle($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->param();
+        $action = $params['action'] ?? null;
+        $refuse_msg = $params['refuse_msg'] ?? '';
+        if ($action == 'refuse' && !$refuse_msg) {
+            $this->error('请输入拒绝原因');
+        }
+
+        $ids = is_array($id) ? $id : explode(',', $id);
+        foreach ($ids as $key => $id) {
+            Db::startTrans();
+            try {
+                $withdraw = $this->model->lock(true)->where('id', $id)->find();
+                if (!$withdraw) {
+                    $this->error(__('No Results were found'));
+                }
+                $withdrawLib = new WithdrawLibrary($withdraw->user_id);
+
+                switch ($action) {
+                    case 'agree':
+                        $withdraw = $withdrawLib->handleAgree($withdraw);
+                        break;
+                    case 'agree&withdraw':
+                        $withdraw = $withdrawLib->handleAgree($withdraw);
+                        $withdraw = $withdrawLib->handleWithdraw($withdraw);
+                        break;
+                    case 'withdraw':
+                        $withdraw = $withdrawLib->handleWithdraw($withdraw);
+                        break;
+                    case 'refuse':
+                        $withdraw = $withdrawLib->handleRefuse($withdraw, $refuse_msg);
+                        break;
+                }
+
+                Db::commit();
+            } catch (ShoproException $e) {
+                Db::commit();       // 不回滚,记录错误日志
+                $this->error($e->getMessage());
+            } catch (HttpResponseException $e) {
+                $data = $e->getResponse()->getData();
+                $message = $data ? ($data['msg'] ?? '') : $e->getMessage();
+                $this->error($message);
+            } catch (\Exception $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+        }
+
+        $this->success('处理成功');
+    }
+}

+ 321 - 0
addons/shopro/application/admin/controller/shopro/activity/Activity.php

@@ -0,0 +1,321 @@
+<?php
+
+namespace app\admin\controller\shopro\activity;
+
+use think\Db;
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\activity\Activity as ActivityModel;
+use app\admin\model\shopro\goods\Goods as GoodsModel;
+use app\admin\model\shopro\goods\Sku as SkuModel;
+use app\admin\model\shopro\goods\SkuPrice as SkuPriceModel;
+use addons\shopro\library\activity\Activity as ActivityManager;
+use addons\shopro\library\activity\traits\CheckActivity;
+use addons\shopro\facade\Activity as ActivityFacade;
+
+/**
+ * 活动管理
+ */
+class Activity extends Common
+{
+
+    use CheckActivity;
+
+    protected $noNeedRight = ['getType', 'select', 'skus'];
+    
+    protected $manager = null;
+
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new ActivityModel;
+        $this->manager = ActivityFacade::instance();
+    }
+
+
+    /**
+     * 查看
+     *
+     * @return string|Json
+     * @throws \think\Exception
+     * @throws DbException
+     */
+    public function index()
+    {
+        $type = $this->request->param('type', null);
+
+        if (!$this->request->isAjax()) {
+            if ($type) {
+                return $this->view->fetch('shopro/activity/activity/index');
+            }
+
+            return $this->view->fetch('shopro/activity/activity/activity');
+        }
+
+        $activities = $this->model->sheepFilter()->where('type', $type)->paginate(request()->param('list_rows', 10))->toArray();
+
+        $items = $activities['data'];
+
+        // 关联活动的商品
+        $goodsIds = array_values(array_filter(array_column($items, 'goods_ids')));
+        $goodsIdsArr = [];
+        foreach ($goodsIds as $ids) {
+            $idsArr = explode(',', $ids);
+            $goodsIdsArr = array_merge($goodsIdsArr, $idsArr);
+        }
+        $goodsIdsArr = array_values(array_filter(array_unique($goodsIdsArr)));
+        if ($goodsIdsArr) {
+            // 查询商品
+            $goods = GoodsModel::where('id', 'in', $goodsIdsArr)->select();
+            $goods = array_column($goods, null, 'id');
+        }
+        foreach ($items as $key => $activity) {
+            $items[$key]['goods'] = [];
+            if ($activity['goods_ids']) {
+                $idsArr = explode(',', $activity['goods_ids']);
+                foreach ($idsArr as $id) {
+                    if (isset($goods[$id])) {
+                        $items[$key]['goods'][] = $goods[$id];
+                    }
+                }
+            }
+        }
+
+        $activities['data'] = $items;
+
+        $this->success('获取成功', null, $activities);
+    }
+
+
+    // 获取数据类型
+    public function getType()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $typeList = $this->model->typeList();
+
+        $result = [
+            'type_list' => $typeList,
+        ];
+
+        $data = [];
+        foreach ($result as $key => $list) {
+            $data[$key][] = ['name' => '全部', 'type' => 'all'];
+
+            foreach ($list as $k => $v) {
+                $data[$key][] = [
+                    'name' => $v,
+                    'type' => $k
+                ];
+            }
+        }
+
+        $this->success('获取成功', null, $data);
+    }
+
+
+
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only([
+            'title', 'goods_ids', 'type', 'prehead_time', 'start_time', 'end_time',
+            'rules', 'richtext_id', 'richtext_title', 'goods_list'
+        ]);
+        if (isset($params['goods_list'])) {
+            $params['goods_list'] = json_decode($params['goods_list'], true);
+        }
+        $this->svalidate($params, ".add");
+
+        Db::transaction(function () use ($params) {
+            $this->manager->save($params);
+        });
+
+        $this->success('保存成功');
+    }
+
+
+
+
+    /**
+     * 详情
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function detail($id)
+    {
+        $activity = $this->model->where('id', $id)->find();
+        if (!$activity) {
+            $this->error(__('No Results were found'));
+        }
+        $activity->goods_list = $activity->goods_list;
+        $activity->rules = $activity->rules;
+
+        $this->success('获取成功', null, $activity);
+    }
+
+
+    /**
+     * 编辑(支持批量)
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->only([
+            'title', 'goods_ids', 'prehead_time', 'start_time', 'end_time',
+            'rules', 'richtext_id', 'richtext_title', 'goods_list'
+        ]);
+        if (isset($params['goods_list'])) {
+            $params['goods_list'] = json_decode($params['goods_list'], true);
+        }
+        $this->svalidate($params);
+
+        $id = explode(',', $id);
+        $items = $this->model->whereIn('id', $id)->select();
+
+        Db::transaction(function () use ($items, $params) {
+            foreach ($items as $activity) {
+                $this->manager->update($activity, $params);
+            }
+        });
+
+        $this->success('更新成功');
+    }
+
+
+    /**
+     * 获取活动商品规格并且初始化
+     */
+    public function skus()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->param();
+        $id = $params['id'] ?? 0;
+        $goods_id = $params['goods_id'] ?? 0;
+        $activity_type = $params['activity_type'] ?? '';
+        $start_time = $params['start_time'] ?? '';
+        $end_time = $params['end_time'] ?? '';
+        $prehead_time = $params['prehead_time'] ?? '';
+
+        if ($start_time && $end_time && $activity_type) {
+            // 如果存在开始结束时间,并且是要修改
+            $goodsList = [$goods_id => ['id' => $goods_id]];
+
+            $this->checkActivityConflict([
+                'type' => $activity_type,
+                'classify' => $this->model->getClassify($activity_type),
+                'start_time' => $start_time,
+                'end_time' => $end_time,
+                'prehead_time' => $prehead_time
+            ], $goodsList, $id);
+        }
+
+        // 商品规格
+        $skus = SkuModel::with('children')->where('goods_id', $goods_id)->where('parent_id', 0)->select();
+
+        // 获取规格
+        $skuPrices = SkuPriceModel::with(['activity_sku_price' => function ($query) use ($id) {
+            $query->where('activity_id', $id);
+        }])->where('goods_id', $goods_id)->select();
+
+
+        //编辑
+        $activitySkuPrices = [];
+        foreach ($skuPrices as $k => $skuPrice) {
+            $activitySkuPrices[$k] = $skuPrice->activity_sku_price ? $skuPrice->activity_sku_price->toArray() : [];
+            // 活动规格数据初始化
+            if (!$activitySkuPrices[$k]) {
+                $activitySkuPrices[$k]['id'] = 0;
+                $activitySkuPrices[$k]['status'] = 'down';
+                $activitySkuPrices[$k]['price'] = '';
+                $activitySkuPrices[$k]['stock'] = '';
+                $activitySkuPrices[$k]['sales'] = '0';
+                $activitySkuPrices[$k]['goods_sku_price_id'] = $skuPrice->id;
+            }
+
+            // 个性化初始化每个活动的 规格 字段
+            $activitySkuPrices[$k] = $this->manager->showSkuPrice($activity_type, $activitySkuPrices[$k]);
+        }
+
+        $this->success('获取成功', null, [
+            'skus' => $skus,
+            'sku_prices' => $skuPrices,
+            'activity_sku_prices' => $activitySkuPrices,
+        ]);
+    }
+
+
+
+    public function select()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $type = $this->request->param('type');
+        $activities = $this->model->sheepFilter()->whereIn('type', $type)->paginate(request()->param('list_rows', 10))->toArray();
+
+        $this->success('获取成功', null, $activities);
+    }
+
+
+    /**
+     * 删除(支持批量)
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $list = $this->model->where('id', 'in', $id)->select();
+
+        $result = Db::transaction(function () use ($list) {
+            $count = 0;
+            foreach ($list as $item) {
+                $count += $this->manager->delete($item);
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('删除成功', null, $result);
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+
+
+    public function recyclebin()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $type = $this->request->param('type');
+        $activities = $this->model->onlyTrashed()->sheepFilter()->where('type', $type)->paginate($this->request->param('list_rows', 10));
+
+        $this->success('获取成功', null, $activities);
+    }
+}

+ 124 - 0
addons/shopro/application/admin/controller/shopro/activity/Groupon.php

@@ -0,0 +1,124 @@
+<?php
+
+declare(strict_types=1);
+
+namespace app\admin\controller\shopro\activity;
+
+use think\Db;
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\activity\Activity as ActivityModel;
+use app\admin\model\shopro\activity\Groupon as GrouponModel;
+use app\admin\model\shopro\Admin;
+use addons\shopro\library\activity\traits\Groupon as GrouponTrait;
+
+/**
+ * 团管理
+ */
+class Groupon extends Common
+{
+    use GrouponTrait;
+
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new GrouponModel;
+    }
+
+
+
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $groupons = $this->model->sheepFilter()->with(['goods', 'user', 'grouponLogs'])
+            ->paginate(request()->param('list_rows', 10));
+
+        $this->success('获取成功', null, $groupons);
+    }
+
+
+
+    /**
+     * 团详情
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function detail($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $groupon = $this->model->with(['goods', 'user', 'grouponLogs'])->where('id', $id)->find();
+        if (!$groupon) {
+            $this->error(__('No Results were found'));
+        }
+
+        $this->success('获取成功', null, $groupon);
+    }
+
+
+    /**
+     * 添加虚拟用户(人满自动成团)
+     *
+     * @param Request $request
+     * @param integer $id
+     */
+    public function addUser($id)
+    {
+        $groupon = $this->model->where('id', $id)->find();
+        if (!$groupon) {
+            $this->error(__('No Results were found'));
+        }
+
+        $activity = ActivityModel::where('id', $groupon['activity_id'])->find();
+        if (!$activity) {
+            $this->error('活动不存在');
+        }
+        if ($groupon['status'] != 'ing' || $groupon['current_num'] > $groupon['num']) {
+            $this->error('团已完成或已失效');
+        }
+
+        $avatar = $this->request->param('avatar', '');
+        $nickname = $this->request->param('nickname', '');
+        $user = ['avatar' => $avatar, 'nickname' => $nickname];
+
+        Db::transaction(function () use ($activity, $groupon, $user) {
+            // 增加人数
+            $this->finishFictitiousGroupon($activity, $groupon, false, 1, [$user]);
+        });
+
+        $this->success('操作成功');
+    }
+
+
+
+    /**
+     * 解散团,自动退款
+     *
+     * @param Request $request
+     * @param integer $id
+     */
+    public function invalid($id)
+    {
+        $admin = $this->auth->getUserInfo();
+        $admin = Admin::find($admin['id']);
+        $groupon = $this->model->where('id', $id)->find();
+
+        if ($groupon['status'] != 'ing') {
+            $this->error('团已完成或已失效');
+        }
+
+        Db::transaction(function () use ($groupon, $admin) {
+            // 解散团,并退款
+            $this->invalidRefundGroupon($groupon, $admin);
+        });
+
+        $this->success('操作成功');
+    }
+}

+ 323 - 0
addons/shopro/application/admin/controller/shopro/app/ScoreShop.php

@@ -0,0 +1,323 @@
+<?php
+
+namespace app\admin\controller\shopro\app;
+
+use think\Db;
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\app\ScoreSkuPrice;
+use app\admin\model\shopro\goods\Goods as GoodsModel;
+use app\admin\model\shopro\goods\Sku as SkuModel;
+use app\admin\model\shopro\goods\SkuPrice as SkuPriceModel;
+
+/**
+ * 积分商城
+ */
+class ScoreShop extends Common
+{
+
+    protected $noNeedRight = ['skuPrices', 'select', 'skus'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new ScoreSkuPrice;
+        $this->goodsModel = new GoodsModel();
+    }
+
+
+    /**
+     * 积分商城商品列表
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $scoreGoodsIds = $this->model->group('goods_id')->column('goods_id');
+
+        $scoreGoods = $this->goodsModel->sheepFilter()->with(['score_sku_prices'])
+            ->whereIn('id', $scoreGoodsIds)
+            ->paginate($this->request->param('list_rows', 10))->each(function($goods) {
+                $goods->score_price = $goods->score_price;
+                $goods->score_sales = $goods->score_sales;
+                $goods->score_stock = $goods->score_stock;
+            });
+
+        $this->success('获取成功', null, $scoreGoods);
+    }
+
+
+    /**
+     * skuPrices列表
+     */
+    public function skuPrices($goods_id)
+    {
+        $skuPrices = $this->model->up()->with(['sku_price'])->where('goods_id', $goods_id)->select();
+        $skuPrices = collection($skuPrices)->each(function ($skuPrice) {
+            $skuPrice->goods_sku_ids = $skuPrice->goods_sku_ids;
+            $skuPrice->goods_sku_text = $skuPrice->goods_sku_text;
+            $skuPrice->image = $skuPrice->image;
+            $skuPrice->score_price = $skuPrice->score_price;
+        });
+
+        $this->success('获取成功', null, $skuPrices);
+    }
+
+
+
+    /**
+     * 添加积分商城商品
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only([
+            'goods_id', 'sku_prices'
+        ]);
+        $this->svalidate($params, ".add");
+
+        // 检查是否已经是积分商品了
+        $count = $this->model->where('goods_id', $params['goods_id'])->count();
+        if ($count) {
+            error_stop('该商品已经是积分商城商品了');
+        }
+
+        $statuses = array_column($params['sku_prices'], 'status');
+        if (!in_array('up', $statuses)) {
+            error_stop('请至少选择一个规格');
+        }
+
+        Db::transaction(function () use ($params) {
+            $this->editSkuPrices($params['goods_id'], $params);
+        });
+        $this->success('保存成功');
+    }
+
+
+    public function skus($goods_id)
+    {
+        $skus = SkuModel::with('children')->where('goods_id', $goods_id)->where('parent_id', 0)->select();
+        $skuPrices = SkuPriceModel::with(['score_sku_price'])->where('goods_id', $goods_id)->select();
+
+        //编辑
+        $scoreSkuPrices = [];
+        foreach ($skuPrices as $k => $skuPrice) {
+            $scoreSkuPrices[$k] = $skuPrice->score_sku_price ? : [];
+            // 活动规格数据初始化
+            if (!$scoreSkuPrices[$k]) {
+                $scoreSkuPrices[$k]['id'] = 0;
+                $scoreSkuPrices[$k]['status'] = 'down';
+                $scoreSkuPrices[$k]['price'] = '';
+                $scoreSkuPrices[$k]['score'] = '';
+                $scoreSkuPrices[$k]['stock'] = '';
+                $scoreSkuPrices[$k]['goods_sku_price_id'] = $skuPrice->id;
+            }
+        }
+
+        $this->success('获取成功', null, [
+            'skus' => $skus,
+            'sku_prices' => $skuPrices,
+            'score_sku_prices' => $scoreSkuPrices
+        ]);
+    }
+
+
+    /**
+     * 编辑积分商城商品
+     */
+    public function edit($goods_id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->only([
+            'sku_prices'
+        ]);
+        $this->svalidate($params, ".edit");
+
+        $statuses = array_column($params['sku_prices'], 'status');
+        if (!in_array('up', $statuses)) {
+            error_stop('请至少选择一个规格');
+        }
+
+        Db::transaction(function () use ($goods_id, $params) {
+            $this->editSkuPrices($goods_id, $params);
+        });
+        $this->success('更新成功');
+    }
+
+
+    public function select()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $type = $this->request->param('type', 'page');
+        $scoreGoodsIds = $this->model->group('goods_id')->column('goods_id');
+        
+        $scoreGoods = $this->goodsModel->sheepFilter()->with(['score_sku_prices'])
+            ->whereIn('id', $scoreGoodsIds);
+        
+        if ($type == 'select') {
+            // 普通结果
+            $scoreGoods = $scoreGoods->select();
+            $scoreGoods = collection($scoreGoods);
+        } else {
+            // 分页结果
+            $scoreGoods = $scoreGoods->paginate($this->request->param('list_rows', 10));
+        }
+
+        $scoreGoods = $scoreGoods->each(function ($goods) {
+            $goods->score_price = $goods->score_price;
+            $goods->score_sales = $goods->score_sales;
+            $goods->score_stock = $goods->score_stock;
+        });
+
+        $this->success('获取成功', null, $scoreGoods);
+    }
+
+
+
+    /**
+     * 删除积分商城商品
+     *
+     * @param string $id 要删除的积分商城商品 id
+     * @return void
+     */
+    public function delete($goods_id)
+    {
+        if (empty($goods_id)) {
+            $this->error(__('Parameter %s can not be empty', 'goods_id'));
+        }
+
+        $goodsIds = explode(',', $goods_id);
+        $list = $this->model->whereIn('goods_id', $goodsIds)->select();
+        $result = Db::transaction(function () use ($list) {
+            $count = 0;
+            foreach ($list as $item) {
+                $count += $item->delete();
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('删除成功', null, $result);
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+
+
+
+    public function recyclebin()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $scoreGoodsIds = $this->model->onlyTrashed()->group('goods_id')->column('goods_id');
+        $scoreGoods = $this->goodsModel->sheepFilter()->with(['del_score_sku_prices'])
+            ->whereIn('id', $scoreGoodsIds)
+            ->paginate($this->request->param('list_rows', 10))->each(function ($skuPrice) {
+                $deleteTimes = collection($skuPrice->del_score_sku_prices)->column('deletetime');
+                $skuPrice->deletetime = $deleteTimes ? max($deleteTimes) : null;     // 取用积分规格的删除时间
+            });
+
+        $this->success('获取成功', null, $scoreGoods);
+    }
+
+
+    /**
+     * 还原(支持批量)
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function restore($id = null)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'goods_id'));
+        }
+        
+        $goodsIds = explode(',', $id);
+        Db::transaction(function () use ($goodsIds) {
+            foreach ($goodsIds as $goods_id) {
+                $count = $this->model->where('goods_id', $goods_id)->count();
+                if ($count) {
+                    error_stop('商品 ID 为 ' . $goods_id . ' 的商品已经是积分商品了,不可还原');
+                }
+
+                $list = $this->model->onlyTrashed()->whereIn('goods_id', $goods_id)->select();
+                foreach ($list as $goods) {
+                    $goods->restore();
+                }
+            }
+        });
+
+        $this->success('还原成功');
+    }
+
+
+    /**
+     * 销毁(支持批量)
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function destroy($id = null)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'goods_id'));
+        }
+
+        $goodsIds = explode(',', $id);
+        Db::transaction(function () use ($goodsIds) {
+            if (!in_array('all', $goodsIds)) {
+                foreach ($goodsIds as $goods_id) {
+                    $list = $this->model->onlyTrashed()->whereIn('goods_id', $goods_id)->select();
+                    foreach ($list as $goods) {
+                        $goods->delete(true);
+                    }
+                }
+            } else {
+                $list = $this->model->onlyTrashed()->select();
+                foreach ($list as $goods) {
+                    $goods->delete(true);
+                }
+            }
+        });
+
+        $this->success('销毁成功');
+    }
+
+
+    private function editSkuPrices($goods_id, $params) 
+    {
+        //下架全部规格
+        $this->model->where('goods_id', $goods_id)->update(['status' => 'down']);
+
+        foreach ($params['sku_prices'] as $key => $skuPrice) {
+            if ($skuPrice['id'] == 0) {
+                unset($skuPrice['id']);
+            }
+            unset($skuPrice['sales']);  //不更新销量
+            unset($skuPrice['createtime'], $skuPrice['updatetime'], $skuPrice['deletetime']);  // 不手动更新时间
+            $skuPrice['goods_id'] = $goods_id;
+
+            $model = new ScoreSkuPrice;
+            if (isset($skuPrice['id'])) {
+                $model = $this->model->find($skuPrice['id']);
+                $model = $model ? : new ScoreSkuPrice;
+            }
+
+            $model->save($skuPrice);
+        }
+    }
+}

+ 163 - 0
addons/shopro/application/admin/controller/shopro/app/mplive/Goods.php

@@ -0,0 +1,163 @@
+<?php
+
+namespace app\admin\controller\shopro\app\mplive;
+
+use app\admin\model\shopro\app\mplive\Goods as MpliveGoodsModel;
+
+/**
+ * 小程序直播商品管理
+ */
+class Goods extends Index
+{
+
+    // 直播间商品
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $model = new MpliveGoodsModel();
+        $list = $model->sheepFilter()->paginate($this->request->param('list_rows', 10));
+
+        // 批量更新直播商品状态
+        // $this->updateAuditStatusByGoods($list);
+
+        $this->success('获取成功', null, $list);
+    }
+
+    // 直播间商品详情
+    public function detail($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $goods = (new MpliveGoodsModel)->findOrFail($id);
+
+        $this->success('', null, $goods);
+    }
+
+    // 创建直播间商品并提交审核
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->param();
+
+        $data = [
+            "coverImgUrl" => $this->uploadMedia($params['cover_img_url']),
+            "name" => $params['name'],
+            "priceType" => $params['price_type'],
+            "price" => $params['price'],
+            "price2" => $params['price_type'] === 1 ? "" : $params['price2'],   // priceType为2或3时必填
+            "url" => $params['url'],
+        ];
+
+        $res = $this->wechat->broadcast->create($data);
+
+        $this->catchLiveError($res);
+
+        $params['id'] = $res['goodsId'];
+        $params['audit_id'] = $res['auditId'];
+        $params['audit_status'] = 1;
+
+        (new MpliveGoodsModel)->save($params);
+
+        $this->success("操作成功");
+    }
+
+    // 直播商品审核
+    public function audit($id)
+    {
+        $id = intval($id);
+        $act = $this->request->param('act');
+
+        $goods = MpliveGoodsModel::where('id', $id)->find();
+        if (!$goods) {
+            error_stop('未找到该商品');
+        }
+        // 撤回审核
+        if ($act === 'reset') {
+            $auditId = $goods->audit_id;
+            if ($auditId) {
+                $res = $this->wechat->broadcast->resetAudit($auditId, $id);
+                $this->catchLiveError($res);
+            }
+        }
+
+        // 重新审核
+        if ($act === 'resubmit') {
+            $res = $this->wechat->broadcast->resubmitAudit($id);
+            $this->catchLiveError($res);
+            $goods->audit_id = $res['auditId'];
+            $goods->save();
+        }
+
+        return $this->status($id);
+    }
+
+    // 删除直播商品
+    public function delete($id)
+    {
+        $id = intval($id);
+        $res = $this->wechat->broadcast->delete($id);
+
+        $this->catchLiveError($res);
+
+        MpliveGoodsModel::where('id', $id)->delete();
+        $this->success('操作成功');
+    }
+
+    // 更新直播商品
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->param();
+
+        $data = [
+            'goodsId' => $id,
+            "coverImgUrl" => $this->uploadMedia($params['cover_img_url']),
+            "name" => $params['name'],
+            "priceType" => $params['price_type'],
+            "price" => $params['price'],
+            "price2" => $params['price_type'] === 1 ? "" : $params['price2'],   // priceType为2或3时必填
+            "url" => $params['url'],
+        ];
+
+        $res = $this->wechat->broadcast->update($data);
+
+        $this->catchLiveError($res);
+
+        $goods = MpliveGoodsModel::where('id', $id)->find();
+        $goods->save($data);
+
+        $this->success('操作成功');
+    }
+
+    // 更新直播商品状态
+    public function status($id)
+    {
+        $res = $this->wechat->broadcast->getGoodsWarehouse([$id]);
+
+        $this->catchLiveError($res);
+
+        $list = $res['goods'];
+
+        foreach ($list as $key => $goods) {
+            $mpliveGoods = MpliveGoodsModel::where('id', $goods['goods_id'])->find();
+            if ($mpliveGoods) {
+                $mpliveGoods->audit_status = $goods['audit_status'];
+                $mpliveGoods->third_party_tag = $goods['third_party_tag'];
+                $mpliveGoods->save();
+            }
+        }
+
+        $this->success('操作成功');
+    }
+}

+ 81 - 0
addons/shopro/application/admin/controller/shopro/app/mplive/Index.php

@@ -0,0 +1,81 @@
+<?php
+
+namespace app\admin\controller\shopro\app\mplive;
+
+use fast\Http;
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\app\mplive\Room as MpliveRoomModel;
+use addons\shopro\library\mplive\ServiceProvider;
+use addons\shopro\facade\Wechat;
+
+/**
+ * 小程序直播
+ */
+class Index extends Common
+{
+    protected $wechat;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->wechat = Wechat::miniProgram();
+
+        (new ServiceProvider())->register($this->wechat);
+    }
+
+    // 上传临时素材
+    protected function uploadMedia($path)
+    {
+        $filesystem = config('filesystem.default');
+        if ($filesystem !== 'local' || is_url($path)) {
+            $body = Http::get(cdnurl($path, true));
+            $dir = RUNTIME_PATH . 'storage' . DS . 'temp';
+            if (!is_dir($dir)) {
+                @mkdir($dir, 0755, true);
+            }
+            $temp_path = $dir . $this->getBaseName($path);
+            file_put_contents($temp_path, $body);
+        } else {
+            $temp_path = ROOT_PATH . 'public' . $path;
+        }
+
+        if (!isset($temp_path) || empty($temp_path)) {
+            error_stop("上传失败,不是一个有效的文件: " . $path);
+        }
+
+        $media = $this->wechat->media;
+        $res = $media->uploadImage($temp_path);
+        @unlink($temp_path);        // 删除临时文件
+        if (isset($res['media_id'])) {
+            return $res['media_id'];
+        }
+        return '';
+    }
+
+    // 解析图片文件名
+    private function getBaseName($path)
+    {
+        if (strpos($path, 'mmbiz.qpic.cn') !== false) {
+            return DS . gen_random_str(8) . '.jpg';
+        }
+
+        return basename($path);
+    }
+
+    // 转义直播错误信息
+    protected function catchLiveError($response)
+    {
+        if (!isset($response['errcode'])) {
+            error_stop("未知错误");
+        }
+
+        $errorMap = MpliveRoomModel::ERR_CODE;
+        if (isset($response['errcode']) && ($response['errcode'] !== 0 && $response['errcode'] !== 1001)) {
+            if (isset($errorMap[$response['errcode']])) {
+                error_stop("{$errorMap[$response['errcode']]} [错误码: {$response['errcode']}]");
+            } else {
+                error_stop("{$response['errmsg']} [错误码: {$response['errcode']}]");
+            }
+        }
+    }
+}

+ 201 - 0
addons/shopro/application/admin/controller/shopro/app/mplive/Room.php

@@ -0,0 +1,201 @@
+<?php
+
+namespace app\admin\controller\shopro\app\mplive;
+
+use app\admin\model\shopro\app\mplive\Room as MpliveRoomModel;
+
+/**
+ * 小程序直播
+ */
+class Room extends Index
+{
+
+    protected $noNeedRight = ['select'];
+
+    // 直播间列表
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $list = (new MpliveRoomModel)->sheepFilter()->select();
+
+        $this->success('', null, $list);
+    }
+
+    // 直播间详情
+    public function detail($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $room = (new MpliveRoomModel)->where('roomid', $id)->findOrFail();
+
+        $this->success('', null, $room);
+    }
+
+    // 同步直播间列表
+    public function sync()
+    {
+        $res = $this->wechat->broadcast->getRooms();
+        $data = [];
+
+        $this->catchLiveError($res);
+
+        MpliveRoomModel::where('roomid', '>', 0)->delete();
+        foreach ($res['room_info'] as $room) {
+            $room['status'] = $room['live_status'];
+            $room['type'] = $room['live_type'];
+            $data[] = $room;
+        }
+
+        MpliveRoomModel::strict(false)->insertAll($data);
+        $list = MpliveRoomModel::select();
+
+        $this->success('', null, $list);
+    }
+
+    // 创建直播间
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->param();
+
+        $data = [
+            'name' => $params['name'],  // 房间名字
+            'coverImg' => $this->uploadMedia($params['cover_img']),   // 通过 uploadfile 上传,填写 mediaID
+            'shareImg' =>   $this->uploadMedia($params['share_img']),  //通过 uploadfile 上传,填写 mediaID
+            'feedsImg' =>  $this->uploadMedia($params['feeds_img']),   //通过 uploadfile 上传,填写 mediaID
+            'startTime' => $params['start_time'],   // 开始时间
+            'endTime' => $params['end_time'], // 结束时间
+            'anchorName' => $params['anchor_name'],  // 主播昵称
+            'anchorWechat' => $params['anchor_wechat'],  // 主播微信号
+            'subAnchorWechat' => $params['sub_anchor_wechat'],  // 主播副号微信号
+            'isFeedsPublic' => $params['is_feeds_public'], // 是否开启官方收录,1 开启,0 关闭
+            'type' => $params['type'], // 直播类型,1 推流 0 手机直播
+            'closeLike' => $params['close_like'], // 是否关闭点赞 1:关闭
+            'closeGoods' => $params['close_goods'], // 是否关闭商品货架,1:关闭
+            'closeComment' => $params['close_comment'], // 是否开启评论,1:关闭
+            'closeReplay' => $params['close_replay'], // 是否关闭回放 1 关闭
+            'closeKf' => $params['close_kf'], // 是否关闭客服,1 关闭
+        ];
+
+        $res = $this->wechat->broadcast->createLiveRoom($data);
+
+        $this->catchLiveError($res);
+
+        return $this->sync();
+    }
+
+    // 更新直播间
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->param();
+
+        $data = [
+            'id' => $id,
+            'name' => $params['name'],  // 房间名字
+            'coverImg' => $this->uploadMedia($params['cover_img']),   // 通过 uploadfile 上传,填写 mediaID
+            'shareImg' =>   $this->uploadMedia($params['share_img']),  //通过 uploadfile 上传,填写 mediaID
+            'feedsImg' =>  $this->uploadMedia($params['feeds_img']),   //通过 uploadfile 上传,填写 mediaID
+            'startTime' => $params['start_time'],   // 开始时间
+            'endTime' => $params['end_time'], // 结束时间
+            'anchorName' => $params['anchor_name'],  // 主播昵称
+            'anchorWechat' => $params['anchor_wechat'],  // 主播昵称
+            'isFeedsPublic' => $params['is_feeds_public'], // 是否开启官方收录,1 开启,0 关闭
+            'type' => $params['type'], // 直播类型,1 推流 0 手机直播
+            'closeLike' => $params['close_like'], // 是否关闭点赞 1:关闭
+            'closeGoods' => $params['close_goods'], // 是否关闭商品货架,1:关闭
+            'closeComment' => $params['close_comment'], // 是否开启评论,1:关闭
+            'closeReplay' => $params['close_replay'], // 是否关闭回放 1 关闭
+            'closeKf' => $params['close_kf'], // 是否关闭客服,1 关闭
+        ];
+
+        $res = $this->wechat->broadcast->updateLiveRoom($data);
+
+        $this->catchLiveError($res);
+
+        return $this->sync();
+    }
+
+    // 删除直播间
+    public function delete($id)
+    {
+        $res = $this->wechat->broadcast->deleteLiveRoom([
+            'id' => $id,
+        ]);
+
+        $this->catchLiveError($res);
+
+        MpliveRoomModel::where('roomid', $id)->delete();
+        $this->success('操作成功');
+    }
+
+    // 推流地址
+    public function pushUrl($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $res = $this->wechat->broadcast->getPushUrl([
+            'roomId' => $id
+        ]);
+
+        $this->catchLiveError($res);
+
+        $this->success('', null, ['pushAddr' => $res['pushAddr']]);
+    }
+
+    // 分享二维码
+    public function qrcode($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $res = $this->wechat->broadcast->getShareQrcode([
+            'roomId' => $id
+        ]);
+
+        $this->catchLiveError($res);
+
+        $this->success('', null, ['pagePath' => $res['pagePath'], 'cdnUrl' => $res['cdnUrl']]);
+    }
+
+    // 查看回放
+    public function playback($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $res = $this->wechat->broadcast->getPlaybacks((int)$id, (int)$start = 0, (int)$limit = 10);
+
+        $this->catchLiveError($res);
+
+        $data = $res['live_replay'];
+
+        $this->success('', null, $data);
+    }
+
+    public function select()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $list = (new MpliveRoomModel)->sheepFilter()->select();
+
+        $this->success('', null, $list);
+    }
+}

+ 129 - 0
addons/shopro/application/admin/controller/shopro/chat/CommonWord.php

@@ -0,0 +1,129 @@
+<?php
+
+namespace app\admin\controller\shopro\chat;
+
+use think\Db;
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\chat\CommonWord as ChatCommonWord;
+
+class CommonWord extends Common
+{
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new ChatCommonWord;
+    }
+
+    /**
+     * 常用语列表
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $commonWords = $this->model->sheepFilter()->paginate($this->request->param('list_rows', 10));
+        $this->success('获取成功', null, $commonWords);
+    }
+
+
+    /**
+     * 常用语添加
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only(['room_id', 'name', 'status', 'weigh']);
+        $params['content'] = $this->request->param('content', '', null);      // content 不经过全局过滤
+        $this->svalidate($params, ".add");
+
+        $this->model->save($params);
+        $this->success('保存成功', null, $this->model);
+    }
+
+
+
+    /**
+     * 常用语详情
+     *
+     * @param  $id
+     */
+    public function detail($id)
+    {
+        $commonWord = $this->model->where('id', $id)->find();
+        if (!$commonWord) {
+            $this->error(__('No Results were found'));
+        }
+
+        $this->success('获取成功', null, $commonWord);
+    }
+
+
+
+    /**
+     * 常用语编辑
+     *
+     * @param  $id
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->only(['room_id', 'name', 'status', 'weigh']);
+        $this->request->has('content') && $params['content'] = $this->request->param('content', '', null);      // content 不经过全局过滤
+        $this->svalidate($params);
+
+        $id = explode(',', $id);
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list, $params) {
+            $count = 0;
+            foreach ($list as $item) {
+                $params['id'] = $item->id;
+                $count += $item->save($params);
+            }
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('更新成功', null, $result);
+        } else {
+            $this->error('更新失败,未改变任何记录');
+        }
+    }
+
+
+    /**
+     * 删除(支持批量)
+     *
+     * @param  $id
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $id = explode(',', $id);
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list) {
+            $count = 0;
+            foreach ($list as $item) {
+                $count += $item->delete();
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('删除成功', null, $result);
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+}

+ 205 - 0
addons/shopro/application/admin/controller/shopro/chat/CustomerService.php

@@ -0,0 +1,205 @@
+<?php
+
+namespace app\admin\controller\shopro\chat;
+
+use think\Db;
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\chat\CustomerService as ChatCustomerService;
+use app\admin\model\shopro\chat\CustomerServiceUser;
+use app\admin\model\Admin;
+
+class CustomerService extends Common
+{
+
+    protected $noNeedRight = ['select'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new ChatCustomerService;
+        $this->adminModel = new Admin;
+    }
+
+    /**
+     * 客服列表
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $customerService = $this->model->sheepFilter()->with('customer_service_user')->paginate($this->request->param('list_rows', 10));
+        $this->success('获取成功', null, $customerService);
+    }
+
+
+    /**
+     * 客服添加
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only(['name', 'avatar', 'room_id', 'max_num', 'auth', 'auth_id']);
+        $this->svalidate($params, ".add");
+
+        if ($this->checkHasAuthId($params['auth'], $params['auth_id'], $params['room_id'])) {
+            error_stop('该身份已绑定其他客服');
+        }
+
+        $data = Db::transaction(function () use ($params) {
+            $this->model->allowField(true)->save($params);
+
+            $customerServiceUser = CustomerServiceUser::create([
+                'customer_service_id' => $this->model->id,
+                'auth' => $params['auth'],
+                'auth_id' => $params['auth_id'],
+            ]);
+
+            return $customerServiceUser;
+        });
+        $this->success('保存成功', null, $data);
+    }
+
+
+
+    /**
+     * 客服详情
+     *
+     * @param  $id
+     */
+    public function detail($id)
+    {
+        $customerService = $this->model->with(['customer_service_user'])->where('id', $id)->find();
+        if (!$customerService) {
+            $this->error(__('No Results were found'));
+        }
+
+        $this->success('获取成功', null, $customerService);
+    }
+
+
+
+    /**
+     * 客服编辑
+     *
+     * @param  $id
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->only(['name', 'avatar', 'room_id', 'max_num', 'auth', 'auth_id']);
+        $this->svalidate($params);
+
+        $id = explode(',', $id);
+        $list = $this->model->where('id', 'in', $id)->with('customer_service_user')->select();
+        Db::transaction(function () use ($list, $params) {
+            foreach ($list as $customerService) {
+                $customerService->allowField(true)->save($params);
+
+                $customerServiceUser = $customerService['customer_service_user'];
+
+                // 编辑了客服身份所有者
+                if ($params['auth'] != $customerServiceUser['auth'] || $params['auth_id'] != $customerServiceUser['auth_id']) {
+                    // 验证新的身份是否已经被绑定别的客服
+                    if ($this->checkHasAuthId($params['auth'], $params['auth_id'], $params['room_id'])) {
+                        error_stop('该身份已绑定其他客服');
+                    }
+
+                    // 删除老的身份
+                    CustomerServiceUser::{'auth' . ucfirst($customerServiceUser['auth'])}($customerServiceUser['auth_id'])
+                        ->where('customer_service_id', $customerService['id'])->delete();
+
+                    // 添加新的身份
+                    $customerServiceUser = CustomerServiceUser::create([
+                        'customer_service_id' => $customerService->id,
+                        'auth' => $params['auth'],
+                        'auth_id' => $params['auth_id'],
+                    ]);
+                }
+            }
+        });
+
+        $this->success('更新成功');
+    }
+
+
+    /**
+     * 客服用语
+     *
+     * @param  $id
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+        $id = explode(',', $id);
+        $list = $this->model->where('id', 'in', $id)->select();
+        Db::transaction(function () use ($list) {
+            foreach ($list as $customerService) {
+                // 删除客服的身份
+                CustomerServiceUser::where('customer_service_id', $customerService['id'])->delete();
+
+                $customerService->delete();
+            }
+        });
+
+        $this->success('删除成功');
+    }
+
+
+    /**
+     * 检验是否已经被绑定了客服(一个管理员或者用户只能是一种客服)
+     *
+     * @param string $auth
+     * @param integer $auth_id
+     * @return void
+     */
+    private function checkHasAuthId($auth, $auth_id, $room_id)
+    {
+        $customerServiceUser = CustomerServiceUser::{'auth' . ucfirst($auth)}($auth_id)->with('customer_service')->find();
+
+        if ($customerServiceUser) {
+            $customerService = $customerServiceUser['customer_service'];
+            if ($customerService && $customerService['room_id'] == $room_id) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+
+    /**
+     * 获取管理员列表
+     *
+     * @return void
+     */
+    public function select()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $id = $this->request->param('id', 0);
+        $room_id = $this->request->param('room_id', 'admin');
+        
+        // 已经被设置为客服的管理员
+        $adminIds = CustomerServiceUser::whereExists(function ($query) use ($room_id) {
+            $table_name = $this->model->getQuery()->getTable();
+            $query->table($table_name)->where('room_id', $room_id)->where('customer_service_id=id');
+        })->where('auth', 'admin')->where('customer_service_id', '<>', $id)->column('auth_id');
+
+        // 正常的,并且排除了已经设置为客服的管理员
+        $admins = $this->adminModel->where('status', 'normal')->whereNotIn('id', $adminIds)->select();
+
+        $this->success('获取成功', null, $admins);
+    }
+}

+ 38 - 0
addons/shopro/application/admin/controller/shopro/chat/Index.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace app\admin\controller\shopro\chat;
+
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\chat\CustomerService;
+
+class Index extends Common
+{
+
+    protected $noNeedRight = ['init'];
+
+    /**
+     * socket 初始化
+     *
+     * @return void
+     */
+    public function init()
+    {
+        // 当前管理员
+        $admin = auth_admin();
+        $token = $this->getUnifiedToken('admin:' . $admin['id']);    // 统一验证 token
+
+        // 客服配置
+        $chatSystem = sheep_config('chat.system');
+
+        // 初始化 socket ssl 类型, 默认 cert
+        $ssl = $chatSystem['ssl'] ?? 'none';
+        $chat_domain = ($ssl == 'none' ? 'http://' : 'https://') . request()->host(true) . ($ssl == 'reverse_proxy' ? '' : (':' . $chatSystem['port'])) . '/chat';
+
+        $data = [
+            'token' => $token,
+            'chat_domain' => $chat_domain,
+            'default_rooms' => CustomerService::defaultRooms()
+        ];
+        $this->success('获取成功', null, $data);
+    }
+}

+ 133 - 0
addons/shopro/application/admin/controller/shopro/chat/Question.php

@@ -0,0 +1,133 @@
+<?php
+
+namespace app\admin\controller\shopro\chat;
+
+use think\Db;
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\chat\Question as ChatQuestion;
+
+class Question extends Common
+{
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new ChatQuestion;
+    }
+
+    /**
+     * 猜你想问列表
+     *
+     * @return \think\Response
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $questions = $this->model->sheepFilter()->paginate($this->request->param('list_rows', 10));
+        $this->success('获取成功', null, $questions);
+    }
+
+
+    /**
+     * 猜你想问添加
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only(['room_id', 'title', 'status', 'weigh']);
+        $params['content'] = $this->request->param('content', '', null);      // content 不经过全局过滤
+        $this->svalidate($params, ".add");
+
+        $this->model->save($params);
+        $this->success('保存成功', null, $this->model);
+    }
+
+
+
+    /**
+     * 猜你想问详情
+     *
+     * @param  $id
+     */
+    public function detail($id)
+    {
+        $question = $this->model->where('id', $id)->find();
+        if (!$question) {
+            $this->error(__('No Results were found'));
+        }
+
+        $this->success('获取成功', null, $question);
+    }
+
+
+
+    /**
+     * 猜你想问编辑
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->only(['room_id', 'title', 'status', 'weigh']);
+        $this->request->has('content') && $params['content'] = $this->request->param('content', '', null);      // content 不经过全局过滤
+        $this->svalidate($params);
+
+        $id = explode(',', $id);
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list, $params) {
+            $count = 0;
+            foreach ($list as $item) {
+                $params['id'] = $item->id;
+                $count += $item->save($params);
+            }
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('更新成功', null, $result);
+        } else {
+            $this->error('更新失败,未改变任何记录');
+        }
+    }
+
+
+    /**
+     * 删除猜你想问
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $id = explode(',', $id);
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list) {
+            $count = 0;
+            foreach ($list as $item) {
+                $count += $item->delete();
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('删除成功', null, $result);
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+}

+ 37 - 0
addons/shopro/application/admin/controller/shopro/chat/Record.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace app\admin\controller\shopro\chat;
+
+use think\Db;
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\chat\Record as ChatRecord;
+
+class Record extends Common
+{
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new ChatRecord;
+    }
+
+    /**
+     * 聊天列表
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $records = $this->model->sheepFilter()->order('id desc')->paginate($this->request->param('list_rows', 10));
+
+        $morphs = [
+            'customer' => \app\admin\model\shopro\chat\User::class,
+            'customer_service' => \app\admin\model\shopro\chat\CustomerService::class,
+        ];
+        $records = morph_to($records, $morphs, ['sender_identify', 'sender_id']);
+        
+        $this->success('获取成功', null, $records);
+    }
+
+}

+ 62 - 0
addons/shopro/application/admin/controller/shopro/chat/User.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace app\admin\controller\shopro\chat;
+
+use think\Db;
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\chat\User as ChatUser;
+use app\admin\model\shopro\chat\ServiceLog;
+use app\admin\model\shopro\chat\Record;
+
+class User extends Common
+{
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new ChatUser;
+    }
+
+    /**
+     * 会话列表
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $user = $this->model->sheepFilter()->with(['user', 'customer_service'])->where('auth', 'user')->order('id desc')->paginate(request()->param('list_rows', 10));
+        $this->success('获取成功', null, $user);
+    }
+
+
+    /**·
+     * 删除会话
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $id = explode(',', $id);
+        $list = $this->model->where('id', 'in', $id)->select();
+        Db::transaction(function () use ($list) {
+            foreach ($list as $user) {
+                // 删除这个会话的所有服务记录
+                ServiceLog::where('chat_user_id', $user->id)->delete();
+
+                // 删除这个会话的所有聊天记录
+                Record::where('chat_user_id', $user->id)->delete();
+                
+                // 删除这个会话
+                $user->delete();
+            }
+        });
+
+        $this->success('删除成功');
+    }
+}

+ 250 - 0
addons/shopro/application/admin/controller/shopro/commission/Agent.php

@@ -0,0 +1,250 @@
+<?php
+
+namespace app\admin\controller\shopro\commission;
+
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\commission\Agent as AgentModel;
+use app\admin\model\shopro\user\User as UserModel;
+use app\admin\model\shopro\commission\Log as LogModel;
+use app\admin\model\shopro\commission\Level as LevelModel;
+use addons\shopro\service\commission\Agent as AgentService;
+use think\Db;
+
+class Agent extends Common
+{
+    protected $noNeedRight = ['select'];
+
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new AgentModel();
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $list = $this->model->sheepFilter()->with(['user.parent_user', 'level_info', 'level_status_info', 'upgrade_level'])->paginate($this->request->param('list_rows', 10));
+
+        $this->success('分销商列表', null, $list);
+    }
+
+    /**
+     * 详情
+     *
+     * @param  $id
+     */
+    public function detail($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $detail = $this->model->with(['user.parent_user', 'level_info', 'level_status_info', 'upgrade_level'])->where('user_id', $id)->find();
+        if (!$detail) {
+            $this->error(__('No Results were found'));
+        }
+
+        $this->success('分销商详情', null, $detail);
+    }
+
+
+    /**
+     * 团队
+     *
+     * @param  $id
+     */
+    public function team($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $detail = $this->model->with(['user.parent_user', 'level_info'])->where('user_id', $id)->find();
+        if (!$detail) {
+            $this->error(__('No Results were found'));
+        }
+        
+        $detail->agent_team = AgentModel::hasWhere('user', function ($query) use ($detail) {
+            return $query->where('parent_user_id', $detail->user_id);
+        })->with(['user', 'level_info'])->select();
+        $this->success('分销商详情', null, $detail);
+    }
+
+    // 选择分销商
+    public function select()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $data = $this->model->sheepFilter()->with(['user', 'level_info', 'level_status_info', 'upgrade_level'])
+            ->paginate($this->request->param('list_rows', 10));
+        
+        $this->success('选择分销商', null, $data);
+    }
+
+    /**
+     * 编辑
+     *
+     * @param  $id
+     */
+    public function edit($id = null)
+    {
+        $params = $this->request->only(['status', 'upgrade_lock', 'level_status', 'level', 'apply_info']);
+
+        $result = Db::transaction(function () use ($id, $params) {
+            $row = $this->model->with(['user', 'level_info', 'level_status_info', 'upgrade_level'])->where('user_id', $id)->find();
+            if (!$row) {
+                $this->error('未找到该分销商');
+            }
+
+            foreach ($params as $field => $value) {
+                switch ($field) {
+                    case 'status':  // 修改状态
+                        return $this->changeStatus($row, $value);
+                        break;
+                    case 'level_status':    // 审核等级
+                        return $this->changeLevelStatus($row, $value);
+                        break;
+                    case 'level':           // 修改等级
+                        return $this->changeLevel($row, $value);
+                        break;
+                    default:
+                        return $row->save([$field => $value]);
+                }
+            }
+        });
+        if ($result) {
+            $this->success('更新成功', null, $result);
+        } else {
+            $this->error('更新失败');
+        }
+    }
+
+
+    // 修改状态
+    private function changeStatus($row, $value)
+    {
+        $result = $row->save(['status' => $value]);
+        if ($result) {
+            LogModel::add($row->user_id, 'agent', ['type' => 'status', 'value' => $value]);
+            (new AgentService($row->user_id))->createAsyncAgentUpgrade();
+        }
+        return $result;
+    }
+
+    // 审核等级
+    private function changeLevelStatus($row, $value)
+    {
+        if ($row->level_status == 0 && $value > 0) {
+            $this->error('非法操作');
+        }
+
+        if ($value == 0) {  // 拒绝操作
+            return $row->save(['level_status' => 0]);
+        } else {            // 同意操作
+            if ($row->upgrade_level) {
+                $result = $row->save(['level_status' => 0, 'level' => $row->upgrade_level->level]);
+                if ($result) {
+                    LogModel::add($row->user_id, 'agent', ['type' => 'level', 'level' => $row->upgrade_level]);
+                    (new AgentService($row->user_id))->createAsyncAgentUpgrade();
+                }
+                return $result;
+            }
+        }
+        return false;
+    }
+
+    // 修改等级
+    private function changeLevel($row, $value)
+    {
+        $level = LevelModel::find($value);
+        if ($level) {
+            $result = $row->save(['level' => $level->level]);
+            if ($result) {
+                LogModel::add($row->user_id, 'agent', ['type' => 'level', 'level' => $level]);
+                (new AgentService($row->user_id))->createAsyncAgentUpgrade();
+            }
+            return $result;
+        } else {
+            $this->error('未找到该等级');
+        }
+    }
+
+    // 更换推荐人
+    public function changeParentUser($id)
+    {
+        $userAgent = new AgentService($id);
+
+        if (!$userAgent->user) {
+            $this->error('未找到该用户');
+        }
+
+        $parentUserId = $this->request->param('parent_user_id', 0);
+
+        // 更换推荐人检查
+        if ($parentUserId != 0) {
+            $parentAgent = new AgentService($parentUserId);
+            if (!$parentAgent->isAgentAvaliable()) {
+                $this->error('选中用户暂未成为分销商,不能成为推荐人');
+            }
+            if (!$this->checkChangeParentAgent($id, $parentUserId)) {
+                $this->error('不能绑定该上级');
+            }
+            LogModel::add($parentUserId, 'share', ['user' => $userAgent->user]);
+
+            if ($userAgent->isAgentAvaliable()) {
+                LogModel::add($id, 'bind', ['user' => $parentAgent->user ?? NULL]);
+            }
+        }
+
+        $lastParentUserId = $userAgent->user->parent_user_id;
+
+        $userAgent->user->parent_user_id = $parentUserId;
+        $userAgent->user->save();
+
+        if ($lastParentUserId > 0) {
+            $userAgent->createAsyncAgentUpgrade($lastParentUserId);
+        }
+
+        if ($parentUserId > 0) {
+            $userAgent->createAsyncAgentUpgrade($parentUserId);
+        }
+        $this->success('绑定成功');
+    }
+
+    // 递归往上找推荐人,防止出现推荐循环
+    private function checkChangeParentAgent($userId, $parentUserId)
+    {
+        if ($userId == $parentUserId) {
+
+            $this->error('推荐人不能是本人');
+        }
+        if ($parentUserId == 0) {
+            return true;
+        }
+
+        $parentAgent = UserModel::find($parentUserId);
+
+        if ($parentAgent) {
+            if ($parentAgent->parent_user_id == $userId) {
+                $this->error("已选中分销商的上级团队中已存在该用户");
+            }
+            if ($parentAgent->parent_user_id == 0) {
+                return true;
+            } else {
+                return $this->checkChangeParentAgent($userId, $parentAgent->parent_user_id);
+            }
+        }
+
+        return false;
+    }
+}

+ 89 - 0
addons/shopro/application/admin/controller/shopro/commission/Goods.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace app\admin\controller\shopro\commission;
+
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\commission\CommissionGoods as CommissionGoodsModel;
+use app\admin\model\shopro\goods\Goods as GoodsModel;
+use think\Db;
+
+class Goods extends Common
+{
+    protected $model = null;
+    protected $goodsModel;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new CommissionGoodsModel();
+        $this->goodsModel = new GoodsModel();
+    }
+
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $data = $this->goodsModel->sheepFilter()->with('commission_goods')->paginate($this->request->param('list_rows', 10));
+        $this->success('分销商品列表', null, $data);
+    }
+
+    /**
+     * 详情
+     *
+     * @param  $id
+     */
+    public function detail($id)
+    {
+        $goodsList = collection(GoodsModel::with(['commission_goods'])->whereIn('id', $id)->select())->each(function ($goods) {
+            $goods->skus = $goods->skus;
+            $goods->sku_prices = $goods->sku_prices;
+        });
+
+        $config = sheep_config('shop.commission');
+        $this->success('分销商品详情', null, [
+            'goods' => $goodsList,
+            'config' => $config
+        ]);
+    }
+
+    /**
+     * 设置佣金(支持批量)
+     *
+     * @param  $id
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        // 接受全部参数
+        $params = $this->request->only(['status', 'self_rules', 'commission_order_status', 'commission_config', 'commission_rules']);
+
+        $result = Db::transaction(function () use ($id, $params) {
+            $count = 0;
+            $ids = explode(',', $id);
+
+            foreach ($ids as $goods_id) {
+                if ($row = $this->model->get($goods_id)) {
+                    $row->save($params);
+                } else {
+                    $model = new CommissionGoodsModel();
+                    $params['goods_id'] = $goods_id;
+                    $model->save($params);
+                }
+                $count++;
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('更新成功', null, $result);
+        } else {
+            $this->error('更新失败');
+        }
+    }
+}

+ 144 - 0
addons/shopro/application/admin/controller/shopro/commission/Level.php

@@ -0,0 +1,144 @@
+<?php
+
+namespace app\admin\controller\shopro\commission;
+
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\commission\Level as LevelModel;
+use think\Db;
+
+class Level extends Common
+{
+
+    protected $noNeedRight = ['select'];
+
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new LevelModel();
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $defaultLevel = $this->model->find(1);
+        if (!$defaultLevel) {
+            $this->model->save([
+                'name' => '默认等级',
+                'level' => 1,
+                'commission_rules' => [
+                    'commission_1' => '0.00',
+                    'commission_2' => '0.00',
+                    'commission_3' => '0.00'
+                ]
+            ]);
+        }
+        $list = $this->model->sheepFilter()->select();
+
+        $this->success('全部等级', null, $list);
+    }
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only(['name', 'level', 'image', 'commission_rules', 'upgrade_type', 'upgrade_rules']);
+
+        $this->model->save($params);
+
+        $this->success('保存成功', null, $this->model);
+    }
+
+    /**
+     * 编辑
+     *
+     * @param  $id
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->only(['level', 'name', 'image', 'commission_rules', 'upgrade_type', 'upgrade_rules']);
+
+        $result = Db::transaction(function () use ($id, $params) {
+
+            $this->svalidate($params);
+
+            $data = $this->model->where('level', $id)->find();
+            if (!$data) {
+                $this->error(__('No Results were found'));
+            }
+
+            return $data->save($params);
+        });
+
+        if ($result) {
+            $this->success('更新成功', null, $result);
+        } else {
+            $this->error('更新失败');
+        }
+    }
+
+    /**
+     * 详情
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function detail($id)
+    {
+        $detail = $this->model->get($id);
+        if (!$detail) {
+            $this->error(__('No Results were found'));
+        }
+        $this->success('等级详情', null, $detail);
+    }
+
+    /**
+     * 删除
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $result = Db::transaction(function () use ($id) {
+            return $this->model->where('level', $id)->delete();
+        });
+
+        if ($result) {
+            $this->success('删除成功', null, $result);
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+
+    // 选择分销商等级
+    public function select()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $data = $this->model->sheepFilter()->field('level, name, image, commission_rules')->select();
+        $this->success('选择等级', null, $data);
+    }
+}

+ 50 - 0
addons/shopro/application/admin/controller/shopro/commission/Log.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace app\admin\controller\shopro\commission;
+
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\commission\Log as LogModel;
+use app\admin\model\shopro\user\User as UserModel;
+use app\admin\model\Admin as AdminModel;
+use addons\shopro\library\Operator;
+
+class Log extends Common
+{
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new LogModel();
+    }
+
+
+    /**
+     * 查看
+     *
+     * @return Response
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $logs = $this->model->sheepFilter()->with(['agent'])->paginate($this->request->param('list_rows', 10));
+        
+        $morphs = [
+            'user' => UserModel::class,
+            'admin' => AdminModel::class,
+            'system' => AdminModel::class
+        ];
+        $logs = morph_to($logs, $morphs, ['oper_type', 'oper_id']);
+        $logs = $logs->toArray();
+
+        // 格式化操作人信息
+        foreach ($logs['data'] as &$log) {
+            $log['oper'] = Operator::info($log['oper_type'], $log['oper'] ?? null);
+        }
+
+        $this->success('获取成功', null, $logs);
+    }
+}

+ 323 - 0
addons/shopro/application/admin/controller/shopro/commission/Order.php

@@ -0,0 +1,323 @@
+<?php
+
+namespace app\admin\controller\shopro\commission;
+
+use think\Db;
+use think\exception\HttpResponseException;
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\commission\Order as OrderModel;
+use app\admin\model\shopro\commission\Reward as RewardModel;
+use addons\shopro\service\commission\Reward as RewardService;
+
+class Order extends Common
+{
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new OrderModel();
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            $exportConfig = (new \addons\shopro\library\Export())->getConfig();
+            $this->assignconfig("save_type", $exportConfig['save_type'] ?? 'download');
+            return $this->view->fetch();
+        }
+
+        $list = $this->model->sheepFilter()->with(['buyer', 'agent', 'order', 'rewards.agent', 'order_item'])->paginate($this->request->param('list_rows', 10));
+        $list = $list->toArray();
+
+        // 统计数据
+        $count = [
+            'total' => $list['total'],
+            'total_amount' => 0,
+            'total_commission' => 0,
+            'total_commission_cancel' => 0,
+            'total_commission_accounted' => 0,
+            'total_commission_back' => 0,
+            'total_commission_pending' => 0
+        ];
+
+        $orders = $this->model->sheepFilter()->with(['rewards'])->select();
+        collection($orders)->each(function ($order) use (&$count) {
+            $count['total_amount'] += $order['amount'];
+            foreach ($order['rewards'] as $reward) {
+                $count['total_commission'] += $reward['commission'];
+                switch ($reward['status']) {
+                    case RewardModel::COMMISSION_REWARD_STATUS_ACCOUNTED:
+                        $count['total_commission_accounted'] += $reward['commission'];
+                        break;
+                    case RewardModel::COMMISSION_REWARD_STATUS_BACK:
+                        $count['total_commission_back'] += $reward['commission'];
+                        break;
+                    case RewardModel::COMMISSION_REWARD_STATUS_PENDING:
+                        $count['total_commission_pending'] += $reward['commission'];
+                        break;
+                    case RewardModel::COMMISSION_REWARD_STATUS_CANCEL:
+                        $count['total_commission_cancel'] += $reward['commission'];
+                        break;
+                }
+            }
+        });
+
+        $this->success('', null, [
+            'list' => $list,
+            'count' => $count
+        ]);
+    }
+
+    /**
+     * 结算佣金
+     *
+     * @return Response
+     */
+    public function confirm()
+    {
+        $params = $this->request->only(['commission_reward_id', 'commission_order_id']);
+
+        try {
+            Db::transaction(function () use ($params) {
+                $rewardService = new RewardService('admin');
+                if (isset($params['commission_reward_id'])) {
+                    return $rewardService->runCommissionReward($params['commission_reward_id']);
+                } elseif (isset($params['commission_order_id'])) {
+                    return $rewardService->runCommissionRewardByOrder($params['commission_order_id']);
+                }
+            });
+        } catch (HttpResponseException $e) {
+            $data = $e->getResponse()->getData();
+            $message = $data ? ($data['msg'] ?? '') : $e->getMessage();
+            $this->error($message);
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+
+        $this->success('操作成功');
+    }
+
+    /**
+     * 取消结算
+     *
+     * @return Response
+     */
+    public function cancel()
+    {
+        $params = $this->request->only(['commission_reward_id', 'commission_order_id']);
+
+        try {
+
+            Db::transaction(function () use ($params) {
+                $rewardService = new RewardService('admin');
+                if (isset($params['commission_reward_id'])) {
+                    return $rewardService->cancelCommissionReward($params['commission_reward_id']);
+                } elseif (isset($params['commission_order_id'])) {
+                    return $rewardService->backCommissionRewardByOrder($params['commission_order_id']);
+                }
+            });
+        } catch (HttpResponseException $e) {
+            $data = $e->getResponse()->getData();
+            $message = $data ? ($data['msg'] ?? '') : $e->getMessage();
+            $this->error($message);
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+        $this->success('操作成功');
+    }
+
+    /**
+     * 退回已结算佣金
+     */
+    public function back()
+    {
+        $params = $this->request->only(['commission_reward_id', 'commission_order_id', 'deduct_order_money']);
+
+        try {
+            Db::transaction(function () use ($params) {
+                $rewardService = new RewardService('admin');
+                if (isset($params['commission_reward_id'])) {
+                    return $rewardService->backCommissionReward($params['commission_reward_id']);
+                } elseif (isset($params['commission_order_id'])) {
+                    return $rewardService->backCommissionRewardByOrder($params['commission_order_id'], $params['deduct_order_money']);
+                }
+            });
+        } catch (HttpResponseException $e) {
+            $data = $e->getResponse()->getData();
+            $message = $data ? ($data['msg'] ?? '') : $e->getMessage();
+            $this->error($message);
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+        $this->success('操作成功');
+    }
+
+    /**
+     * 修改待结算佣金
+     */
+    public function edit($id = null)
+    {
+        $params = $this->request->only(['commission_reward_id', 'commission']);
+        $reward = RewardModel::get($params['commission_reward_id']);
+        if (!$reward) {
+            $this->error(__('No Results were found'));
+        }
+        
+        $reward->commission = $params['commission'];
+        $result = $reward->save();
+        if ($result) {
+            $this->success('操作成功');
+        }
+        $this->error('操作失败');
+    }
+
+
+
+
+    public function export()
+    {
+        $cellTitles = [
+            // 主要字段
+            'commission_order_id' => 'Id',
+            'order_sn' => '订单号',
+            'goods_title' => '商品名称',
+            'goods_sku_text' => '商品规格',
+            'goods_price' => '商品价格',
+            'goods_num' => '购买数量',
+            'refund_status_text' => '退款状态',
+            'buyer_nickname' => '下单用户',
+            'buyer_mobile' => '手机号',
+            'share_nickname' => '推广分销商',
+            'share_mobile' => '手机号',
+            'commission_reward_status_text' => '佣金状态',
+            'reward_event_text' => '结算方式',
+            'commission_time' => '结算时间',
+            'reward_type_text' => '商品结算方式',
+            'amount' => '商品结算金额',
+            'commission_order_status_text' => '分销商业绩',
+            'total_commission' => '分销总金额',
+            'total_commissioned' => '到账金额',
+            // 佣金明细
+            'reward_agent_nickname' => '分佣用户',
+            'reward_agent_mobile' => '分佣手机号',
+            'reward_commission' => '分佣金额',
+            'reward_status_text' => '分佣状态',
+            'reward_type_text' => '入账方式',
+            'reward_commission_time' => '结算时间',
+        ];
+
+        // 数据总条数
+        $total = $this->model->sheepFilter()->count();
+        if ($total <= 0) {
+            $this->error('导出数据为空');
+        }
+
+        $export = new \addons\shopro\library\Export();
+        $params = [
+            'file_name' => '分销订单列表',
+            'cell_titles' => $cellTitles,
+            'total' => $total,
+            'is_sub_cell' => true,
+            'sub_start_cell' => 'reward_agent_nickname',
+            'sub_field' => 'rewards'
+        ];
+
+        $total_amount = 0;
+        $total_commission = 0;
+        $total_commission_cancel = 0;
+        $total_commission_accounted = 0;
+        $total_commission_back = 0;
+        $total_commission_pending = 0;
+        $result = $export->export($params, function ($pages) use (&$total_amount, &$total_commission, &$total_commission_cancel, &$total_commission_accounted, &$total_commission_back, &$total_commission_pending, $total) {
+            $datas = $this->model->sheepFilter()->with(['buyer', 'agent', 'order', 'rewards.agent', 'order_item'])
+                ->limit((($pages['page'] - 1) * $pages['list_rows']), $pages['list_rows'])
+                ->select();
+
+            $datas = collection($datas);
+            $datas->each(function ($commissionOrder) use (&$total_amount, &$total_commission, &$total_commission_cancel, &$total_commission_accounted, &$total_commission_back, &$total_commission_pending, $total) {
+                $total_amount += $commissionOrder['amount'];
+                foreach ($commissionOrder['rewards'] as $reward) {
+                    $total_commission += $reward['commission'];
+                    switch ($reward['status']) {
+                        case RewardModel::COMMISSION_REWARD_STATUS_ACCOUNTED:
+                            $total_commission_accounted += $reward['commission'];
+                            break;
+                        case RewardModel::COMMISSION_REWARD_STATUS_BACK:
+                            $total_commission_back += $reward['commission'];
+                            break;
+                        case RewardModel::COMMISSION_REWARD_STATUS_PENDING:
+                            $total_commission_pending += $reward['commission'];
+                            break;
+                        case RewardModel::COMMISSION_REWARD_STATUS_CANCEL:
+                            $total_commission_cancel += $reward['commission'];
+                            break;
+                    }
+                }
+            })->toArray();
+
+            $newDatas = [];
+            foreach ($datas as $commissionOrder) {
+                $commission = 0;
+                $commissioned = 0;
+                foreach ($commissionOrder['rewards'] as $reward) {
+                    if ($reward['status'] == 1) {
+                        $commissioned += $reward['commission'];
+                    }
+                    $commission += $reward['commission'];
+                }
+
+                $data = [
+                    'commission_order_id' => $commissionOrder['id'],
+                    'order_sn' => $commissionOrder['order'] ? $commissionOrder['order']['order_sn'] : '',
+                    'goods_title' => $commissionOrder['order_item'] ? '#' . $commissionOrder['order_item']['goods_id'] . ' ' . $commissionOrder['order_item']['goods_title'] : '',
+                    'goods_sku_text' => $commissionOrder['order_item'] ? $commissionOrder['order_item']['goods_sku_text'] : '',
+                    'goods_price' => $commissionOrder['order_item'] ? $commissionOrder['order_item']['goods_price'] : '',
+                    'goods_num' => $commissionOrder['order_item'] ? $commissionOrder['order_item']['goods_num'] : '',
+                    'refund_status_text' => $commissionOrder['order_item'] ? $commissionOrder['order_item']['refund_status_text'] : '',
+                    'buyer_nickname' => $commissionOrder['buyer'] ? $commissionOrder['buyer']['nickname'] : '-',
+                    'buyer_mobile' => $commissionOrder['buyer'] ? $commissionOrder['buyer']['mobile'] . ' ' : '-',
+                    'share_nickname' => $commissionOrder['agent'] ? $commissionOrder['agent']['nickname'] : '-',
+                    'share_mobile' => $commissionOrder['agent'] ? $commissionOrder['agent']['mobile'] . ' ' : '-',
+
+                    // 这里循环 rewards 佣金详情
+
+                    'commission_reward_status_text' => $commissionOrder['commission_reward_status_text'],
+                    'reward_event_text' => $commissionOrder['reward_event_text'],
+                    'commission_time' => $commissionOrder['commission_time'],
+                    'reward_type_text' => $commissionOrder['reward_type_text'],
+                    'amount' => $commissionOrder['amount'],
+                    'commission_order_status_text' => $commissionOrder['commission_order_status_text'],
+                    'total_commission' => $commission,
+                    'total_commissioned' => $commissioned,
+                ];
+
+                $rewardsItems = [];
+                foreach ($commissionOrder['rewards'] as $reward) {
+                    $rewardsItems[] = [
+                        'reward_agent_nickname' => $reward['agent'] ? $reward['agent']['nickname'] : '',
+                        'reward_agent_mobile' => $reward['agent'] ? $reward['agent']['mobile'] : '',
+                        'reward_commission' => $reward['commission'],
+                        'reward_status_text' => $reward['status_text'],
+                        'reward_type_text' => $reward['type_text'],
+                        'reward_commission_time' => $reward['commission_time']
+                    ];
+                }
+
+                $data['rewards'] = $rewardsItems;
+
+                $newDatas[] = $data;
+            }
+
+            if ($pages['is_last_page']) {
+                $newDatas[] = ['order_id' => "商品总订单数:" . $total . ";商品结算总金额:¥" . $total_amount .  ";分佣总金额:¥" . $total_commission . ";已取消佣金:¥" . $total_commission_cancel . ";已退回佣金:¥" . $total_commission_back . ";未结算佣金:" . $total_commission_pending . ";已结算佣金:" . $total_commission_accounted];
+            }
+            return $newDatas;
+        });
+
+        $this->success('导出成功' . (isset($result['file_path']) && $result['file_path'] ? ',请在服务器: “' . $result['file_path'] . '” 查看' : ''), null, $result);
+    }
+}

+ 108 - 0
addons/shopro/application/admin/controller/shopro/commission/Reward.php

@@ -0,0 +1,108 @@
+<?php
+
+namespace app\admin\controller\shopro\commission;
+
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\commission\Reward as RewardModel;
+
+class Reward extends Common
+{
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new RewardModel();
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            $exportConfig = (new \addons\shopro\library\Export())->getConfig();
+            $this->assignconfig("save_type", $exportConfig['save_type'] ?? 'download');
+            return $this->view->fetch();
+        }
+
+        $list = $this->model->sheepFilter()->with(['buyer', 'agent', 'order', 'order_item'])->paginate($this->request->param('list_rows', 10));
+
+        $this->success('获取成功', null, $list);
+    }
+
+
+    public function export()
+    {
+        $cellTitles = [
+            'reward_id' => 'Id',
+            'order_sn' => '订单号',
+            'buyer_nickname' => '下单用户',
+            'buyer_mobile' => '手机号',
+            'agent_nickname' => '分销用户',
+            'agent_mobile' => '分销手机号',
+            'original_commission' => '原始佣金',
+            'commission' => '分销佣金',
+            'commission_level' => '执行层级',
+            'agent_level' => '执行等级',
+            'status_text' => '状态',
+            'type_text' => '入账方式',
+            'commission_time' => '结算时间'
+        ];
+
+        // 数据总条数
+        $total = $this->model->sheepFilter()->count();
+        if ($total <= 0) {
+            $this->error('导出数据为空');
+        }
+
+        $export = new \addons\shopro\library\Export();
+        $params = [
+            'file_name' => '佣金明细列表',
+            'cell_titles' => $cellTitles,
+            'total' => $total,
+            'is_sub_cell' => false,
+        ];
+
+        $total_commission = 0;
+        $result = $export->export($params, function ($pages) use (&$total_commission, $total) {
+            $datas = $this->model->sheepFilter()->with(['buyer', 'agent', 'order', 'order_item'])
+            ->limit((($pages['page'] - 1) * $pages['list_rows']), $pages['list_rows'])
+            ->select();
+
+            $datas = collection($datas);
+            $datas->each(function ($order) {
+            })->toArray();
+
+            $newDatas = [];
+            foreach ($datas as &$reward) {
+                $data = [
+                    'reward_id' => $reward['id'],
+                    'order_sn' => $reward['order'] ? $reward['order']['order_sn'] : '',
+                    'buyer_nickname' => $reward['buyer'] ? $reward['buyer']['nickname'] : '-',
+                    'buyer_mobile' => $reward['buyer'] ? $reward['buyer']['mobile'] . ' ' : '-',
+                    'agent_nickname' => $reward['agent'] ? $reward['agent']['nickname'] : '-',
+                    'agent_mobile' => $reward['agent'] ? $reward['agent']['mobile'] . ' ' : '-',
+                    'original_commission' => $reward['original_commission'],
+                    'commission' => $reward['commission'],
+                    'commission_level' => $reward['commission_level'],
+                    'agent_level' => $reward['agent_level'],
+                    'status_text' => $reward['status_text'],
+                    'type_text' => $reward['type_text'],
+                    'commission_time' => $reward['commission_time'],
+                ];
+
+                $newDatas[] = $data;
+            }
+
+            $total_commission += array_sum(array_column($newDatas, 'commission'));
+
+            if ($pages['is_last_page']) {
+                $newDatas[] = ['reward_id' => "总数:" . $total . ";总佣金金额:¥" . $total_commission .  ";"];
+            }
+            return $newDatas;
+        });
+
+        $this->success('导出成功' . (isset($result['file_path']) && $result['file_path'] ? ',请在服务器: “' . $result['file_path'] . '” 查看' : ''), null, $result);
+    }
+}

+ 170 - 0
addons/shopro/application/admin/controller/shopro/data/Area.php

@@ -0,0 +1,170 @@
+<?php
+
+namespace app\admin\controller\shopro\data;
+
+use app\admin\controller\shopro\Common;
+use think\Db;
+
+/**
+ * 地区管理
+ */
+class Area extends Common
+{
+
+    protected $noNeedRight = ['select'];
+
+    /**
+     * Faq模型对象
+     * @var \app\admin\model\shopro\data\Express
+     */
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new \app\admin\model\shopro\data\Area;
+    }
+
+
+    /**
+     * 查看
+     *
+     * @return string|Json
+     * @throws \think\Exception
+     * @throws DbException
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $list = $this->model->sheepFilter()->with('children.children')->where('pid', 0)->select();               // 查询全部
+        $this->success('', null, $list);
+    }
+
+
+    public function select()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $level = $this->request->param('level', 'all');
+        $with = ['children.children'];
+        if ($level == 'city') {
+            $with = ['children'];
+        } else if ($level == 'province') {
+            $with = [];
+        }
+        
+        $list = $this->model->with($with)->where('pid', 0)->field('id, name, pid, level')->select();               // 查询全部
+        $this->success('', null, $list);
+    }
+
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only(['id', 'pid', 'name']);
+
+        $params['level'] = 'province';
+        if (isset($params['pid']) && $params['pid']) {
+            $parent = $this->model->find($params['pid']);
+            if (!$parent) {
+                $this->error(__('No Results were found'));
+            }
+            if ($parent['level'] == 'province') {
+                $params['level'] = 'city';
+            } else if ($parent['level'] == 'city') {
+                $params['level'] = 'district';
+            }
+        }
+        $this->model->save($params);
+
+        $this->success('保存成功', null, $this->model);
+    }
+
+
+
+
+    /**
+     * 详情
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function detail($id)
+    {
+        $detail = $this->model->where('id', $id)->find();
+        if (!$detail) {
+            $this->error(__('No Results were found'));
+        }
+        $this->success('获取成功', null, $detail);
+    }
+
+
+    /**
+     * 编辑(支持批量)
+     */
+    public function edit($old_id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->only(['id', 'pid', 'name']);
+        $params['level'] = 'province';
+        if (isset($params['pid']) && $params['pid']) {
+            $parent = $this->model->find($params['pid']);
+            if (!$parent) {
+                $this->error(__('No Results were found'));
+            }
+            if ($parent['level'] == 'province') {
+                $params['level'] = 'city';
+            } else if ($parent['level'] == 'city') {
+                $params['level'] = 'district';
+            }
+        }
+
+        $area = $this->model->where('id', 'in', $old_id)->find();
+        if (!$area) {
+            $this->error(__('No Results were found'));
+        }
+
+        $area->save($params);
+        $this->success('更新成功', null, $area);
+    }
+
+    /**
+     * 删除
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $area = $this->model->where('id', 'in', $id)->find();
+        if (!$area) {
+            $this->error(__('No Results were found'));
+        }
+
+        $children = $this->model->where('pid', $id)->count();
+        if ($children) {
+            $this->error('请先删除下级地市');
+        }
+        
+        $area->delete();
+        $this->success('删除成功');
+    }
+}

+ 156 - 0
addons/shopro/application/admin/controller/shopro/data/Express.php

@@ -0,0 +1,156 @@
+<?php
+
+namespace app\admin\controller\shopro\data;
+
+use app\admin\controller\shopro\Common;
+use think\Db;
+
+/**
+ * 快递公司
+ */
+class Express extends Common
+{
+
+    protected $noNeedRight = ['select'];
+
+    /**
+     * Faq模型对象
+     * @var \app\admin\model\shopro\data\Express
+     */
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new \app\admin\model\shopro\data\Express;
+    }
+
+
+    /**
+     * 查看
+     *
+     * @return string|Json
+     * @throws \think\Exception
+     * @throws DbException
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $list = $this->model->sheepFilter()->paginate($this->request->request('list_rows', 10));
+        $this->success('', null, $list);
+    }
+
+
+    /**
+     * 选择快递公司
+     *
+     * @return void
+     */
+    public function select()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $list = $this->model->sheepFilter()->paginate($this->request->param('list_rows', 10));
+        $this->success('', null, $list);
+    }
+
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only(['name', 'code', 'weigh']);
+        $this->svalidate($params, '.add');
+
+        $this->model->save($params);
+
+        $this->success('保存成功', null, $this->model);
+    }
+
+
+
+
+    /**
+     * 详情
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function detail($id)
+    {
+        $detail = $this->model->where('id', $id)->find();
+        if (!$detail) {
+            $this->error(__('No Results were found'));
+        }
+        $this->success('获取成功', null, $detail);
+    }
+
+
+    /**
+     * 编辑(支持批量)
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->only(['name', 'code', 'weigh']);
+
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list, $params) {
+            $count = 0;
+            foreach ($list as $item) {
+                $params['id'] = $item->id;
+                $this->svalidate($params);
+                $count += $item->save($params);
+            }
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('更新成功', null, $result);
+        } else {
+            $this->error('更新失败');
+        }
+    }
+
+    /**
+     * 删除(支持批量)
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list) {
+            $count = 0;
+            foreach ($list as $item) {
+                $count += $item->delete();
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('删除成功', null, $result);
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+}

+ 222 - 0
addons/shopro/application/admin/controller/shopro/data/FakeUser.php

@@ -0,0 +1,222 @@
+<?php
+
+namespace app\admin\controller\shopro\data;
+
+use app\admin\controller\shopro\Common;
+use think\Db;
+
+/**
+ * 虚拟用户
+ */
+class FakeUser extends Common
+{
+
+    protected $noNeedRight = ['select', 'getRandom'];
+
+    /**
+     * Faq模型对象
+     * @var \app\admin\model\shopro\data\Express
+     */
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new \app\admin\model\shopro\data\FakeUser;
+    }
+
+
+    /**
+     * 查看
+     *
+     * @return string|Json
+     * @throws \think\Exception
+     * @throws DbException
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $list = $this->model->sheepFilter()->paginate($this->request->param('list_rows', 10));
+        $this->success('', null, $list);
+    }
+
+
+    /**
+     * 随机获取一个虚拟用户
+     *
+     * @return void
+     */
+    public function getRandom()
+    {
+        $userFake = $this->model->orderRaw('rand()')->find();
+
+        $userFake ? $this->success('获取成功', null, $userFake) : $this->error('请在数据维护中添加虚拟用户');
+    }
+
+
+    public function select()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $list = $this->model->sheepFilter()->paginate($this->request->param('list_rows', 10));
+        $this->success('', null, $list);
+    }
+
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only(['username', 'nickname', 'mobile', 'password', 'avatar', 'gender', 'email']);
+        $this->svalidate($params, '.add');
+
+        $this->model->save($params);
+
+        $this->success('保存成功', null, $this->model);
+    }
+
+
+
+    /**
+     * 随机生成用户
+     */
+    public function random()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        set_time_limit(0);
+        $num = $this->request->param('num', 1);
+
+        for ($i = 0; $i < $num; $i++) {
+            $style = [
+                'adventurer',
+                'adventurer-neutral',
+                'big-smile',
+                'big-ears-neutral',
+                'bottts',
+                'croodles',
+                'croodles-neutral',
+                'fun-emoji',
+                'icons',
+                'identicon',
+                'lorelei',
+                'lorelei-neutral',
+                'micah',
+                'notionists'
+            ];
+
+            $username = gen_random_str(mt_rand(6, 15), mt_rand(0, 1));
+            $avatarSources = [
+                // "https://joeschmoe.io/api/v1/random",        // 生成的是 svg ,无法使用
+                "https://api.dicebear.com/7.x/%s/png" . "?seed=" . $username . '&size=256',
+                "https://api.multiavatar.com/" . $username . ".png"
+            ];
+
+            $avatar_url = $avatarSources[array_rand($avatarSources)];
+            $avatar_url = sprintf($avatar_url, $style[array_rand($style)]);
+
+            $store_path = '/uploads/' . date('Ymd') . '/' . md5(time() . mt_rand(1000, 9999)) . '.png';       // 存数据库路径
+            $save_path = ROOT_PATH . 'public' . $store_path;                                            // 服务器绝对路径
+            image_resize_save($avatar_url, $save_path);
+
+            $fakeUser = new \app\admin\model\shopro\data\FakeUser();
+            $fakeUser->username = $username;
+            $fakeUser->nickname = $username;
+            $fakeUser->mobile = random_mobile();
+            $fakeUser->password = gen_random_str();
+            $fakeUser->avatar = cdnurl($store_path, true);            // 这里存了完整路径
+            $fakeUser->gender = mt_rand(0, 1);
+            $fakeUser->email = random_email($fakeUser->mobile);
+            $fakeUser->save();
+        }
+
+        $this->success('生成成功');
+    }
+
+
+    /**
+     * 详情
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function detail($id)
+    {
+        $detail = $this->model->where('id', $id)->find();
+        if (!$detail) {
+            $this->error(__('No Results were found'));
+        }
+        $this->success('获取成功', null, $detail);
+    }
+
+
+    /**
+     * 编辑(支持批量)
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->only(['username', 'nickname', 'mobile', 'password', 'avatar', 'gender', 'email']);
+
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list, $params) {
+            $count = 0;
+            foreach ($list as $item) {
+                $params['id'] = $item->id;
+                $this->svalidate($params);
+                $count += $item->save($params);
+            }
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('更新成功', null, $result);
+        } else {
+            $this->error('更新失败');
+        }
+    }
+
+    /**
+     * 删除(支持批量)
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list) {
+            $count = 0;
+            foreach ($list as $item) {
+                $count += $item->delete();
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('删除成功', null, $result);
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+}

+ 138 - 0
addons/shopro/application/admin/controller/shopro/data/Faq.php

@@ -0,0 +1,138 @@
+<?php
+
+namespace app\admin\controller\shopro\data;
+
+use app\admin\controller\shopro\Common;
+use think\Db;
+
+/**
+ * 常见问题
+ */
+class Faq extends Common
+{
+
+    /**
+     * Faq模型对象
+     * @var \app\admin\model\shopro\data\Faq
+     */
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new \app\admin\model\shopro\data\Faq;
+    }
+
+
+    /**
+     * 查看
+     *
+     * @return string|Json
+     * @throws \think\Exception
+     * @throws DbException
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $list = $this->model->sheepFilter()->paginate($this->request->request('list_rows', 10));
+        $this->success('', null, $list);
+    }
+
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only(['title', 'content', 'status']);
+        $this->svalidate($params, '.add');
+
+        $this->model->save($params);
+
+        $this->success('保存成功', null, $this->model);
+    }
+
+
+
+
+    /**
+     * 详情
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function detail($id)
+    {
+        $detail = $this->model->where('id', $id)->find();
+        if (!$detail) {
+            $this->error(__('No Results were found'));
+        }
+        $this->success('获取成功', null, $detail);
+    }
+
+
+    /**
+     * 编辑(支持批量)
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->only(['title', 'content', 'status']);
+
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list, $params) {
+            $count = 0;
+            foreach ($list as $item) {
+                $params['id'] = $item->id;
+                $this->svalidate($params);
+                $count += $item->save($params);
+            }
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('更新成功', null, $result);
+        } else {
+            $this->error('更新失败,未改变任何记录');
+        }
+    }
+
+    /**
+     * 删除(支持批量)
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list) {
+            $count = 0;
+            foreach ($list as $item) {
+                $count += $item->delete();
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('删除成功', null, $result);
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+}

+ 156 - 0
addons/shopro/application/admin/controller/shopro/data/Page.php

@@ -0,0 +1,156 @@
+<?php
+
+namespace app\admin\controller\shopro\data;
+
+use app\admin\controller\shopro\Common;
+use think\Db;
+
+/**
+ * 前端路由
+ */
+class Page extends Common
+{
+
+    protected $noNeedRight = ['select'];
+    
+    /**
+     * Page模型对象
+     * @var \app\admin\model\shopro\data\Page
+     */
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new \app\admin\model\shopro\data\Page;
+
+    }
+
+
+    /**
+     * 查看
+     *
+     * @return string|Json
+     * @throws \think\Exception
+     * @throws DbException
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $list = $this->model->sheepFilter()->paginate($this->request->request('list_rows', 10));
+        $this->success('', null, $list);
+    }
+
+
+
+    public function select()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $list = $this->model->group('group')->with(['children'])->field('group')->select();
+        $this->success('', null, $list);
+    }
+
+
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only(['name', 'path', 'group']);
+        $this->svalidate($params, '.add');
+
+        $this->model->save($params);
+
+        $this->success('保存成功', null, $this->model);
+    }
+
+
+
+
+    /**
+     * 详情
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function detail($id)
+    {
+        $detail = $this->model->where('id', $id)->find();
+        if (!$detail) {
+            $this->error(__('No Results were found'));
+        }
+        $this->success('获取成功', null, $detail);
+    }
+
+
+    /**
+     * 编辑(支持批量)
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->only(['name', 'path', 'group']);
+
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list, $params) {
+            $count = 0;
+            foreach ($list as $item) {
+                $params['id'] = $item->id;
+                $this->svalidate($params);
+                $count += $item->save($params);
+            }
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('更新成功', null, $result);
+        } else {
+            $this->error('更新失败,未改变任何记录');
+        }
+    }
+
+    /**
+     * 删除(支持批量)
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list) {
+            $count = 0;
+            foreach ($list as $item) {
+                $count += $item->delete();
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('删除成功', null, $result);
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+
+
+}

+ 170 - 0
addons/shopro/application/admin/controller/shopro/data/Richtext.php

@@ -0,0 +1,170 @@
+<?php
+
+namespace app\admin\controller\shopro\data;
+
+use app\admin\controller\shopro\Common;
+use think\Db;
+
+/**
+ * 富文本
+ */
+class Richtext extends Common
+{
+
+    protected $noNeedRight = ['select'];
+
+    /**
+     * Richtext模型对象
+     * @var \app\admin\model\shopro\data\Richtext
+     */
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new \app\admin\model\shopro\data\Richtext;
+
+    }
+
+
+
+    /**
+     * 查看
+     *
+     * @return string|Json
+     * @throws \think\Exception
+     * @throws DbException
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $list = $this->model->sheepFilter()->paginate($this->request->request('list_rows', 10));
+        $this->success('', null, $list);
+    }
+
+
+
+    public function select()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $type = $this->request->param('type', 'page');
+
+        $list = $this->model->sheepFilter();
+
+        if ($type == 'select') {
+            // 普通结果
+            $list = $list->select();
+        } elseif ($type == 'find') {
+            $list = $list->find();
+        } else {
+            // 分页结果
+            $list = $list->paginate($this->request->request('list_rows', 10));
+        }
+
+        $this->success('', null, $list);
+    }
+
+
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only(['title', 'content']);
+        $this->svalidate($params, '.add');
+
+        $this->model->save($params);
+
+        $this->success('保存成功', null, $this->model);
+    }
+
+
+
+
+    /**
+     * 详情
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function detail($id)
+    {
+        $detail = $this->model->where('id', $id)->find();
+        if (!$detail) {
+            $this->error(__('No Results were found'));
+        }
+        $this->success('获取成功', null, $detail);
+    }
+
+
+    /**
+     * 编辑(支持批量)
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->only(['title', 'content']);
+
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list, $params) {
+            $count = 0;
+            foreach ($list as $item) {
+                $params['id'] = $item->id;
+                $this->svalidate($params);
+                $count += $item->save($params);
+            }
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('更新成功', null, $result);
+        } else {
+            $this->error('更新失败,未改变任何记录');
+        }
+    }
+
+    /**
+     * 删除(支持批量)
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list) {
+            $count = 0;
+            foreach ($list as $item) {
+                $count += $item->delete();
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('删除成功', null, $result);
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+
+
+}

+ 48 - 0
addons/shopro/application/admin/controller/shopro/decorate/Designer.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace app\admin\controller\shopro\decorate;
+
+use app\admin\controller\shopro\Common;
+use think\Db;
+use app\admin\model\shopro\decorate\Decorate as DecorateModel;
+use app\admin\model\shopro\decorate\Page as PageModel;
+
+class Designer extends Common
+{
+
+    /**
+     * 使用设计师模板
+     */
+    public function use()
+    {
+        $params = $this->request->param();
+        Db::transaction(function () use ($params) {
+            $decorate = DecorateModel::create([
+                'name' => $params['name'],
+                'type' => $params['type'],
+                'memo' => $params['memo'],
+                'platform' => $params['platform'],
+                'status' => 'disabled'
+            ]);
+            $pageList = [];
+            $params['page'] = json_decode($params['page'], true);
+            foreach ($params['page'] as $page) {
+                array_push($pageList, [
+                    'decorate_id' => $decorate->id,
+                    'image' => $page['image'] ?? '',
+                    'type' => $page['type'],
+                    'page' => json_encode($page['page'], JSON_UNESCAPED_UNICODE)
+                ]);
+            }
+
+            PageModel::insertAll($pageList);
+            $this->downLoadDecorateImages($params['imageList']);
+        });
+        $this->success();
+    }
+
+    private function downLoadDecorateImages($imageList)
+    {
+        \think\Queue::push('\addons\shopro\job\Designer@redeposit', ['imageList' => $imageList], 'shopro');
+    }
+}

+ 76 - 0
addons/shopro/application/admin/controller/shopro/decorate/Page.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace app\admin\controller\shopro\decorate;
+
+use think\Db;
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\decorate\Page as PageModel;
+
+class Page extends Common
+{
+
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new PageModel();
+    }
+    /**
+     * 页面列表
+     */
+    public function index()
+    {
+        return $this->view->fetch();
+    }
+
+    /**
+     * 页面详情
+     *
+     * @param  int $id
+     */
+    public function detail($id)
+    {
+        $type = $this->request->param('type');
+        $page = $this->model->where(['decorate_id' => $id, 'type' => $type])->find();
+        $this->success('获取成功', null, $page);
+    }
+
+    /**
+     * 编辑
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function edit($id = null)
+    {
+        $type = $this->request->param('type');
+        $page = $this->request->param('page');
+        $image = $this->request->param('image');
+
+        $pageRow = PageModel::where(['decorate_id' => $id, 'type' => $type])->find();
+
+        operate_filter();
+        if ($pageRow) {
+            $pageRow->page = $page;
+            $pageRow->image = $image;
+            $pageRow->save();
+        } else {
+            PageModel::create([
+                'decorate_id' => $id,
+                'type' => $type,
+                'page' => $page,
+                'image' => $image
+            ]);
+        }
+        $this->success('保存成功');
+    }
+
+    /**
+     * 预览
+     */
+    public function preview()
+    {
+        return $this->view->fetch();
+    }
+}

+ 286 - 0
addons/shopro/application/admin/controller/shopro/decorate/Template.php

@@ -0,0 +1,286 @@
+<?php
+
+namespace app\admin\controller\shopro\decorate;
+
+use think\Db;
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\decorate\Decorate as DecorateModel;
+use app\admin\model\shopro\decorate\Page as PageModel;
+
+class Template extends Common
+{
+    protected $model = null;
+
+    protected $noNeedRight = ['select'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new DecorateModel();
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $list = $this->model->sheepFilter()->with(['page' => function ($query) {
+            return $query->where('type', 'in', ['home', 'user', 'diypage'])->field('decorate_id, type, image');
+        }])->select();
+        $this->success('获取成功', null, $list);
+    }
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only(['type', 'name', 'memo', 'platform']);
+        $this->svalidate($params, '.add');
+
+        $this->model->save($params);
+
+        $this->success('保存成功', null, $this->model);
+    }
+
+
+
+    /**
+     * 详情
+     *
+     * @param  $id
+     */
+    public function detail($id)
+    {
+        $detail = $this->model->where('id', $id)->find();
+        if (!$detail) {
+            $this->error(__('No Results were found'));
+        }
+        $this->success('获取成功', null, $detail);
+    }
+
+
+    /**
+     * 编辑(支持批量)
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        operate_filter();
+        $params = $this->request->only([
+            'type', 'name', 'memo', 'platform'
+        ]);
+
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list, $params) {
+            $count = 0;
+            foreach ($list as $item) {
+                $params['id'] = $item->id;
+                $this->svalidate($params);
+                $count += $item->save($params);
+            }
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('更新成功', null, $result);
+        } else {
+            $this->error('更新失败,未改变任何记录');
+        }
+    }
+
+
+
+    /**
+     * 复制
+     *
+     * @param  $id
+     */
+    public function copy($id)
+    {
+        $template = $this->model->where('id', $id)->find();
+        if (!$template) {
+            $this->error(__('No Results were found'));
+        }
+
+        Db::transaction(function () use ($template) {
+            $params = [
+                'name' => '复制 ' . $template->name,
+                'type' => $template->type,
+                'memo' => $template->memo,
+                'platform' => $template->platform,
+                'status' => 'disabled'
+            ];
+            $newTemplate = $this->model->create($params);
+
+            $pageList = PageModel::where('decorate_id', $template->id)->select();
+
+            $newPageList = [];
+            foreach ($pageList as $page) {
+                $newPageList[] = [
+                    'decorate_id' => $newTemplate->id,
+                    'type' => $page['type'],
+                    'page' => json_encode($page['page']),
+                    'image' => $page['image']
+                ];
+            }
+            if (count($newPageList) > 0) {
+                PageModel::insertAll($newPageList);
+            }
+        });
+
+        $this->success('复制成功');
+    }
+
+    /**
+     * 启用/禁用
+     *
+     * @param  $id
+     */
+    public function status($id)
+    {
+        $status = $this->request->param('status');
+        $template = $this->model->where('id', $id)->find();
+        if (!$template) {
+            $this->error(__('No Results were found'));
+        }
+
+        operate_filter();
+        $template->status = $status;
+        $template->save();
+        $this->success('操作成功', null, $template);
+    }
+
+
+    /**
+     * 删除(支持批量)
+     *
+     * @param  $id
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        operate_filter();
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list) {
+            $count = 0;
+            foreach ($list as $item) {
+                $count += $item->delete();
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('删除成功', null, $result);
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+
+
+    public function recyclebin()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $templates = $this->model->onlyTrashed()->sheepFilter()->paginate($this->request->param('list_rows', 10));
+        $this->success('获取成功', null, $templates);
+    }
+
+
+    /**
+     * 还原(支持批量)
+     *
+     * @param  $id
+     */
+    public function restore($id = null)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $items = $this->model->onlyTrashed()->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($items) {
+            $count = 0;
+            foreach ($items as $item) {
+                $count += $item->restore();
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('还原成功', null, $result);
+        } else {
+            $this->error(__('No rows were updated'));
+        }
+    }
+
+
+
+    /**
+     * 销毁(支持批量)
+     *
+     * @param  $id
+     */
+    public function destroy($id = null)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        if ($id !== 'all') {
+            $items = $this->model->onlyTrashed()->whereIn('id', $id)->select();
+        } else {
+            $items = $this->model->onlyTrashed()->select();
+        }
+        $result = Db::transaction(function () use ($items) {
+            $count = 0;
+            foreach ($items as $item) {
+                PageModel::where('decorate_id', $item->id)->delete();
+
+                // 删除商品
+                $count += $item->delete(true);
+            }
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('销毁成功', null, $result);
+        }
+        $this->error('销毁失败');
+    }
+
+
+    /**
+     * 选择自定义页面
+     */
+    public function select()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $list = $this->model->where('type', 'diypage')->with(['page' => function ($query) {
+            return $query->field('decorate_id, type, image');
+        }])->select();
+
+        $this->success('获取成功', null, $list);
+    }
+}

+ 232 - 0
addons/shopro/application/admin/controller/shopro/dispatch/Dispatch.php

@@ -0,0 +1,232 @@
+<?php
+
+namespace app\admin\controller\shopro\dispatch;
+
+use think\Db;
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\dispatch\Dispatch as DispatchModel;
+use app\admin\model\shopro\dispatch\DispatchExpress as DispatchExpressModel;
+use app\admin\model\shopro\dispatch\DispatchAutosend as DispatchAutosendModel;
+
+/**
+ * 配送管理
+ */
+class Dispatch extends Common
+{
+
+    protected $noNeedRight = ['select'];
+
+    protected $expressModel;
+    protected $autosendModel;
+    protected $dispatch_type;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new DispatchModel;
+        $this->expressModel = new DispatchExpressModel;
+        $this->autosendModel = new DispatchAutosendModel;
+
+        $this->dispatch_type = $this->request->param('type', 'express');
+    }
+
+
+
+    /**
+     * 配送方式列表
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $dispatchs = $this->model->sheepFilter()->with([$this->dispatch_type])->where('type', $this->dispatch_type)->paginate($this->request->param('list_rows', 10));
+
+        $this->success('获取成功', null, $dispatchs);
+    }
+
+
+
+
+    /**
+     * 添加配送方式
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only([
+            'name', 'type', 'status', 'express', 'autosend'
+        ]);
+        $this->svalidate($params, ".add");
+        $data = $params[$this->dispatch_type] ?? [];
+        unset($params['express'], $params['autosend']);
+
+        if ($this->dispatch_type == 'express') {
+            // 验证 express
+            foreach ($data as $key => $express) {
+                $this->svalidate($express, '.express');
+            }
+        } else if ($this->dispatch_type == 'autosend') {
+            // 验证 autosend
+            $this->svalidate($data, '.autosend');
+        }
+
+        Db::transaction(function () use ($params, $data) {
+            unset($params['createtime'], $params['updatetime'], $params['id']);      // 删除时间
+            $this->model->allowField(true)->save($params);
+
+            if ($this->dispatch_type == 'express') {
+                foreach ($data as $key => $express) {
+                    $express['dispatch_id'] = $this->model->id;
+                    $expressModel = new DispatchExpressModel();
+                    unset($express['createtime'], $express['updatetime'], $express['id']);      // 删除时间
+                    $expressModel->allowField(true)->save($express);
+                }
+            } else if ($this->dispatch_type == 'autosend') {
+                $data['dispatch_id'] = $this->model->id;
+                $autosendModel = new DispatchAutosendModel();
+                unset($data['createtime'], $data['updatetime'], $data['id']);      // 删除时间
+                $autosendModel->allowField(true)->save($data);
+            }
+        });
+
+        $this->success('保存成功');
+    }
+
+
+    /**
+     * 配送方式详情
+     *
+     * @param  $id
+     */
+    public function detail($id)
+    {
+        $dispatch = $this->model->with([$this->dispatch_type])->where('type', $this->dispatch_type)->where('id', $id)->find();
+        if (!$dispatch) {
+            $this->error(__('No Results were found'));
+        }
+
+        $this->success('获取成功', null, $dispatch);
+    }
+
+
+
+    /**
+     * 修改配送方式
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->only([
+            'name', 'type', 'status', 'express', 'autosend'
+        ]);
+        $this->svalidate($params);
+        $data = $params[$this->dispatch_type] ?? [];
+        unset($params['express'], $params['autosend']);
+
+        if ($this->dispatch_type == 'express') {
+            // 验证 express
+            foreach ($data as $key => $express) {
+                $this->svalidate($express, '.express');
+            }
+        } else if ($this->dispatch_type == 'autosend') {
+            // 验证 autosend
+            $this->svalidate($data, '.autosend');
+        }
+
+        $id = explode(',', $id);
+        $lists = $this->model->whereIn('id', $id)->select();
+        Db::transaction(function () use ($lists, $params, $data) {
+            foreach ($lists as $dispatch) {
+                $dispatch->allowField(true)->save($params);
+                if ($data) {
+                    if ($this->dispatch_type == 'express') {
+                        // 修改,不是只更新状态
+                        $expressIds = array_column($data, 'id');
+                        DispatchExpressModel::where('dispatch_id', $dispatch->id)->whereNotIn('id', $expressIds)->delete();      // 先删除被删除的记录
+                        foreach ($data as $key => $express) {
+                            if (isset($express['id']) && $express['id']) {
+                                $expressModel = $this->expressModel->find($express['id']);
+                            } else {
+                                $expressModel = new DispatchExpressModel();
+                                $express['dispatch_id'] = $dispatch->id;
+                            }
+                            $express['weigh'] = count($data) - $key;           // 权重
+                            unset($express['createtime'], $express['updatetime']);
+                            $expressModel && $expressModel->allowField(true)->save($express);
+                        }
+                    } else if ($this->dispatch_type == 'autosend') {
+                        if (isset($data['id']) && $data['id']) {
+                            $autosendModel = $this->autosendModel->find($data['id']);
+                        } else {
+                            $autosendModel = new DispatchAutosendModel();
+                            $data['dispatch_id'] = $dispatch->id;
+                        }
+
+                        unset($data['createtime'], $data['updatetime']);      // 删除时间
+                        $autosendModel->allowField(true)->save($data);
+                    }
+                }
+            }
+        });
+        $this->success('更新成功');
+    }
+
+
+
+    /**
+     * 删除配送方式
+     *
+     * @param string $id 要删除的配送方式列表
+     * @return void
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+        $id = explode(',', $id);
+        $list = $this->model->with([$this->dispatch_type])->where('type', $this->dispatch_type)->where('id', 'in', $id)->select();
+        Db::transaction(function () use ($list) {
+            $count = 0;
+            foreach ($list as $item) {
+                if ($this->dispatch_type == 'express') {
+                    // 删除相关的 express 数据
+                    foreach ($item->express as $express) {
+                        $express->delete();
+                    }
+                } else if ($this->dispatch_type == 'autosend') {
+                    $item->{$this->dispatch_type}->delete();
+                }
+
+                $count += $item->delete();
+            }
+
+            return $count;
+        });
+
+        $this->success('删除成功');
+    }
+
+
+    /**
+     * 获取所有配送模板
+     */
+    public function select()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $dispatchs = $this->model->sheepFilter()->field('id, name, type, status')->normal()->where('type', $this->dispatch_type)->select();
+
+        $this->success('获取成功', null, $dispatchs);
+    }
+}

+ 247 - 0
addons/shopro/application/admin/controller/shopro/goods/Comment.php

@@ -0,0 +1,247 @@
+<?php
+
+namespace app\admin\controller\shopro\goods;
+
+use think\Db;
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\goods\Comment as CommentModel;
+use app\admin\model\shopro\data\FakeUser as FakeUserModel;
+
+class Comment extends Common
+{
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new CommentModel;
+    }
+
+    /**
+     * 商品评价列表
+     *
+     * @return \think\Response
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $comments = $this->model->sheepFilter()->with(['goods' => function ($query) {
+            $query->field('id,image,title');
+        }, 'order' => function ($query) {
+            $query->removeOption('soft_delete');
+        }, 'order_item'])->paginate($this->request->param('list_rows', 10));
+
+        $morphs = [
+            'user' => \app\admin\model\shopro\user\User::class,
+            'fake_user' => \app\admin\model\shopro\data\FakeUser::class
+        ];
+        $comments = morph_to($comments, $morphs, ['user_type', 'user_id']);
+
+        $this->success('获取成功', null, $comments);
+    }
+
+
+
+    public function detail($id)
+    {
+        $comment = $this->model->with(['admin', 'goods' => function ($query) {
+            $query->field('id,image,title,price');
+        }, 'order' => function ($query) {
+            $query->removeOption('soft_delete');
+        }, 'order_item'])->where('id', $id)->find();
+
+        if (!$comment) {
+            $this->error(__('No Results were found'));
+        }
+
+        $morphs = [
+            'user' => \app\admin\model\shopro\user\User::class,
+            'fake_user' => \app\admin\model\shopro\data\FakeUser::class
+        ];
+        $comments = morph_to([$comment], $morphs, ['user_type', 'user_id']);
+
+        $this->success('获取成功', null, $comments->all()[0]);
+    }
+
+
+    public function add() 
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only([
+            'goods_id', 'user_id', 'level', 'content', 'images', 'status'
+        ]);
+        $params['user_type'] = 'fake_user';
+        $this->svalidate($params, ".add");
+        $fakeUser = FakeUserModel::find($params['user_id']);
+        $params['user_nickname'] = $fakeUser ? $fakeUser->nickname : null;
+        $params['user_avatar'] = $fakeUser ? $fakeUser->avatar : null;
+
+        Db::transaction(function () use ($params) {
+            $this->model->save($params);
+        });
+        $this->success('保存成功');
+    }
+
+
+    public function edit($id = null) 
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->only([
+            'status'
+        ]);
+
+        $id = explode(',', $id);
+        $list = $this->model->whereIn('id', $id)->select();
+        $result = Db::transaction(function () use ($list, $params) {
+            $count = 0;
+            foreach ($list as $comment) {
+                $comment->status = $params['status'] ?? 'hidden';
+                $count += $comment->save();
+            }
+
+            return $count;
+        });
+        
+        if ($result) {
+            $this->success('更新成功', null, $result);
+        } else {
+            $this->error('更新失败,未改变任何记录');
+        }
+    }
+
+
+    public function reply($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only([
+            'content'
+        ]);
+        $this->svalidate($params, '.reply');
+
+        $comment = $this->model->noReply()->find($id);
+        if (!$comment) {
+            $this->error(__('No Results were found'));
+        }
+
+        $comment->reply_content = $params['content'];
+        $comment->reply_time = time();
+        $comment->admin_id = $this->auth->id;
+        $comment->save();
+
+        $this->success('回复成功');
+    }
+
+    
+    /**
+     * 删除商品评价
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $list = $this->model->where('id', 'in', $id)->select();
+        Db::transaction(function () use ($list) {
+            $count = 0;
+            foreach ($list as $item) {
+                $count += $item->delete();
+            }
+
+            return $count;
+        });
+
+        $this->success('删除成功');
+    }
+
+
+
+    /**
+     * 评价回收站
+     *
+     * @return void
+     */
+    public function recyclebin() 
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $comments = $this->model->onlyTrashed()->sheepFilter()->paginate($this->request->param('list_rows', 10));
+
+        $this->success('获取成功', null, $comments);
+    }
+
+
+    /**
+     * 还原(支持批量)
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function restore($id = null)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+        $items = $this->model->onlyTrashed()->where('id', 'in', $id)->select();
+        Db::transaction(function () use ($items) {
+            $count = 0;
+            foreach ($items as $item) {
+                $count += $item->restore();
+            }
+
+            return $count;
+        });
+
+        $this->success('还原成功');
+    }
+
+
+    /**
+     * 销毁(支持批量)
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function destroy($id = null)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+        if ($id !== 'all') {
+            $items = $this->model->onlyTrashed()->whereIn('id', $id)->select();
+        } else {
+            $items = $this->model->onlyTrashed()->select();
+        }
+        $result = Db::transaction(function () use ($items) {
+            $count = 0;
+            foreach ($items as $comment) {
+                // 删除评价
+                $count += $comment->delete(true);
+            }
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('销毁成功', null, $result);
+        }
+        $this->error('销毁失败');
+    }
+
+}

+ 444 - 0
addons/shopro/application/admin/controller/shopro/goods/Goods.php

@@ -0,0 +1,444 @@
+<?php
+
+namespace app\admin\controller\shopro\goods;
+
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\goods\Goods as GoodsModel;
+use app\admin\model\shopro\goods\Sku as SkuModel;
+use app\admin\model\shopro\goods\SkuPrice as SkuPriceModel;
+use app\admin\model\shopro\goods\StockWarning as StockWarningModel;
+use app\admin\model\shopro\activity\Activity as ActivityModel;
+use app\admin\controller\shopro\traits\SkuPrice as SkuPriceTrait;
+use addons\shopro\traits\StockWarning as StockWarningTrait;
+use addons\shopro\service\goods\GoodsService;
+use think\Db;
+
+/**
+ * 商品管理
+ */
+class Goods extends Common
+{
+
+    use SkuPriceTrait, StockWarningTrait;
+
+    protected $noNeedRight = ['getType', 'select', 'activitySelect'];
+
+    /**
+     * 商品模型对象
+     * @var \app\admin\model\shopro\goods\Goods
+     */
+    protected $model = null;
+    protected $activityModel = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new GoodsModel;
+        $this->activityModel = new ActivityModel;
+    }
+
+
+    /**
+     * 查看
+     *
+     * @return string|Json
+     * @throws \think\Exception
+     * @throws DbException
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $goodsTableName = $this->model->getQuery()->getTable();
+
+        $goods = $this->model->sheepFilter()->with(['max_sku_price']);
+
+        // 聚合库存 (包含下架的规格)
+        $skuSql = SkuPriceModel::field('sum(stock) as stock, goods_id as sku_goods_id')->group('goods_id')->buildSql();
+        $goods = $goods->join([$skuSql => 'sp'], $goodsTableName . '.id = sp.sku_goods_id', 'left')
+            ->field("$goodsTableName.*, sp.stock")       // ,score.*
+            ->paginate($this->request->param('list_rows', 10))->each(function ($goods) {
+                // 获取活动信息
+                $goods->activities = $goods->activities;
+                $goods->promos = $goods->promos;
+
+                $data_type = request()->param('data_type', '');       // 特殊 type 需要处理的数据
+                if ($data_type == 'score_shop') {
+                    $goods->is_score_shop = $goods->is_score_shop;
+                }
+            });
+
+        $this->success('获取成功', null, $goods);
+    }
+
+
+    // 获取数据类型
+    public function getType()
+    {
+        $activityTypes = $this->activityModel->typeList();
+        $statusList = $this->model->statusList();
+
+        $result = [
+            'activity_type' => $activityTypes,
+            'status' => $statusList
+        ];
+
+        $data = [];
+        foreach ($result as $key => $list) {
+            $data[$key][] = ['name' => '全部', 'type' => 'all'];
+
+            foreach ($list as $k => $v) {
+                $data[$key][] = [
+                    'name' => $v,
+                    'type' => $k
+                ];
+            }
+        }
+
+        $this->success('获取成功', null, $data);
+    }
+
+
+
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only([
+            'type', 'title', 'subtitle', 'category_ids', 'image', 'images', 'image_wh', 'params',
+            'original_price', 'price', 'is_sku', 'limit_type', 'limit_num', 'sales_show_type',
+            'stock_show_type', 'show_sales', 'service_ids', 'dispatch_type', 'dispatch_id', 'is_offline', 'status', 'weigh',
+        ]);         // likes, views, sales,
+        $params['content'] = $this->request->param('content', '', null);      // content 不经过全局过滤
+        $this->svalidate($params, ".add");
+        if (!$params['is_sku']) {
+            // 校验单规格属性
+            $sku_params = $this->request->only(['stock', 'stock_warning', 'sn', 'weight', 'cost_price', 'original_price', 'price']);
+            $this->svalidate($sku_params, '.sku_params');
+        }
+
+        $data = Db::transaction(function () use ($params) {
+            $this->model->save($params);
+
+            $this->editSku($this->model, 'add');
+        });
+        $this->success('保存成功', null, $data);
+    }
+
+
+
+
+    /**
+     * 详情
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function detail($id)
+    {
+        $goods = $this->model->where('id', $id)->find();
+        if (!$goods) {
+            $this->error(__('No Results were found'));
+        }
+        $goods->category_ids_arr = $goods->category_ids_arr;
+
+        if ($goods->is_sku) {
+            $goods->skus = $goods->skus;
+            $goods->sku_prices = $goods->sku_prices;
+        } else {
+            // 将单规格的部分数据直接放到 row 上
+            $goodsSkuPrice = SkuPriceModel::where('goods_id', $id)->order('id', 'asc')->find();
+
+            $goods->stock = $goodsSkuPrice->stock;
+            $goods->sn = $goodsSkuPrice->sn;
+            $goods->weight = $goodsSkuPrice->weight;
+            $goods->stock_warning = $goodsSkuPrice->stock_warning;
+            $goods->cost_price = $goodsSkuPrice->cost_price;
+        }
+
+        $content = $goods['content'];
+        $goods = $goods->toArray();
+        $goods['content'] = $content;
+        $this->success('保存成功', null, $goods);
+    }
+
+
+    /**
+     * 编辑(支持批量)
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->only([
+            'type', 'title', 'subtitle', 'image', 'images', 'image_wh', 'params',
+            'original_price', 'price', 'is_sku', 'limit_type', 'limit_num', 'sales_show_type',
+            'stock_show_type', 'show_sales', 'service_ids', 'dispatch_type', 'dispatch_id', 'is_offline', 'status', 'weigh',
+        ]);         // likes, views, sales,
+        $this->request->has('content') && $params['content'] = $this->request->param('content', '', null);      // content 不经过全局过滤
+        $this->svalidate($params);
+        isset($params['is_sku']) && $params['category_ids'] = $this->request->param('category_ids', '');        // 分类不判空
+        isset($params['is_sku']) && $params['params'] = $this->request->param('params/a', []);        // 编辑如果没有传 params 赋值为空
+        if (isset($params['is_sku']) && !$params['is_sku']) {
+            // 校验单规格属性
+            $sku_params = $this->request->only(['stock_warning', 'sn', 'weight', 'cost_price', 'original_price', 'price']);
+            $this->svalidate($sku_params, 'sku_params');
+        }
+
+        $id = explode(',', $id);
+
+        $items = $this->model->whereIn('id', $id)->select();
+        Db::transaction(function () use ($items, $params) {
+            foreach ($items as $goods) {
+                $goods->save($params);
+
+                if (isset($params['is_sku'])) {
+                    // 编辑商品(如果没有 is_sku 就是批量编辑上下架等)
+                    $this->editSku($goods, 'edit');
+                }
+            }
+        });
+
+        $this->success('更新成功', null);
+    }
+
+
+    public function addStock($id) 
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $goods = $this->model->where('id', $id)->find();
+        if (!$goods) {
+            $this->error(__('No Results were found'));
+        }
+        if ($goods->is_sku) {
+            // 多规格
+            $skuPrices = $this->request->post('sku_prices/a', []);
+            foreach ($skuPrices as $skuPrice) {
+                if (isset($skuPrice['add_stock']) && $skuPrice['add_stock'] != 0 && $skuPrice['id']) {
+                    $skuPriceModel = SkuPriceModel::where('goods_id', $id)->order('id', 'asc')->find($skuPrice['id']);
+                    if ($skuPriceModel) {
+                        Db::transaction(function () use ($skuPriceModel, $skuPrice) {
+                            $this->addStockToSkuPrice($skuPriceModel, $skuPrice['add_stock'], 'goods');
+                        });
+                    }
+                }
+            }
+        } else {
+            $add_stock = $this->request->param('add_stock', 0);
+            $skuPriceModel = SkuPriceModel::where('goods_id', $id)->order('id', 'asc')->find();
+
+            if ($skuPriceModel) {
+                Db::transaction(function () use ($skuPriceModel, $add_stock) {
+                    $this->addStockToSkuPrice($skuPriceModel, $add_stock, 'goods');
+                });
+            }
+        }
+
+        $this->success('补货成功');
+    }
+
+
+
+    public function select()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $type = $this->request->param('type', 'page');
+        $goodsTableName = $this->model->getQuery()->getTable();
+
+        $goods = $this->model->sheepFilter()->with(['max_sku_price']);
+
+        // 聚合库存 (包含下架的规格)
+        $skuSql = SkuPriceModel::field('sum(stock) as stock, goods_id as sku_goods_id')->group('goods_id')->buildSql();
+        $goods = $goods->join([$skuSql => 'sp'], $goodsTableName . '.id = sp.sku_goods_id', 'left')
+            ->field("$goodsTableName.*, sp.stock");       // ,score.*
+
+        if ($type == 'select') {
+            // 普通结果
+            $goods = collection($goods->select());
+        } else {
+            // 分页结果
+            $goods = $goods->paginate($this->request->param('list_rows', 10));
+        }
+
+        $goods = $goods->each(function ($goods) {
+            // 获取活动信息
+            $goods->activities = $goods->activities;
+            $goods->promos = $goods->promos;
+
+            $data_type = $this->request->param('data_type', '');       // 特殊 type 需要处理的数据
+            if ($data_type == 'score_shop') {
+                $goods->is_score_shop = $goods->is_score_shop;
+            }
+        });
+
+        $this->success('获取成功', null, $goods);
+    }
+
+
+
+    /**
+     * 获取指定活动相关商品
+     *
+     * @param Request $request
+     * @return void
+     */
+    public function activitySelect()
+    {
+        $activity_id = $this->request->param('activity_id');
+        $need_buyers = $this->request->param('need_buyers', 0);      // 需要查询哪些人在参与活动
+        $activity = $this->activityModel->where('id', $activity_id)->find();
+        if (!$activity) {
+            $this->error(__('No Results were found'));
+        }
+        $goodsIds = $activity->goods_ids ? explode(',', $activity->goods_ids) : [];
+
+        // 存一下,获取器获取指定活动的时候会用到
+        foreach ($goodsIds as $id) {
+            session('goods-activity_id:' . $id, $activity_id);
+        }
+        $service = new GoodsService(function ($goods) use ($need_buyers) {
+            if ($need_buyers) {
+                $goods->buyers = $goods->buyers;
+            }
+            $goods->activity = $goods->activity;
+            return $goods;
+        });
+
+        $goods = $service->activity($activity_id)->whereIds($goodsIds)->show()->select();
+        $goods = collection($goods)->toArray();     // 可以将里面的单个 model也转为数组
+        foreach ($goods as &$gd) {
+            unset($gd['new_sku_prices'], $gd['activity']);
+        }
+        $this->success('获取成功', null, $goods);
+    }
+
+
+
+
+    /**
+     * 删除(支持批量)
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list) {
+            $count = 0;
+            foreach ($list as $item) {
+                // 删除相关库存预警记录
+                StockWarningModel::destroy(function ($query) use ($item) {
+                    $query->where('goods_id', $item->id);
+                });
+                $count += $item->delete();
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('删除成功', null, $result);
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+
+
+    public function recyclebin()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $goods = $this->model->onlyTrashed()->sheepFilter()->paginate($this->request->param('list_rows', 10));
+        $this->success('获取成功', null, $goods);
+    }
+
+
+    /**
+     * 还原(支持批量)
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function restore($id = null)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $items = $this->model->onlyTrashed()->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($items) {
+            $count = 0;
+            foreach ($items as $item) {
+                $count += $item->restore();
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('还原成功', null, $result);
+        } else {
+            $this->error(__('No rows were updated'));
+        }
+    }
+
+
+    /**
+     * 销毁(支持批量)
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function destroy($id = null)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        if ($id !== 'all') {
+            $items = $this->model->onlyTrashed()->whereIn('id', $id)->select();
+        } else {
+            $items = $this->model->onlyTrashed()->select();
+        }
+        $result = Db::transaction(function () use ($items) {
+            $count = 0;
+            foreach ($items as $goods) {
+                // 删除商品相关的规格,规格记录
+                SkuModel::where('goods_id', $goods->id)->delete();
+                SkuPriceModel::where('goods_id', $goods->id)->delete();
+
+                // 删除商品
+                $count += $goods->delete(true);
+            }
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('销毁成功', null, $result);
+        }
+        $this->error('销毁失败');
+    }
+}

+ 157 - 0
addons/shopro/application/admin/controller/shopro/goods/Service.php

@@ -0,0 +1,157 @@
+<?php
+
+namespace app\admin\controller\shopro\goods;
+
+use app\admin\controller\shopro\Common;
+use think\Db;
+use app\admin\model\shopro\goods\Service as ServiceModel;
+
+/**
+ * 服务保障
+ */
+class Service extends Common
+{
+
+    protected $noNeedRight = ['select'];
+    
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new ServiceModel;
+    }
+
+
+    /**
+     * 服务保障列表
+     *
+     * @return \think\Response
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $services = $this->model->sheepFilter()->paginate(request()->param('list_rows', 10));
+
+        $this->success('获取成功', null, $services);
+    }
+
+
+
+
+    /**
+     * 添加服务保障
+     *
+     * @return \think\Response
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only([
+            'name', 'image', 'description'
+        ]);
+        $this->svalidate($params, ".add");
+
+        Db::transaction(function () use ($params) {
+            $this->model->save($params);
+        });
+        $this->success('保存成功');
+    }
+
+
+    /**
+     * 服务保障详情
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function detail($id)
+    {
+        $service = $this->model->where('id', $id)->find();
+
+        if (!$service) {
+            $this->error(__('No Results were found'));
+        }
+
+        $this->success('获取成功', null, $service);
+    }
+
+
+
+    /**
+     * 修改服务保障
+     *
+     * @return \think\Response
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $params = $this->request->only([
+            'name', 'image', 'description'
+        ]);
+        $this->svalidate($params, ".edit");
+
+        $id = explode(',', $id);
+        $list = $this->model->whereIn('id', $id)->select();
+        $result = Db::transaction(function () use ($list, $params) {
+            $count = 0;
+            foreach ($list as $item) {
+                $params['id'] = $item->id;
+                $count += $item->save($params);
+            }
+            return $count;
+        });
+        if ($result) {
+            $this->success('更新成功', null, $result);
+        } else {
+            $this->error('更新失败,未改变任何记录');
+        }
+    }
+
+
+
+    /**
+     * 删除服务标签
+     *
+     * @param string $id 要删除的服务保障列表
+     * @return void
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $list = $this->model->where('id', 'in', $id)->select();
+        Db::transaction(function () use ($list) {
+            $count = 0;
+            foreach ($list as $item) {
+                $count += $item->delete();
+            }
+
+            return $count;
+        });
+
+        $this->success('删除成功');
+    }
+
+
+    /**
+     * 获取所有服务列表
+     *
+     * @return \think\Response
+     */
+    public function select()
+    {
+        $services = $this->model->field('id, name')->select();
+
+        $this->success('获取成功', null, $services);
+    }
+}

+ 57 - 0
addons/shopro/application/admin/controller/shopro/goods/SkuPrice.php

@@ -0,0 +1,57 @@
+<?php
+
+namespace app\admin\controller\shopro\goods;
+
+use app\admin\controller\shopro\Common;
+use think\Db;
+use app\admin\model\shopro\goods\SkuPrice as SkuPriceModel;
+
+class SkuPrice extends Common
+{
+
+    protected $noNeedRight = ['index'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new SkuPriceModel;
+    }
+
+    /**
+     * skuPrices列表
+     *
+     * @return \think\Response
+     */
+    public function index()
+    {
+        $goods_id = $this->request->param('goods_id');
+        $skuPrices = $this->model->where('goods_id', $goods_id)->select();
+
+        $this->success('获取成功', null, $skuPrices);
+    }
+
+
+
+    /**
+     * skuPrices编辑
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function edit($id = null)
+    {
+        $params = $this->request->only([
+            'status',
+        ]);
+
+        $id = explode(',', $id);
+        $items = $this->model->whereIn('id', $id)->select();
+        Db::transaction(function () use ($items, $params) {
+            foreach ($items as $skuPrice) {
+                $skuPrice->save($params);
+            }
+        });
+
+        $this->success('更新成功');
+    }
+}

+ 48 - 0
addons/shopro/application/admin/controller/shopro/goods/StockLog.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace app\admin\controller\shopro\goods;
+
+use app\admin\controller\shopro\Common;
+use think\Db;
+use app\admin\model\shopro\goods\SkuPrice as SkuPriceModel;
+use app\admin\model\shopro\goods\StockLog as StockLogModel;
+use addons\shopro\library\Operator;
+
+/**
+ * 补库存记录
+ */
+class StockLog extends Common
+{
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new StockLogModel;
+    }
+
+
+    /**
+     * 库存补货列表
+     *
+     * @return \think\Response
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $skuPriceTableName = (new SkuPriceModel())->getQuery()->getTable();
+        $stockLogs = $this->model->sheepFilter()->alias('g')->with(['goods' => function ($query) {
+                $query->removeOption('soft_delete');
+            }, 'oper'])
+            ->join($skuPriceTableName . ' sp', 'g.goods_sku_price_id = sp.id', 'left')
+            ->field('g.*,sp.stock as total_stock')
+            ->paginate($this->request->param('list_rows', 10))->toArray();
+                    // 解析操作人信息
+        foreach ($stockLogs['data'] as &$log) {
+            $log['oper'] = Operator::info('admin', $log['oper'] ?? null);
+        }
+        $this->success('获取成功', null, $stockLogs);
+    }
+}

+ 120 - 0
addons/shopro/application/admin/controller/shopro/goods/StockWarning.php

@@ -0,0 +1,120 @@
+<?php
+
+namespace app\admin\controller\shopro\goods;
+
+use app\admin\controller\shopro\Common;
+use think\Db;
+use app\admin\model\shopro\goods\SkuPrice as SkuPriceModel;
+use app\admin\model\shopro\goods\StockWarning as StockWarningModel;
+use addons\shopro\traits\StockWarning as StockWarningTrait;
+
+/**
+ * 库存预警
+ */
+class StockWarning extends Common
+{
+    use StockWarningTrait;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new StockWarningModel;
+    }
+
+
+    /**
+     * 库存预警列表
+     *
+     * @return \think\Response
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $skuPriceTableName = (new SkuPriceModel())->getQuery()->getTable();
+        $stockWarnings = $this->model->sheepFilter()->alias('g')->with(['goods' => function ($query) {
+                $query->removeOption('soft_delete');
+            }])
+            ->join($skuPriceTableName . ' sp', 'g.goods_sku_price_id = sp.id', 'left')
+            ->field('g.*,sp.stock')
+            ->paginate($this->request->param('list_rows', 10));
+
+        $warning_total = $this->model->sheepFilter(false, function ($filters) {
+                $filters['stock_type'] = 'no_enough';
+                return $filters;
+            })->alias('g')
+            ->join($skuPriceTableName . ' sp', 'g.goods_sku_price_id = sp.id', 'left')
+            ->field('g.*,sp.stock')
+            ->count();
+        $over_total = $this->model->sheepFilter(false, function ($filters) {
+                $filters['stock_type'] = 'over';
+                return $filters;
+            })->alias('g')
+            ->join($skuPriceTableName . ' sp', 'g.goods_sku_price_id = sp.id', 'left')
+            ->field('g.*,sp.stock')
+            ->count();
+
+        $result = [
+            'rows' => $stockWarnings,
+            'warning_total' => $warning_total,
+            'over_total' => $over_total,
+        ];
+
+        $this->success('获取成功', null, $result);
+    }
+
+
+
+
+    /**
+     * 补货
+     *
+     * @param [type] $ids
+     * @param [type] $stock
+     * @return void
+     */
+    public function addStock ($id) {
+        if ($this->request->isAjax()) {
+            $params = $this->request->only(['stock']);
+            $this->svalidate($params, ".add");
+
+            $stockWarning = $this->model->with(['sku_price'])->where('id', $id)->find();
+            if (!$stockWarning) {
+                $this->error(__('No Results were found'));
+            }
+            if (!$stockWarning->sku_price) {
+                $this->error('库存规格不存在');
+            }
+
+            Db::transaction(function () use ($stockWarning, $params) {
+                // 补货
+                $this->addStockToSkuPrice($stockWarning->sku_price, $params['stock'], 'stock_warning');
+            });
+
+            $this->success('补货成功');
+        }
+
+        return $this->view->fetch();
+
+    }
+
+
+    public function recyclebin()
+    {
+        if ($this->request->isAjax()) {
+            $skuPriceTableName = (new SkuPriceModel())->getQuery()->getTable();
+            $stockWarnings = $this->model->onlyTrashed()->sheepFilter()->alias('g')->with(['goods' => function ($query) {
+                    $query->removeOption('soft_delete');
+                }])
+                ->join($skuPriceTableName . ' sp', 'g.goods_sku_price_id = sp.id', 'left')
+                ->field('g.*,sp.stock')
+                ->paginate($this->request->param('list_rows', 10));
+
+            $this->success('获取成功', null, $stockWarnings);
+        }
+
+        return $this->view->fetch();
+    }
+}

+ 227 - 0
addons/shopro/application/admin/controller/shopro/notification/Config.php

@@ -0,0 +1,227 @@
+<?php
+
+namespace app\admin\controller\shopro\notification;
+
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\notification\Config as ConfigModel;
+use app\admin\controller\shopro\notification\traits\Notification as NotificationTraits;
+use addons\shopro\facade\Wechat;
+
+class Config extends Common
+{
+    use NotificationTraits;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new ConfigModel;
+    }
+
+
+
+    /**
+     * 消息通知配置
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $receiver_type = $this->request->param('receiver_type');
+
+        $notifications = $this->getNotificationsByReceiverType($receiver_type);
+
+        $groupConfigs = $this->getGroupConfigs();
+        foreach ($notifications as $key => &$notification) {
+            $currentConfigs = $groupConfigs[$notification['event']] ?? [];
+            foreach ($notification['channels'] as $channel) {
+                $notification['configs'][$channel] = [
+                    'status' => isset($currentConfigs[$channel]) ? $currentConfigs[$channel]['status'] : 'disabled',
+                    'send_num' => isset($currentConfigs[$channel]) ? $currentConfigs[$channel]['send_num'] : 0,
+                ];
+            }
+        }
+
+        $this->success('获取成功', null, $notifications);
+    }
+
+
+
+    public function detail()
+    {
+        $event = $this->request->param('event');
+        $channel = $this->request->param('channel');
+        if (!$event || !$channel) {
+            error_stop('参数错误');
+        }
+
+        $notification = $this->getNotificationByEvent($event);
+
+        $notification = $this->formatNotification($notification, $event, $channel);
+
+        $this->success('获取成功', null, $notification);
+    }
+
+
+    // 编辑配置
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $event = $this->request->param('event');
+        $channel = $this->request->param('channel');
+        if ($channel == 'Email') {
+            $content = $this->request->param('content', '');
+        } else {
+            $content = $this->request->param('content/a', []);
+        }
+        $type = $this->request->param('type', 'default');
+        if (!$event || !$channel) {
+            error_stop('参数错误');
+        }
+
+        $config = $this->model->where('event', $event)->where('channel', $channel)->find();
+
+        if (!$config) {
+            $config = $this->model;
+            $config->event = $event;
+            $config->channel = $channel;
+        }
+
+        if (in_array($channel, ['WechatOfficialAccount', 'WechatMiniProgram']) && $type == 'default') {
+            // 自动组装微信默认模板
+            $content['fields'] = $this->formatWechatTemplateFields($event, $channel, $content['fields']);
+        }
+
+        $config->type = $type;
+        $config->content = $content;
+        $config->save();
+
+        $this->success('设置成功');
+    }
+
+
+
+    // 配置状态
+    public function setStatus($event, $channel)
+    {
+        $event = $this->request->param('event');
+        $channel = $this->request->param('channel');
+        $status = $this->request->param('status', 'disabled');
+
+        if (!$event || !$channel) {
+            $this->error('参数错误');
+        }
+        
+        $config = $this->model->where('event', $event)->where('channel', $channel)->find();
+        if (!$config) {
+            $config = $this->model;
+            $config->event = $event;
+            $config->channel = $channel;
+            $config->type = 'default';
+        }
+        $config->status = $status;
+        $config->save();
+
+        $this->success('设置成功');
+    }
+
+
+    /**
+     * 自动获取微信模板 id
+     */
+    public function getTemplateId()
+    {
+        $event = $this->request->param('event');
+        $channel = $this->request->param('channel');
+        $is_delete = $this->request->param('is_delete', 0);
+        $template_id = $this->request->param('template_id', '');
+        if (!$event || !$channel) {
+            error_stop('参数错误');
+        }
+
+        $notification = $this->getNotificationByEvent($event);
+
+        $template = $notification['template'][$channel] ?? null;
+        if (!$template) {
+            $this->error('模板不存在');
+        }
+
+        // 请求微信接口
+        switch ($channel) {
+            case 'WechatMiniProgram':           // 小程序订阅消息
+                $requestParams['tid'] = $template['tid'];
+                $requestParams['kid'] = $template['kid'];
+                $requestParams['sceneDesc'] = $template['scene_desc'];
+                if (!$requestParams['tid'] || !$requestParams['kid']) {
+                    $this->error('缺少模板参数');
+                }
+                $wechat = Wechat::miniProgram()->subscribe_message;
+                $delete_method = 'deleteTemplate';
+                $result_key = 'priTmplId';
+                break;
+            // case 'WechatOfficialAccount':       // 公众号模板消息
+            //     $requestParams['template_id'] = $template['temp_no'];
+            //     if (!$requestParams['template_id']) {
+            //         $this->error('缺少模板参数,获取失败');
+            //     }
+            //     $wechat = Wechat::officialAccount()->template_message;    // 微信管理
+            //     $result_key = 'template_id';
+            //     $delete_method = 'deletePrivateTemplate';
+            //     break;
+            case 'WechatOfficialAccount':       // 新版公众号模板消息
+                $requestParams['template_id'] = $template['temp_no'];
+                $requestParams['keywords'] = $template['keywords'];
+
+                if (!$requestParams['template_id']) {
+                    $this->error('公众号类目模板库目前不完善,请自行在公众号后台->模板消息->选择模板配置');
+                }
+                if (!$requestParams['keywords']) {
+                    $this->error('缺少模板关键字,获取失败');
+                }
+
+                $wechat = new \addons\shopro\library\easywechatPlus\WechatOfficialTemplate(Wechat::officialAccount());
+
+                $result_key = 'template_id';
+                $delete_method = 'deletePrivateTemplate';
+                break;
+            case 'WechatOfficialAccountBizsend':       // 公众号订阅消息(待补充)
+                $requestParams['tid'] = $template['tid'];
+                $requestParams['kid'] = $template['kid'];
+                if (!$requestParams['tid'] || !$requestParams['kid']) {
+                    $this->error('缺少模板参数,获取失败');
+                }
+                $wechat = Wechat::officialAccount()->subscribe_message;   // 微信管理
+                $result_key = 'priTmplId';
+                $delete_method = 'deleteTemplate';
+                break;
+            default:
+                $this->error('当前发送渠道不能获取模板');
+                break;
+        }
+
+        $result = $wechat->addTemplate(...array_values($requestParams));
+
+        if ($result['errcode'] != 0) {
+            $this->error('获取失败: errcode:' . $result['errcode'] . '; errmsg:' . $result['errmsg']);
+        } else {
+            if ($is_delete) {
+                // 删除传入的老模板
+                if ($template_id) {
+                    $deleteResult = $wechat->{$delete_method}($template_id);
+                }
+                // 删除数据库的老模板
+                $config = $this->model->where('event', $event)->where('channel', $channel)->find();
+                $template_id = $config ? ($config->content['template_id'] ?? null) : null;
+                if ($template_id) {
+                    $deleteResult = $wechat->{$delete_method}($template_id);
+                }
+            }
+        }
+
+        $this->success('获取成功', null, ($result[$result_key] ?? null));
+    }
+}

+ 128 - 0
addons/shopro/application/admin/controller/shopro/notification/Notification.php

@@ -0,0 +1,128 @@
+<?php
+
+namespace app\admin\controller\shopro\notification;
+
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\notification\Notification as NotificationModel;
+use app\admin\model\shopro\Admin;
+
+class Notification extends Common
+{
+
+    protected $noNeedRight = ['index', 'read', 'delete', 'notificationType'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new NotificationModel;
+    }
+
+
+    /**
+     * 获取管理员的消息列表
+     *
+     * @return \think\Response
+     */
+    public function index()
+    {
+        $admin = auth_admin();
+        $admin = Admin::where('id', $admin['id'])->find();
+
+        $notifiable_type = $admin->getNotifiableType();
+        $notifications = NotificationModel::sheepFilter(false)
+            ->where('notifiable_type', $notifiable_type)
+            ->where('notifiable_id', $admin['id'])
+            ->order('createtime', 'desc')
+            ->paginate($this->request->param('list_rows', 10));
+
+        $this->success('消息列表', null, $notifications);
+    }
+
+
+    /**
+     * 指定消息标记已读
+     *
+     * @param string $id
+     * @return void
+     */
+    public function read($id)
+    {
+        $admin = auth_admin();
+        $admin = Admin::where('id', $admin['id'])->find();
+
+        $notifiable_type = $admin->getNotifiableType();
+        $notification = NotificationModel::sheepFilter()
+            ->where('notifiable_type', $notifiable_type)
+            ->where('notifiable_id', $admin['id'])
+            ->where('id', $id)
+            ->find();
+        
+        if (!$notification) {
+            $this->error(__('No Results were found'));
+        }
+
+        $notification->read_time = time();
+        $notification->save();
+
+        $this->success('已读成功', null, $notification);
+    }
+
+
+    /**
+     * 删除已读消息
+     *
+     * @return void
+     */
+    public function delete()
+    {
+        $admin = auth_admin();
+        $admin = Admin::where('id', $admin['id'])->find();
+
+        // 将已读的消息全部删除
+        $notifiable_type = $admin->getNotifiableType();
+        NotificationModel::sheepFilter()
+            ->where('notifiable_type', $notifiable_type)
+            ->where('notifiable_id', $admin['id'])
+            ->whereNotNull('read_time')
+            ->delete();
+
+        $this->success('删除成功');
+    }
+
+
+
+
+    /**
+     * 消息类别,以及未读消息数
+     *
+     * @return void
+     */
+    public function notificationType()
+    {
+        $admin = auth_admin();
+
+        $notificationType = NotificationModel::$notificationType;
+
+        $newType = [];
+        foreach ($notificationType as $type => $name) {
+            // 未读消息数
+            $unread_num = NotificationModel::where('notifiable_type', 'admin')->where('notifiable_id', $admin['id'])
+                ->notificationType($type)
+                ->where('read_time', null)
+                ->order('createtime', 'desc')->count();
+
+            $newType[] = [
+                'label' => $name,
+                'value' => $type,
+                'unread_num' => $unread_num
+            ];
+        }
+
+        $result = [
+            'unread_num' => array_sum(array_column($newType, 'unread_num')),
+            'notification_type' => $newType
+        ];
+
+        $this->success('消息类型', null, $result);
+    }
+}

+ 183 - 0
addons/shopro/application/admin/controller/shopro/notification/traits/Notification.php

@@ -0,0 +1,183 @@
+<?php
+
+namespace app\admin\controller\shopro\notification\traits;
+
+/**
+ * 消息通知,额外方法
+ */
+trait Notification
+{
+
+    protected $notificationTypes = [
+        \addons\shopro\notification\order\OrderNew::class,
+        \addons\shopro\notification\order\OrderDispatched::class,
+        \addons\shopro\notification\order\aftersale\OrderAftersaleChange::class,
+        \addons\shopro\notification\order\aftersale\OrderAdminAftersaleChange::class,
+        \addons\shopro\notification\order\OrderRefund::class,
+        \addons\shopro\notification\order\OrderApplyRefund::class,
+        // 商品
+        \addons\shopro\notification\goods\StockWarning::class,
+        // 钱包
+        \addons\shopro\notification\wallet\CommissionChange::class,
+        \addons\shopro\notification\wallet\MoneyChange::class,
+        \addons\shopro\notification\wallet\ScoreChange::class,
+        // 活动
+        \addons\shopro\notification\activity\GrouponFail::class,
+        \addons\shopro\notification\activity\GrouponFinish::class,
+    ];
+
+
+    /**
+     * 根据接收人类型,获取消息类型
+     *
+     * @param array|string $receiverType
+     * @return array
+     */
+    protected function getNotificationsByReceiverType($receiverType = 'user')
+    {
+        $receiverType = is_array($receiverType) ? $receiverType : [$receiverType];
+
+        $notifications = $this->getNotifications();
+
+        $receiverNotifications = [];
+        foreach ($notifications as $notification) {
+            if (in_array($notification['receiver_type'], $receiverType)) {
+                $receiverNotifications[] = $notification;
+            }
+        }
+
+        return $receiverNotifications;
+    }
+
+
+
+    /**
+     * 根据事件类型获取消息
+     *
+     * @param string $event
+     * @return void
+     */
+    protected function getNotificationByEvent($event)
+    {
+        $notifications = $this->getNotifications();
+
+        $notifications = array_column($notifications, null, 'event');
+        return $notifications[$event] ?? null;
+    }
+
+
+
+    /**
+     * 按照事件类型获取配置分组
+     *
+     * @param string $event
+     * @return array
+     */
+    protected function getGroupConfigs($event = null)
+    {
+        // 获取所有配置
+        $configs = $this->model->select();
+        $newConfigs = [];
+        foreach ($configs as $config) {
+            $newConfigs[$config['event']][$config['channel']] = $config;
+        }
+
+        return $event ? ($newConfigs[$event] ?? []) : $newConfigs;
+    }
+
+
+
+    /**
+     * 获取所有消息类型
+     *
+     * @return array
+     */
+    protected function getNotifications()
+    {
+        $types = [];
+        foreach ($this->notificationTypes as $key => $class_name) {
+            $class = new $class_name();
+            $currentFields = $class->returnField;
+            $currentFields['event'] = $class->event;
+            $currentFields['receiver_type'] = $class->receiver_type;
+            $currentFields['template'] = $class->template;
+
+            $types[] = $currentFields;
+        }
+
+        return $types;
+    }
+
+
+
+    /**
+     * 格式化详情返回结果
+     *
+     * @param array $notification
+     * @param string $event
+     * @param string $channel
+     * @return array
+     */
+    protected function formatNotification($notification, $event, $channel) 
+    {
+        $currentConfigs = $this->getGroupConfigs($event);
+        $currentConfig = $currentConfigs[$channel] ?? null;
+        
+        if (in_array($channel, ['WechatOfficialAccount', 'WechatMiniProgram', 'WechatOfficialAccountBizsend'])) {
+            $currentTemplate = $notification['template'][$channel] ?? [];
+            unset($notification['template']);
+            $notification['wechat'] = $currentTemplate;
+        }
+        
+        $notification['type'] = $currentConfig['type'] ?? 'default';
+        $content = $currentConfig['content'] ?? null;
+        if (!is_array($content)) {
+            $notification['content_text'] = $content;
+        }
+        if ($content && is_array($content)) {
+            $contentFields = [];
+            if (isset($content['fields']) && $content['fields']) {    // 判断数组是否存在 fields 设置
+                $contentFields = array_column($content['fields'], null, 'field');
+            }
+
+            $tempFields = array_column($notification['fields'], null, 'field');
+            $configField = array_merge($tempFields, $contentFields);
+
+            $content['fields'] = array_values($configField);
+            $notification['content'] = $content;
+        } else {
+            $notification['content'] = [
+                'template_id' => '',
+                'fields' => $notification['fields']
+            ];
+        }
+
+        unset($notification['fields']);
+
+        return $notification;
+    }
+
+
+
+    /**
+     * 格式化微信公众号,小程序默认模板时 自动配置 模板字段
+     *
+     * @return void
+     */
+    protected function formatWechatTemplateFields($event, $channel, $fields)
+    {
+        $notification = $this->getNotificationByEvent($event);
+
+        $channelFields = $notification['template'][$channel]['fields'] ?? [];
+        $channelFields = array_column($channelFields, null, 'field');
+
+        foreach ($fields as $key => &$field) {
+            $field_name = $field['field'] ?? '';
+            if ($field_name && isset($channelFields[$field_name])) {
+                $field['template_field'] = $channelFields[$field_name]['template_field'] ?? '';
+            }
+        }
+        
+        return $fields;
+    }
+}

+ 338 - 0
addons/shopro/application/admin/controller/shopro/order/Aftersale.php

@@ -0,0 +1,338 @@
+<?php
+
+namespace app\admin\controller\shopro\order;
+
+use think\Db;
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\order\Aftersale as OrderAftersaleModel;
+use app\admin\model\shopro\order\AftersaleLog as OrderAftersaleLogModel;
+use app\admin\model\shopro\order\Order as OrderModel;
+use app\admin\model\shopro\order\OrderItem as OrderItemModel;
+use app\admin\model\shopro\order\Action as OrderActionModel;
+use addons\shopro\service\order\OrderRefund;
+use addons\shopro\library\Operator;
+
+class Aftersale extends Common
+{
+
+    protected $noNeedRight = ['getType'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new OrderAftersaleModel;
+        $this->orderModel = new OrderModel;
+    }
+
+    /**
+     * 售后单列表
+     *
+     * @return \think\Response
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        // 查询主表是订单表
+        $orders = $this->orderModel->withTrashed()->sheepFilter()->with(['user', 'aftersales' => function ($query) {
+                $query->removeOption('soft_delete');
+            }])->paginate(request()->param('list_rows', 10));
+
+        $this->success('获取成功', null, $orders);
+    }
+
+
+    // 获取数据类型
+    public function getType()
+    {
+        $type = $this->model->typeList();
+        $dispatchStatus = $this->model->dispatchStatusList();
+        $aftersaleStatus = $this->model->aftersaleStatusList();
+        $refundStatus = $this->model->refundStatusList();
+
+        $result = [
+            'type' => $type,
+            'dispatch_status' => $dispatchStatus,
+            'aftersale_status' => $aftersaleStatus,
+            'refund_status' => $refundStatus,
+        ];
+
+        $data = [];
+        foreach ($result as $key => $list) {
+            $data[$key][] = ['name' => '全部', 'type' => 'all'];
+
+            foreach ($list as $k => $v) {
+                $data[$key][] = [
+                    'name' => $v,
+                    'type' => $k
+                ];
+            }
+        }
+
+        $this->success('获取成功', null, $data);
+    }
+
+
+
+    /**
+     * 售后单详情
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function detail($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $aftersale = $this->model->withTrashed()->with(['user', 'order' => function ($query) {
+            $query->removeOption('soft_delete');
+        }, 'logs'])->where('id', $id)->find();
+        if (!$aftersale) {
+            $this->error(__('No Results were found'));
+        }
+
+        // 建议退款金额
+        $aftersale->suggest_refund_fee = $aftersale->suggest_refund_fee;
+
+        // 多态关联 oper
+        $morphs = [
+            'user' => \app\admin\model\shopro\user\User::class,
+            'admin' => \app\admin\model\Admin::class,
+            'system' => \app\admin\model\Admin::class,
+        ];
+        $aftersale['logs'] = morph_to($aftersale['logs'], $morphs, ['oper_type', 'oper_id']);
+
+        $aftersale = $aftersale->toArray();
+        foreach ($aftersale['logs'] as &$log) {
+            $log['oper'] = Operator::info($log['oper_type'], $log['oper'] ?? null);
+        }
+        $this->success('获取成功', null, $aftersale);
+    }
+
+
+
+    /**
+     * 完成售后
+     */
+    public function completed($id)
+    {
+        $admin = $this->auth->getUserInfo();
+
+        $aftersale = $this->model->withTrashed()->canOper()->where('id', $id)->find();
+        if (!$aftersale) {
+            $this->error('售后单不存在或不可完成');
+        }
+
+        $order = $this->orderModel->withTrashed()->find($aftersale->order_id);
+        $orderItem = OrderItemModel::find($aftersale->order_item_id);
+        if (!$order || !$orderItem) {
+            $this->error('订单或订单商品不存在');
+        }
+
+        $aftersale = Db::transaction(function () use ($aftersale, $order, $orderItem, $admin) {
+            $aftersale->aftersale_status = OrderAftersaleModel::AFTERSALE_STATUS_COMPLETED;    // 售后完成
+            $aftersale->save();
+            // 增加售后单变动记录、
+            OrderAftersaleLogModel::add($order, $aftersale, $admin, 'admin', [
+                'log_type' => 'completed',
+                'content' => '售后订单已完成',
+                'images' => []
+            ]);
+
+            $orderItem->aftersale_status = OrderItemModel::AFTERSALE_STATUS_COMPLETED;
+            $orderItem->save();
+            OrderActionModel::add($order, $orderItem, $admin, 'admin', '管理员完成售后');
+
+            // 售后单完成之后
+            $data = ['aftersale' => $aftersale, 'order' => $order, 'item' => $orderItem];
+            \think\Hook::listen('order_aftersale_completed', $data);
+
+            return $aftersale;
+        });
+
+        $this->success('操作成功', null, $aftersale);
+    }
+
+
+    /**
+     * 拒绝售后
+     */
+    public function refuse($id = 0)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $admin = $this->auth->getUserInfo();
+
+        $params = $this->request->param();
+        $this->svalidate($params, '.refuse');
+
+        $aftersale = $this->model->withTrashed()->canOper()->where('id', $id)->find();
+        if (!$aftersale) {
+            $this->error('售后单不存在或不可拒绝');
+        }
+
+        $order = $this->orderModel->withTrashed()->find($aftersale->order_id);
+        $orderItem = OrderItemModel::find($aftersale->order_item_id);
+        if (!$order || !$orderItem) {
+            $this->error('订单或订单商品不存在');
+        }
+
+        $aftersale = Db::transaction(function () use ($aftersale, $order, $orderItem, $params, $admin) {
+            $aftersale->aftersale_status = OrderAftersaleModel::AFTERSALE_STATUS_REFUSE;    // 售后拒绝
+            $aftersale->save();
+            // 增加售后单变动记录
+            OrderAftersaleLogModel::add($order, $aftersale, $admin, 'admin', [
+                'log_type' => 'refuse',
+                'content' => $params['refuse_msg'],
+                'images' => []
+            ]);
+
+            $orderItem->aftersale_status = OrderItemModel::AFTERSALE_STATUS_REFUSE;    // 拒绝售后
+            $orderItem->save();
+
+            OrderActionModel::add($order, $orderItem, $admin, 'admin', '管理员拒绝订单售后:' . $params['refuse_msg']);
+
+            // 售后单拒绝后
+            $data = ['aftersale' => $aftersale, 'order' => $order, 'item' => $orderItem];
+            \think\Hook::listen('order_aftersale_refuse', $data);
+
+            return $aftersale;
+        });
+
+        $this->success('操作成功', null, $aftersale);
+    }
+
+
+    /**
+     * 同意退款
+     */
+    public function refund($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $admin = $this->auth->getUserInfo();
+
+        $params = $this->request->param();
+        $this->svalidate($params, '.refund');
+
+        $refund_money = round(floatval($params['refund_money']), 2);
+        $refund_type = $params['refund_type'] ?? 'back';
+        if ($refund_money <= 0) {
+            $this->error('请输入正确的退款金额');
+        }
+
+        $aftersale = $this->model->withTrashed()->canOper()->where('id', $id)->find();
+        if (!$aftersale) {
+            $this->error('售后单不存在或不可退款');
+        }
+
+        $order = $this->orderModel->withTrashed()->with('items')->find($aftersale->order_id);
+        if (!$order) {
+            $this->error('订单不存在');
+        }
+        $items = $order->items;
+        $items = array_column($items, null, 'id');
+
+        // 当前订单已退款总金额
+        $refunded_money = array_sum(array_column($items, 'refund_fee'));
+        // 剩余可退款金额
+        $refund_surplus_money = bcsub($order->pay_fee, (string)$refunded_money, 2);
+        // 如果退款金额大于订单支付总金额
+        if (bccomp((string)$refund_money, $refund_surplus_money, 2) === 1) {
+            $this->error('退款总金额不能大于实际支付金额');
+        }
+
+        $orderItem = $items[$aftersale['order_item_id']];
+
+        if (!$orderItem || in_array($orderItem['refund_status'], [
+            OrderItemModel::REFUND_STATUS_AGREE,
+            OrderItemModel::REFUND_STATUS_COMPLETED,
+        ])) {
+            $this->error('订单商品已退款,不能重复退款');
+        }
+
+        $aftersale = Db::transaction(function () use ($aftersale, $order, $orderItem, $refund_money, $refund_type, $refund_surplus_money, $admin) {
+            $aftersale->aftersale_status = OrderAftersaleModel::AFTERSALE_STATUS_COMPLETED;    // 售后同意
+            $aftersale->refund_status = OrderAftersaleModel::REFUND_STATUS_AGREE;    // 同意退款
+            $aftersale->refund_fee = $refund_money;     // 退款金额
+            $aftersale->save();
+
+            // 增加售后单变动记录
+            OrderAftersaleLogModel::add($order, $aftersale, $admin, 'admin', [
+                'log_type' => 'refund',
+                'content' => '售后订单已退款',
+                'images' => []
+            ]);
+
+            $orderItem->aftersale_status = OrderItemModel::AFTERSALE_STATUS_COMPLETED;
+            $orderItem->save();
+            OrderActionModel::add($order, $orderItem, $admin, 'admin', '管理员同意售后退款');
+
+            // 开始退款
+            $orderRefund = new OrderRefund($order);
+            $orderRefund->refund($orderItem, $refund_money, $admin, [
+                'refund_type' => $refund_type,
+                'remark' => '管理员同意售后退款'
+            ]);
+
+            $data = ['aftersale' => $aftersale, 'order' => $order, 'item' => $orderItem];
+            \think\Hook::listen('order_aftersale_completed', $data);
+
+            return $aftersale;
+        });
+
+        $this->success('操作成功', null, $aftersale);
+    }
+
+
+    /**
+     * 留言
+     */
+    public function addLog($id = 0)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $admin = $this->auth->getUserInfo();
+
+        $params = $this->request->param();
+        $this->svalidate($params, '.add_log');
+
+        $aftersale = $this->model->withTrashed()->where('id', $id)->find();
+        if (!$aftersale) {
+            $this->error('售后单不存在');
+        }
+
+        $order = $this->orderModel->withTrashed()->with('items')->find($aftersale->order_id);
+        if (!$order) {
+            $this->error('订单不存在');
+        }
+
+        $aftersale = Db::transaction(function () use ($order, $aftersale, $params, $admin) {
+            if ($aftersale['aftersale_status'] == 0) {
+                $aftersale->aftersale_status = OrderAftersaleModel::AFTERSALE_STATUS_ING;    // 售后处理中
+                $aftersale->save();
+            }
+
+            // 增加售后单变动记录
+            OrderAftersaleLogModel::add($order, $aftersale, $admin, 'admin', [
+                'log_type' => 'add_log',
+                'content' => $params['content'],
+                'images' => $params['images'] ?? []
+            ]);
+
+            return $aftersale;
+        });
+
+        $this->success('操作成功', null, $aftersale);
+    }
+}

+ 62 - 0
addons/shopro/application/admin/controller/shopro/order/Invoice.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace app\admin\controller\shopro\order;
+
+use think\Db;
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\order\Invoice as OrderInvoiceModel;
+use app\admin\model\shopro\order\Order as OrderModel;
+
+class Invoice extends Common
+{
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new OrderInvoiceModel;
+    }
+
+    /**
+     * 发票列表
+     *
+     * @return \think\Response
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $invoices = $this->model->sheepFilter()->with(['user', 'order', 'order_items'])
+            ->paginate(request()->param('list_rows', 10))->each(function ($invoice) {
+                $invoice->order_status = $invoice->order_status;
+                $invoice->order_status_text = $invoice->order_status_text;
+                $invoice->order_fee = $invoice->order_fee;
+            });
+
+        $this->success('获取成功', null, $invoices);
+    }
+
+
+
+    public function confirm($id) 
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->param();
+        $invoice = $this->model->waiting()->whereIn('id', $id)->find();
+        if (!$invoice) {
+            $this->error(__('No Results were found'));
+        }
+
+        $invoice->download_urls = $params['download_urls'] ?? null;
+        $invoice->invoice_amount = $params['invoice_amount'];
+        $invoice->status = 'finish';
+        $invoice->finish_time = time();
+        $invoice->save();
+
+        $this->success('开具成功', null, $invoice);
+    }
+}

+ 968 - 0
addons/shopro/application/admin/controller/shopro/order/Order.php

@@ -0,0 +1,968 @@
+<?php
+
+namespace app\admin\controller\shopro\order;
+
+use think\Db;
+use think\exception\HttpResponseException;
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\order\Order as OrderModel;
+use app\admin\model\shopro\order\Address as OrderAddressModel;
+use app\admin\model\shopro\order\OrderItem;
+use app\admin\model\shopro\Admin;
+use app\admin\model\shopro\user\User;
+use app\admin\model\shopro\order\Action as OrderActionModel;
+use app\admin\model\shopro\order\Express as OrderExpressModel;
+use addons\shopro\service\pay\PayOper;
+use addons\shopro\service\order\OrderOper;
+use addons\shopro\service\order\OrderRefund;
+use app\admin\model\shopro\Pay as PayModel;
+use addons\shopro\service\order\OrderDispatch as OrderDispatchService;
+use addons\shopro\library\express\Express as ExpressLib;
+use addons\shopro\library\Operator;
+use addons\shopro\facade\Wechat;
+
+class Order extends Common
+{
+
+    protected $noNeedRight = ['getType', 'dispatchList'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new OrderModel;
+    }
+
+    /**
+     * 订单列表
+     *
+     * @return \think\Response
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            if (sheep_config('shop.platform.WechatMiniProgram.status')) {       // 如果开启了小程序平台
+                // 设置小程序发货信息管理,消息通知跳转地址,有缓存, 不会每次都设置
+                $uploadshoppingInfo = new \addons\shopro\library\easywechatPlus\WechatMiniProgramShop(Wechat::miniProgram());
+                $uploadshoppingInfo->checkAndSetMessageJumpPath();
+            }
+
+            $exportConfig = (new \addons\shopro\library\Export())->getConfig();
+            $this->assignconfig("save_type", $exportConfig['save_type'] ?? 'download');
+            return $this->view->fetch();
+        }
+
+        $orders = $this->model->withTrashed()->sheepFilter()->with(['user', 'items', 'address', 'activity_orders'])
+            ->paginate(request()->param('list_rows', 10))->each(function ($order) {
+                $order->pay_types = $order->pay_types;
+                $order->pay_types_text = $order->pay_types_text;
+                $order->items = collection($order->items);
+                $order->items->each(function ($item) use ($order) {
+                    // 处理每个商品的 activity_order
+                    $item->activity_orders = new \think\Collection;
+                    foreach ($order->activity_orders as $activityOrder) {
+                        if ($activityOrder->goods_ids && in_array($item->goods_id, $activityOrder->goods_ids)) {
+                            $item->activity_orders->push($activityOrder);
+                        }
+                    }
+
+                    return $item;
+                });
+            })->toArray();
+
+        foreach ($orders['data'] as &$order) {
+            $order = $this->model->setOrderItemStatusByOrder($order);
+        }
+
+        $result = [
+            'orders' => $orders,
+        ];
+
+        // 查询各个状态下的订单数量
+        $searchStatus = $this->model->searchStatusList();
+        // 所有的数量
+        $result['all'] = $this->model->withTrashed()->sheepFilter(true, function ($filters) {
+            unset($filters['status']);
+            return $filters;
+        })->count();
+        foreach ($searchStatus as $status => $text) {
+            $result[$status] = $this->model->withTrashed()->sheepFilter(true, function ($filters) use ($status) {
+                $filters['status'] = $status;
+                return $filters;
+            })->count();
+        }
+
+        $this->success('获取成功', null, $result);
+    }
+
+
+    // 获取数据类型
+    public function getType()
+    {
+        $type = $this->model->typeList();
+        $payType = (new PayModel)->payTypeList();
+        $platform = $this->model->platformList();
+        $classify = (new \app\admin\model\shopro\activity\Activity)->classifies();
+        $activityType = $classify['activity'];
+        $promoType = $classify['promo'];
+        $applyRefundStatus = $this->model->applyRefundStatusList();
+        $status = $this->model->searchStatusList();
+
+        $result = [
+            'type' => $type,
+            'pay_type' => $payType,
+            'platform' => $platform,
+            'activity_type' => $activityType,
+            'promo_types' => $promoType,
+            'apply_refund_status' => $applyRefundStatus,
+            'status' => $status
+        ];
+
+        $data = [];
+        foreach ($result as $key => $list) {
+            $data[$key][] = ['name' => '全部', 'type' => 'all'];
+
+            foreach ($list as $k => $v) {
+                $data[$key][] = [
+                    'name' => $v,
+                    'type' => $k
+                ];
+            }
+        }
+
+        $this->success('获取成功', null, $data);
+    }
+
+
+
+
+
+    /**
+     * 订单详情
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function detail($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        // 更新包裹信息(5分钟缓存)
+        (new ExpressLib)->updateOrderExpress($id);
+
+        $order = $this->model->withTrashed()->with(['user', 'items', 'address', 'activity_orders', 'pays', 'invoice'])->where('id', $id)->find();
+        if (!$order) {
+            $this->error(__('No Results were found'));
+        }
+        $order->express = $order->express;
+        if ($order->invoice) {
+            $order->invoice->order_status = $order->invoice->order_status;
+            $order->invoice->order_status_text = $order->invoice->order_status_text;
+            $order->invoice->order_fee = $order->invoice->order_fee;
+        }
+
+        foreach ($order->activity_orders as $activityOrder) {
+            // 处理每个活动中参与的商品
+            $activityOrder->items = new \think\Collection();
+            foreach ($order->items as $item) {
+                if ($activityOrder->goods_ids && in_array($item->goods_id, $activityOrder->goods_ids)) {
+                    $activityOrder->items->push($item);
+                }
+            }
+        }
+
+        foreach ($order->items as $item) {
+            // 处理 order_item 建议退款金额
+            $item->suggest_refund_fee = $item->suggest_refund_fee;
+        }
+
+        // 处理未支付订单 item status_code
+        $order = $order->setOrderItemStatusByOrder($order);
+
+        $this->success('获取成功', null, $order);
+    }
+
+
+    /**
+     * 批量发货渲染模板
+     *
+     * @return void
+     */
+    public function batchDispatch()
+    {
+        return $this->view->fetch();
+    }
+
+
+    /**
+     * 批量发货渲染模板
+     *
+     * @return void
+     */
+    public function dispatchList()
+    {
+        return $this->view->fetch();
+    }
+
+
+
+    /**
+     * 手动发货
+     *
+     * @return void
+     */
+    public function customDispatch()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only(['order_id', 'order_item_ids', 'custom_type', 'custom_content']);
+        $this->svalidate($params, '.custom_dispatch');
+
+        Db::transaction(function () use ($params) {
+            $service = new OrderDispatchService($params);
+            $service->customDispatch($params);
+        });
+
+        $this->success('发货成功');
+    }
+
+
+    /**
+     * 发货
+     * 
+     * @description 
+     * 支持分包裹发货
+     * 支持手动发货
+     * 支持上传发货单发货
+     * 支持推送api运单发货 默认使用配置项
+     * 支持修改发货信息
+     * 支持取消发货
+     * 
+     * @remark 此处接口设计如此复杂是因为考虑到权限的问题,订单发货权限可以完成所有发货行为
+     * 
+     * @param  array    $action     发货行为(默认:confirm=确认发货, cancel=取消发货, change=修改运单, multiple=解析批量发货单)
+     * @param  int      $order_id   订单id
+     * @param  array    $order_item_ids   订单商品id
+     * @param  string   $method     发货方式(input=手动发货, api=推送运单, upload=上传发货单)
+     * @param  array    $sender     发货人信息
+     * @param  array    $express    物流信息
+     * 
+     * @return \think\Response
+     */
+    public function dispatch()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only(['action', 'order_id', 'order_ids', 'order_item_ids', 'method', 'sender', 'express', 'order_express_id']);
+
+        $action = $params['action'] ?? 'confirm';
+        if (!in_array($action, ['confirm', 'cancel', 'change', 'multiple'])) {
+            $this->error('发货参数错误');
+        }
+
+        $service = new OrderDispatchService($params);
+        switch ($action) {
+            case 'confirm':
+                $express = $service->confirm($params);
+                $this->success('发货成功', null, $express);
+                break;
+            case 'cancel':
+                $result = $service->cancel($params);
+                if ($result) {
+                    $this->success('取消发货成功');
+                }
+                break;
+            case 'change':
+                $express = $service->change($params);
+                $this->success('修改成功', null, $express);
+                break;
+            case 'multiple':
+                $params['file'] = $this->request->file('file');
+                $result = $service->multiple($params);
+                $this->success('待发货列表', null, $result);
+                break;
+        }
+
+        $this->error('操作失败');
+    }
+
+
+    /**
+     * 获取物流快递信息
+     */
+    public function updateExpress($order_express_id = 0)
+    {
+        $type = $this->request->param('type');
+
+        // 获取包裹
+        $orderExpress = OrderExpressModel::where('id', $order_express_id)->find();
+        if (!$orderExpress) {
+            $this->error('包裹不存在');
+        }
+
+        $expressLib = new ExpressLib();
+
+        try {
+            if ($type == 'subscribe') {
+                // 重新订阅
+                $expressLib->subscribe([
+                    'express_code' => $orderExpress['express_code'],
+                    'express_no' => $orderExpress['express_no']
+                ]);
+            } else {
+                // 手动查询
+                $result = $expressLib->search([
+                    'order_id' => $orderExpress['order_id'],
+                    'express_code' => $orderExpress['express_code'],
+                    'express_no' => $orderExpress['express_no']
+                ], $orderExpress);
+            }
+        } catch (HttpResponseException $e) {
+            $data = $e->getResponse()->getData();
+            $message = $data ? ($data['msg'] ?? '') : $e->getMessage();
+            $this->error($message);
+        } catch (\Exception $e) {
+            $this->error(($type == 'subscribe' ? '订阅失败:' : '刷新失败:') . $e->getMessage());
+        }
+
+        $this->success(($type == 'subscribe' ? '订阅成功' : '刷新成功'));
+    }
+
+
+    /**
+     * 线下付款,确认收货
+     */
+    public function offlineConfirm($id)
+    {
+        $admin = $this->auth->getUserInfo();
+
+        $order = OrderModel::offline()->where('id', $id)->find();
+        if (!$order) {
+            $this->error(__('No Results were found'));
+        }
+
+        $order = Db::transaction(function () use ($order, $admin) {
+            // 加锁读订单
+            $order = $order->lock(true)->find($order->id);
+            $user = User::get($order->user_id);
+
+            $payOper = new PayOper($user);
+            $order = $payOper->offline($order, $order->remain_pay_fee, 'order');        // 订单会变为已支付 paid
+
+            // 触发订单支付完成事件
+            $data = ['order' => $order, 'user' => $user];
+            \think\Hook::listen('order_offline_paid_after', $data);
+
+            $orderOper = new OrderOper();
+            // 确认收货
+            $order = $orderOper->confirm($order, [], $admin, 'admin');
+
+            return $order;
+        });
+
+        $this->success('收货成功', $order);
+    }
+
+
+
+    /**
+     * 线下付款,拒收
+     */
+    public function offlineRefuse($id)
+    {
+        $admin = $this->auth->getUserInfo();
+
+        $order = OrderModel::offline()->where('id', $id)->find();
+        if (!$order) {
+            $this->error(__('No Results were found'));
+        }
+
+        $order = Db::transaction(function () use ($order, $admin) {
+            // 加锁读订单
+            $order = $order->lock(true)->find($order->id);
+
+            // 拒收
+            $orderOper = new OrderOper();
+            $order = $orderOper->refuse($order, $admin, 'admin');
+
+            // 交易关闭
+            $order = $orderOper->close($order, $admin, 'admin', '用户拒绝收货,管理员关闭订单', ['closed_type' => 'refuse']);
+            return $order;
+        });
+
+        $this->success('收货成功', $order);
+    }
+
+
+    /**
+     * 订单改价,当剩余应支付金额为 0 时,订单将自动支付
+     *
+     * @param int $id
+     * @return void
+     */
+    public function changeFee($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $admin = $this->auth->getUserInfo();
+
+        $params = $this->request->param();
+        $this->svalidate($params, '.change_fee');
+
+        $order = $this->model->unpaid()->where('id', $id)->find();
+        if (!$order) {
+            $this->error('订单不可改价');
+        }
+
+        Db::transaction(function () use ($order, $admin, $params) {
+            $pay_fee = $params['pay_fee'];
+            $change_msg = $params['change_msg'];
+
+            $payOper = new PayOper($order->user_id);
+            $payed_fee = $payOper->getPayedFee($order, 'order');
+
+            if ($pay_fee < $payed_fee) {
+                $this->error('改价金额不能低于订单已支付金额');
+            }
+
+            // 记录原始值
+            $last_pay_fee = $order->pay_fee;     // 上次pay_fee,非原始 pay_fee
+            $order->pay_fee = $pay_fee;
+            $order->save();
+
+            OrderActionModel::add($order, null, $admin, 'admin', "应支付金额由 ¥" . $last_pay_fee . " 改为 ¥" . $pay_fee . ",改价原因:" . $change_msg);
+
+            // 检查订单支付状态, 改价可以直接将订单变为已支付
+            $order = $payOper->checkAndPaid($order, 'order');
+        });
+
+        $this->success('改价成功');
+    }
+
+
+
+    /**
+     * 修改收货人信息
+     *
+     * @param Request $request
+     * @param integer $id
+     * @return void
+     */
+    public function editConsignee($id)
+    {
+        $admin = $this->auth->getUserInfo();
+
+        $params = $this->request->param();
+        $this->svalidate($params, '.edit_consignee');
+
+        $order = $this->model->withTrashed()->where('id', $id)->find();
+        if (!$order) {
+            $this->error(__('No Results were found'));
+        }
+
+        Db::transaction(function () use ($admin, $order, $params) {
+            $orderAddress = OrderAddressModel::where('order_id', $order->id)->find();
+            if (!$orderAddress) {
+                $this->error(__('No Results were found'));
+            }
+            $orderAddress->consignee = $params['consignee'];
+            $orderAddress->mobile = $params['mobile'];
+            $orderAddress->province_name = $params['province_name'];
+            $orderAddress->city_name = $params['city_name'];
+            $orderAddress->district_name = $params['district_name'];
+            $orderAddress->address = $params['address'];
+            $orderAddress->province_id = $params['province_id'];
+            $orderAddress->city_id = $params['city_id'];
+            $orderAddress->district_id = $params['district_id'];
+            $orderAddress->save();
+
+            OrderActionModel::add($order, null, $admin, 'admin', "修改订单收货人信息");
+        });
+
+        $this->success('收货人信息修改成功');
+    }
+
+
+
+    /**
+     * 编辑商家备注
+     *
+     * @param Request $request
+     * @param integer $id
+     * @return void
+     */
+    public function editMemo($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $admin = $this->auth->getUserInfo();
+
+        $params = $this->request->param();
+        $this->svalidate($params, '.edit_memo');
+
+        $order = $this->model->withTrashed()->where('id', $id)->find();
+        if (!$order) {
+            $this->error(__('No Results were found'));
+        }
+        Db::transaction(function () use ($admin, $order, $params) {
+            $order->memo = $params['memo'];
+            $order->save();
+
+            OrderActionModel::add($order, null, $admin, 'admin', "修改卖家备注:" . $params['memo']);
+        });
+
+        $this->success('卖家备注修改成功');
+    }
+
+
+    /**
+     * 拒绝用户全额退款申请
+     *
+     * @param Request $request
+     * @param integer $id
+     * @return void
+     */
+    public function applyRefundRefuse($id)
+    {
+        $admin = $this->auth->getUserInfo();
+
+        $params = $this->request->param();
+        // $this->svalidate($params, '.apply_refund_refuse');
+
+        $order = $this->model->withTrashed()->paid()->applyRefundIng()->where('id', $id)->find();
+        if (!$order) {
+            $this->error('订单未找到或不可拒绝申请');
+        }
+        Db::transaction(function () use ($admin, $order, $params) {
+            $order->apply_refund_status = OrderModel::APPLY_REFUND_STATUS_REFUSE;
+            $order->save();
+
+            OrderActionModel::add($order, null, $admin, 'admin', "拒绝用户申请全额退款");
+        });
+
+        $this->success('拒绝申请成功');
+    }
+
+
+    /**
+     * 全额退款 (必须没有进行过任何退款才能使用)
+     *
+     * @param Request $request
+     * @param integer $id
+     * @return void
+     */
+    public function fullRefund($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $admin = $this->auth->getUserInfo();
+        $admin = Admin::where('id', $admin['id'])->find();
+
+        $params = $this->request->param();
+        $refund_type = $params['refund_type'] ?? 'back';
+
+        Db::transaction(function () use ($admin, $id, $refund_type) {
+            $order = $this->model->paid()->where('id', $id)->lock(true)->find();
+            if (!$order) {
+                $this->error('订单不存在或不可退款');
+            }
+
+            $orderRefund = new OrderRefund($order);
+            $orderRefund->fullRefund($admin, [
+                'refund_type' => $refund_type,
+                'remark' => '平台主动全额退款'
+            ]);
+        });
+
+        $this->success('全额退款成功');
+    }
+
+
+
+    /**
+     * 订单单商品退款
+     *
+     * @param Request $request
+     * @param integer $id
+     * @param integer $item_id
+     * @return void
+     */
+    public function refund($id, $item_id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $admin = $this->auth->getUserInfo();
+
+        $params = $this->request->param();
+        $this->svalidate($params, '.refund');
+
+        $refund_money = round(floatval($params['refund_money']), 2);
+        $refund_type = $params['refund_type'] ?? 'back';
+        if ($refund_money <= 0) {
+            $this->error('请输入正确的退款金额');
+        }
+
+        $order = $this->model->paid()->where('id', $id)->find();
+        if (!$order) {
+            $this->error('订单不存在或不可退款');
+        }
+
+        $item = OrderItem::where('order_id', $order->id)->where('id', $item_id)->find();
+        if (!$item) {
+            $this->error(__('No Results were found'));
+        }
+
+        if (in_array($item['refund_status'], [
+            OrderItem::REFUND_STATUS_AGREE,
+            OrderItem::REFUND_STATUS_COMPLETED,
+        ])) {
+            $this->error('订单商品已退款,不能重复退款');
+        }
+
+        $payOper = new PayOper($order->user_id);
+        // 获取订单最大可退款金额(不含积分抵扣金额)
+        $remain_max_refund_money = $payOper->getRemainRefundMoney($order->id);
+
+        // 如果退款金额大于订单支付总金额
+        if (bccomp((string)$refund_money, $remain_max_refund_money, 2) === 1) {
+            $this->error('退款总金额不能大于实际支付金额');
+        }
+
+        Db::transaction(function () use ($admin, $order, $item, $refund_money, $refund_type) {
+            // 重新锁定读查询 orderItem
+            $item = OrderItem::where('order_id', $order->id)->lock(true)->where('id', $item->id)->find();
+            if (!$item) {
+                $this->error('订单不存在或不可退款');
+            }
+
+            $orderRefund = new OrderRefund($order);
+            $orderRefund->refund($item, $refund_money, $admin, [
+                'refund_type' => $refund_type,
+                'remark' => '平台主动退款'
+            ]);
+        });
+
+        $this->success('退款成功');
+    }
+
+
+    /**
+     * 获取订单操作记录
+     *
+     * @param integer $id
+     * @return void
+     */
+    public function action($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $actions = OrderActionModel::where('order_id', $id)->order('id', 'desc')->select();
+
+        $morphs = [
+            'user' => \app\admin\model\shopro\user\User::class,
+            'admin' => \app\admin\model\Admin::class,
+            'system' => \app\admin\model\Admin::class,
+        ];
+        $actions = morph_to($actions, $morphs, ['oper_type', 'oper_id']);
+
+        foreach ($actions as &$action) {
+            $action['oper'] = Operator::info($action['oper_type'], $action['oper'] ?? null);
+        }
+
+        $this->success('获取成功', null, $actions);
+    }
+
+
+
+
+    public function export()
+    {
+        $cellTitles = [
+            // 订单表字段
+            'order_id' => 'Id',
+            'order_sn' => '订单号',
+            'type_text' => '订单类型',
+            'user_nickname' => '下单用户',
+            'user_mobile' => '手机号',
+            'status_text' => '订单状态',
+            'pay_text' => '支付状态',
+            'pay_types_text' => '支付类型',
+            'remark' => '用户备注',
+            'memo' => '卖家备注',
+            'order_amount' => '订单总金额',
+            'score_amount' => '积分支付数量',
+            'dispatch_amount' => '运费',
+            'pay_fee' => '应付总金额',
+            'real_pay_fee' => '实付总金额',
+            'remain_pay_fee' => '剩余支付金额',
+            'total_discount_fee' => '总优惠金额',
+            'coupon_discount_fee' => '优惠券金额',
+            'promo_discount_fee' => '营销优惠金额',
+            'paid_time' => '支付完成时间',
+            'platform_text' => '交易平台',
+            'consignee_info' => '收货信息',
+            'createtime' => '下单时间',
+
+            // 订单商品表字段
+            'activity_type_text' => '活动',
+            'promo_types' => '促销',
+            'goods_title' => '商品名称',
+            'goods_sku_text' => '商品规格',
+            'goods_num' => '购买数量',
+            'goods_original_price' => '商品原价',
+            'goods_price' => '商品价格',
+            'goods_weight' => '商品重量',
+            'discount_fee' => '优惠金额',
+            'goods_pay_fee' => '商品支付金额',
+            'dispatch_type_text' => '发货方式',
+            'dispatch_status_text' => '发货状态',
+            'aftersale_refund' => '售后/退款',
+            'comment_status_text' => '评价状态',
+            'refund_fee' => '退款金额',
+            'refund_msg' => '退款原因',
+            'express_name' => '快递公司',
+            'express_no' => '快递单号',
+        ];
+
+        // 数据总条数
+        $total = $this->model->withTrashed()->sheepFilter()->count();
+        if ($total <= 0) {
+            $this->error('导出数据为空');
+        }
+
+        $export = new \addons\shopro\library\Export();
+        $params = [
+            'file_name' => '订单列表',
+            'cell_titles' => $cellTitles,
+            'total' => $total,
+            'is_sub_cell' => true,
+            'sub_start_cell' => 'activity_type_text',
+            'sub_field' => 'items'
+        ];
+
+        $total_order_amount = 0;
+        $total_pay_fee = 0;
+        $total_real_pay_fee = 0;
+        $total_discount_fee = 0;
+        $total_score_amount = 0;
+        $result = $export->export($params, function ($pages) use (&$total_order_amount, &$total_pay_fee, &$total_real_pay_fee, &$total_discount_fee, &$total_score_amount, $total) {
+            $datas = $this->model->withTrashed()->sheepFilter()->with(['user', 'items' => function ($query) {
+                $query->with(['express']);
+            }, 'address'])
+                ->limit((($pages['page'] - 1) * $pages['list_rows']), $pages['list_rows'])
+                ->select();
+
+            $datas = collection($datas);
+            $datas = $datas->each(function ($order) {
+                $order->pay_types_text = $order->pay_types_text;
+            })->toArray();
+
+            $newDatas = [];
+            foreach ($datas as &$order) {
+                $order = $this->model->setOrderItemStatusByOrder($order);
+
+                // 收货人信息
+                $consignee_info = '';
+                if ($order['address']) {
+                    $address = $order['address'];
+                    $consignee_info = ($address['consignee'] ? ($address['consignee'] . ':' . $address['mobile'] . '-') : '') . ($address['province_name'] . '-' . $address['city_name'] . '-' . $address['district_name']) . ' ' . $address['address'];
+                }
+
+                $data = [
+                    'order_id' => $order['id'],
+                    'order_sn' => $order['order_sn'],
+                    'type_text' => $order['type_text'],
+                    'user_nickname' => $order['user'] ? $order['user']['nickname'] : '-',
+                    'user_mobile' => $order['user'] ? $order['user']['mobile'] . ' ' : '-',
+                    'status_text' => $order['status_text'],
+                    'pay_text' => in_array($order['status'], [OrderModel::STATUS_PAID, OrderModel::STATUS_COMPLETED]) ? '已支付' : '未支付',
+                    'pay_types_text' => is_array($order['pay_types_text']) ? join(',', $order['pay_types_text']) : ($order['pay_types_text'] ?: ''),
+                    'remark' => $order['remark'],
+                    'memo' => $order['memo'],
+                    'order_amount' => $order['order_amount'],
+                    'score_amount' => $order['score_amount'],
+                    'dispatch_amount' => $order['dispatch_amount'],
+                    'pay_fee' => $order['pay_fee'],
+                    'real_pay_fee' => bcsub($order['pay_fee'], $order['remain_pay_fee'], 2),
+                    'remain_pay_fee' => $order['remain_pay_fee'],
+                    'total_discount_fee' => $order['total_discount_fee'],
+                    'coupon_discount_fee' => $order['coupon_discount_fee'],
+                    'promo_discount_fee' => $order['promo_discount_fee'],
+                    'paid_time' => $order['paid_time'],
+                    'platform_text' => $order['platform_text'],
+                    'consignee_info' => $consignee_info,
+                    'createtime' => $order['createtime'],
+                ];
+
+                $items = [];
+                foreach ($order['items'] as $item) {
+                    $items[] = [
+                        'activity_type_text' => $item['activity_type_text'],
+                        'promo_types_text' => is_array($item['promo_types_text']) ? join(',', $item['promo_types_text']) : ($item['promo_types_text'] ?: '-'),
+                        'goods_title' => $item['goods_title'],
+                        'goods_sku_text' => $item['goods_sku_text'],
+                        'goods_num' => $item['goods_num'],
+                        'goods_original_price' => $item['goods_original_price'],
+                        'goods_price' => $item['goods_price'],
+                        'goods_weight' => $item['goods_weight'],
+                        'discount_fee' => $item['discount_fee'],
+                        'goods_pay_fee' => $item['pay_fee'],
+                        'dispatch_type_text' => $item['dispatch_type_text'],
+                        'dispatch_status_text' => $item['dispatch_status_text'],
+                        'aftersale_refund' => $item['aftersale_status_text'] . '/' . $item['refund_status_text'],
+                        'comment_status_text' => $item['comment_status_text'],
+                        'refund_fee' => $item['refund_fee'],
+                        'refund_msg' => $item['refund_msg'],
+                        'express_name' => $item['express'] ? $item['express']['express_name'] : '-',
+                        'express_no' => $item['express'] ? $item['express']['express_no'] . ' ' : '-',
+                    ];
+                }
+
+                $data['items'] = $items;
+
+                $newDatas[] = $data;
+            }
+
+            $total_order_amount += array_sum(array_column($newDatas, 'order_amount'));
+            $total_score_amount += array_sum(array_column($newDatas, 'score_amount'));
+            $total_pay_fee += array_sum(array_column($newDatas, 'pay_fee'));
+            $total_real_pay_fee += array_sum(array_column($newDatas, 'real_pay_fee'));
+            $total_discount_fee += array_sum(array_column($newDatas, 'discount_fee'));
+
+            if ($pages['is_last_page']) {
+                $newDatas[] = ['order_id' => "订单总数:" . $total . ";订单总金额:¥" . $total_order_amount .  ";优惠总金额:¥" . $total_discount_fee . ";应付总金额:¥" . $total_pay_fee . ";实付总金额:¥" . $total_real_pay_fee . ";支付总积分:" . $total_score_amount];
+            }
+            return $newDatas;
+        });
+
+        $this->success('导出成功' . (isset($result['file_path']) && $result['file_path'] ? ',请在服务器: “' . $result['file_path'] . '” 查看' : ''), null, $result);
+    }
+
+
+    public function exportDelivery()
+    {
+        $cellTitles = [
+            // 订单表字段
+            'order_id' => 'Id',
+            'order_sn' => '订单号',
+            'type_text' => '订单类型',
+            'consignee_info' => '收货信息',
+            'remark' => '用户备注',
+            'memo' => '商家备注',
+            'createtime' => '下单时间',
+
+            // 订单商品表字段
+            'order_item_id' => '子订单Id',
+            'goods_title' => '商品名称',
+            'goods_sku_text' => '商品规格',
+            'goods_num' => '购买数量',
+            // 'dispatch_fee' => '发货费用',
+            'dispatch_type_text' => '发货方式',
+            'dispatch_status_text' => '发货状态',
+            'aftersale_refund' => '售后/退款',
+            'express_no' => '快递单号',
+        ];
+
+        // 数据总条数
+        $total = $this->model->sheepFilter()->count();      // nosend 加了 noApplyRefund
+        if ($total <= 0) {
+            $this->error('导出数据为空');
+        }
+
+        $export = new \addons\shopro\library\Export();
+        $params = [
+            'file_name' => '订单发货单列表',
+            'cell_titles' => $cellTitles,
+            'total' => $total,
+            'is_sub_cell' => true,
+            'sub_start_cell' => 'order_item_id',
+            'sub_field' => 'items'
+        ];
+
+        $result = $export->export($params, function ($pages) use (&$total) {
+            // 未申请全额退款的
+            $datas = $this->model->sheepFilter()->with(['user', 'items' => function ($query) {      // nosend 加了 noApplyRefund
+                $query->with(['express']);
+            }, 'address'])
+                ->limit((($pages['page'] - 1) * $pages['list_rows']), $pages['list_rows'])
+                ->select();
+            $datas = collection($datas)->toArray();
+
+            $newDatas = [];
+            foreach ($datas as &$order) {
+                $order = $this->model->setOrderItemStatusByOrder($order);
+
+                if (in_array($order['status_code'], ['groupon_ing', 'groupon_invalid'])) {
+                    // 拼团正在进行中,不发货
+                    $total--;       // total 减少 1
+                    continue;
+                }
+
+                // 收货人信息
+                $consignee_info = '';
+                if ($order['address']) {
+                    $address = $order['address'];
+                    $consignee_info = ($address['consignee'] ? ($address['consignee'] . ':' . $address['mobile'] . '-') : '') . ($address['province_name'] . '-' . $address['city_name'] . '-' . $address['district_name']) . ' ' . $address['address'];
+                }
+
+                $data = [
+                    'order_id' => $order['id'],
+                    'order_sn' => $order['order_sn'],
+                    'type_text' => $order['type_text'],
+                    'consignee_info' => $consignee_info,
+                    'remark' => $order['remark'],
+                    'memo' => $order['memo'],
+                    'createtime' => $order['createtime']
+                ];
+
+                $items = [];
+                foreach ($order['items'] as $k => $item) {
+                    // 未发货,并且未退款,并且未在申请售后中,并且是快递物流的
+                    if (
+                        $item['dispatch_status'] == OrderItem::DISPATCH_STATUS_NOSEND
+                        && !in_array($item['refund_status'], [OrderItem::REFUND_STATUS_AGREE, OrderItem::REFUND_STATUS_COMPLETED])
+                        && $item['aftersale_status'] != OrderItem::AFTERSALE_STATUS_ING
+                        && $item['dispatch_type'] == 'express'
+                    ) {
+                        $items[] = [
+                            'order_item_id' => $item['id'],
+                            'goods_title' => strpos($item['goods_title'], '=') === 0 ? ' ' . $item['goods_title'] : $item['goods_title'],
+                            'goods_sku_text' => $item['goods_sku_text'],
+                            'goods_num' => $item['goods_num'],
+                            // 'dispatch_fee' => $item['dispatch_fee'],
+                            'dispatch_type_text' => $item['dispatch_type_text'],
+                            'dispatch_status_text' => $item['dispatch_status_text'],
+                            'aftersale_refund' => $item['aftersale_status_text'] . '/' . $item['refund_status_text'],
+                            'express_no' => $item['express'] ? $item['express']['express_no'] . ' ' : '',
+                        ];
+                    }
+                }
+
+                $data['items'] = $items;
+                $newDatas[] = $data;
+            };
+
+            if ($pages['is_last_page']) {
+                $newDatas[] = ['order_id' => "订单总数(仅快递物流的待发货订单):" . $total . ";备注:订单中同一包裹请填写相同运单号"];
+            }
+            return $newDatas;
+        });
+
+        $this->success('导出成功' . (isset($result['file_path']) && $result['file_path'] ? ',请在服务器: “' . $result['file_path'] . '” 查看' : ''), null, $result);
+    }
+}

+ 199 - 0
addons/shopro/application/admin/controller/shopro/trade/Order.php

@@ -0,0 +1,199 @@
+<?php
+
+namespace app\admin\controller\shopro\trade;
+
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\trade\Order as TradeOrderModel;
+use app\admin\model\shopro\Pay as PayModel;
+
+class Order extends Common
+{
+
+    protected $noNeedRight = ['getType'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new TradeOrderModel;
+    }
+
+    /**
+     * 订单列表
+     *
+     * @return \think\Response
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            $exportConfig = (new \addons\shopro\library\Export())->getConfig();
+            $this->assignconfig("save_type", $exportConfig['save_type'] ?? 'download');
+            return $this->view->fetch();
+        }
+
+        $orders = $this->model->withTrashed()->sheepFilter()->with(['user'])
+            ->paginate(request()->param('list_rows', 10))->each(function ($order) {
+                $order->pay_type_text = $order->pay_type_text;
+                $order->pay_type = $order->pay_type;
+            })->toArray();
+
+        $result = [
+            'orders' => $orders,
+        ];
+
+        // 查询各个状态下的订单数量
+        $searchStatus = $this->model->searchStatusList();
+        // 所有的数量
+        $result['all'] = $this->model->withTrashed()->sheepFilter(true, function ($filters) {
+            unset($filters['status']);
+            return $filters;
+        })->count();
+        foreach ($searchStatus as $status => $text) {
+            $result[$status] = $this->model->withTrashed()->sheepFilter(true, function ($filters) use ($status) {
+                $filters['status'] = $status;
+                return $filters;
+            })->count();
+        }
+
+        $this->success('获取成功', null, $result);
+    }
+
+
+    // 获取数据类型
+    public function getType()
+    {
+        $type = $this->model->typeList();
+        $payType = (new PayModel)->payTypeList();
+        $platform = $this->model->platformList();
+        $status = $this->model->searchStatusList();
+
+        $result = [
+            'type' => $type,
+            'pay_type' => $payType,
+            'platform' => $platform,
+            'status' => $status
+        ];
+
+        $data = [];
+        foreach ($result as $key => $list) {
+            $data[$key][] = ['name' => '全部', 'type' => 'all'];
+
+            foreach ($list as $k => $v) {
+                $data[$key][] = [
+                    'name' => $v,
+                    'type' => $k
+                ];
+            }
+        }
+
+        $this->success('获取成功', null, $data);
+    }
+
+
+
+    /**
+     * 订单详情
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function detail($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $order = $this->model->withTrashed()->with(['user', 'pays'])->where('id', $id)->find();
+        if (!$order) {
+            $this->error(__('No Results were found'));
+        }
+
+        $order->pay_type = $order->pay_type;
+        $order->pay_type_text = $order->pay_type_text;
+
+        $this->success('获取成功', null, $order);
+    }
+
+
+
+    public function export()
+    {
+        $cellTitles = [
+            // 订单表字段
+            'order_id' => 'Id',
+            'order_sn' => '订单号',
+            'type_text' => '订单类型',
+            'user_nickname' => '下单用户',
+            'user_mobile' => '手机号',
+            'status_text' => '订单状态',
+            'pay_type_text' => '支付类型',
+            'remark' => '用户备注',
+            'order_amount' => '订单总金额',
+            'pay_fee' => '应付总金额',
+            'real_pay_fee' => '实付总金额',
+            'remain_pay_fee' => '剩余支付金额',
+            'paid_time' => '支付完成时间',
+            'platform_text' => '交易平台',
+            'createtime' => '下单时间',
+        ];
+
+        // 数据总条数
+        $total = $this->model->withTrashed()->sheepFilter()->count();
+        if ($total <= 0) {
+            $this->error('导出数据为空');
+        }
+
+        $export = new \addons\shopro\library\Export();
+        $params = [
+            'file_name' => '交易订单列表',
+            'cell_titles' => $cellTitles,
+            'total' => $total,
+        ];
+
+        $total_order_amount = 0;
+        $total_pay_fee = 0;
+        $total_real_pay_fee = 0;
+        $result = $export->export($params, function ($pages) use (&$total_order_amount, &$total_pay_fee, &$total_real_pay_fee, $total) {
+            $datas = $this->model->withTrashed()->sheepFilter()->with(['user'])
+                ->limit((($pages['page'] - 1) * $pages['list_rows']), $pages['list_rows'])
+                ->select();
+
+            $datas = collection($datas);
+            $datas = $datas->each(function ($order) {
+                $order->pay_type_text = $order->pay_type_text;
+            })->toArray();
+
+            $newDatas = [];
+            foreach ($datas as &$order) {
+                $data = [
+                    'order_id' => $order['id'],
+                    'order_sn' => $order['order_sn'],
+                    'type_text' => $order['type_text'],
+                    'user_nickname' => $order['user'] ? $order['user']['nickname'] : '-',
+                    'user_mobile' => $order['user'] ? $order['user']['mobile'] . ' ' : '-',
+                    'status_text' => $order['status_text'],
+                    'pay_type_text' => is_array($order['pay_type_text']) ? join(',', $order['pay_type_text']) : ($order['pay_type_text'] ?: ''),
+                    'remark' => $order['remark'],
+                    'order_amount' => $order['order_amount'],
+                    'pay_fee' => $order['pay_fee'],
+                    'real_pay_fee' => bcsub($order['pay_fee'], $order['remain_pay_fee'], 2),
+                    'remain_pay_fee' => $order['remain_pay_fee'],
+                    'paid_time' => $order['paid_time'],
+                    'platform_text' => $order['platform_text'],
+                    'createtime' => $order['createtime'],
+                ];
+                $newDatas[] = $data;
+            }
+
+            $total_order_amount += array_sum(array_column($newDatas, 'order_amount'));
+            $total_pay_fee += array_sum(array_column($newDatas, 'pay_fee'));
+            $total_real_pay_fee += array_sum(array_column($newDatas, 'real_pay_fee'));
+
+            if ($pages['is_last_page']) {
+                $newDatas[] = ['order_id' => "订单总数:" . $total . ";订单总金额:¥" . $total_order_amount . ";应付总金额:¥" . $total_pay_fee . ";实付总金额:¥" . $total_real_pay_fee];
+            }
+            return $newDatas;
+        });
+
+        $this->success('导出成功' . (isset($result['file_path']) && $result['file_path'] ? ',请在服务器: “' . $result['file_path'] . '” 查看' : ''), null, $result);
+    }
+}

+ 303 - 0
addons/shopro/application/admin/controller/shopro/traits/SkuPrice.php

@@ -0,0 +1,303 @@
+<?php
+
+namespace app\admin\controller\shopro\traits;
+
+use app\admin\model\shopro\goods\Goods as GoodsModel;
+use app\admin\model\shopro\goods\Sku as SkuModel;
+use app\admin\model\shopro\goods\SkuPrice as SkuPriceModel;
+use addons\shopro\traits\StockWarning as StockWarningTrait;
+
+trait SkuPrice
+{
+    use StockWarningTrait;
+
+    /**
+     * 编辑规格
+     *
+     * @param GoodsModel $goods
+     * @param array $sku
+     * @param string $type
+     * @return void
+     */
+    private function editSku($goods, $type = 'add')
+    {
+        if ($goods['is_sku']) {
+            // 多规格
+            $this->editMultSku($goods, $type);
+        } else {
+            $this->editSimSku($goods, $type);
+        }
+    }
+
+
+    /**
+     * 添加编辑单规格
+     *
+     * @param GoodsModel $goods
+     * @param string $type
+     * @return void
+     */
+    protected function editSimSku($goods, $type = 'add')
+    {
+        $params = $this->request->only([
+            'stock', 'stock_warning', 'sn', 'weight', 'cost_price', 'original_price', 'price'
+        ]);
+
+        $data = [
+            "goods_sku_ids" => null,
+            "goods_sku_text" => null,
+            "image" => null,
+            "goods_id" => $goods->id,
+            "stock" => $params['stock'] ?? 0,
+            "stock_warning" => isset($params['stock_warning']) && is_numeric($params['stock_warning'])
+                ? $params['stock_warning'] : null,
+            "sn" => $params['sn'] ?? "",
+            "weight" => isset($params['weight']) ? floatval($params['weight']) : 0,
+            "cost_price" => $params['cost_price'] ?? 0,
+            "original_price" => $params['original_price'] ?? 0,
+            "price" => $params['price'] ?? 0,
+            "status" => 'up'
+        ];
+
+        if ($type == 'edit') {
+            // 查询
+            $skuPrice = SkuPriceModel::where('goods_id', $goods->id)->order('id', 'asc')->find();
+            if ($skuPrice) {
+                // 删除多余的这个商品的其他规格以及规格项(防止多规格改为了单规格,遗留一批多余的 sku_price)
+                SkuPriceModel::where('goods_id', $goods->id)->where('id', '<>', $skuPrice->id)->delete();
+                SkuModel::where('goods_id', $goods->id)->delete();
+            }
+
+            unset($data['stock']);      // 移除库存(库存只能通过补货增加)
+        }
+
+        if (!isset($skuPrice) || !$skuPrice) {
+            $skuPrice = new SkuPriceModel();
+        }
+
+        $skuPrice->save($data);
+        if ($type == 'add') {
+            // 增加补货记录
+            $this->addStockLog($skuPrice, 0, $data['stock'], $type);
+
+            // 检测库存预警
+            $this->checkStockWarning($skuPrice, $type);
+        }
+
+    }
+
+
+    /**
+     * 添加编辑多规格
+     * 
+     * @param GoodsModel $goods
+     * @param string $type
+     * @return void
+     */
+    protected function editMultSku($goods, $type = 'add')
+    {
+        $params = $this->request->only([
+            'skus', 'sku_prices'
+        ]);
+        $skus = $params['skus'] ?? [];
+        $skuPrices = $params['sku_prices'] ?? [];
+
+        $this->checkMultSku($skus, $skuPrices);
+
+        // 编辑保存规格项
+        $allChildrenSku = $this->saveSkus($goods, $skus, $type);
+
+        if ($type == 'edit') {
+            // 编辑旧商品,先删除老的不用的 skuPrice
+            $oldSkuPriceIds = array_column($skuPrices, 'id');
+            // 删除当前商品老的除了在基础上修改的skuPrice
+            SkuPriceModel::where('goods_id', $goods->id)
+                ->whereNotIn('id', $oldSkuPriceIds)->delete();
+
+            // 删除失效的库存预警记录
+            $this->delNotStockWarning($oldSkuPriceIds, $goods->id);
+        }
+
+        $min_key = null;    // 最小加个对应的键值
+        $min_price = min(array_column($skuPrices, 'price'));        // 规格最小价格
+        $originPrices = array_filter(array_column($skuPrices, 'original_price'));
+        $min_original_price = $originPrices ? min($originPrices) : 0;        // 规格最小原始价格
+        foreach ($skuPrices as $key => &$skuPrice) {
+            $skuPrice['goods_sku_ids'] = $this->getRealSkuIds($skuPrice['goods_sku_temp_ids'], $allChildrenSku);
+            $skuPrice['goods_id'] = $goods->id;
+            $skuPrice['goods_sku_text'] = is_array($skuPrice['goods_sku_text']) ? join(',', $skuPrice['goods_sku_text']) : $skuPrice['goods_sku_text'];
+            $skuPrice['stock_warning'] = isset($skuPrice['stock_warning']) && is_numeric($skuPrice['stock_warning'])
+                ? $skuPrice['stock_warning'] : null;        // null 为关闭商品库存预警, 采用默认库存预警
+
+            // 移除无用 属性
+            if ($type == 'add') {
+                // 添加直接移除 id
+                unset($skuPrice['id']);
+            }
+            unset($skuPrice['temp_id']);                  // 前端临时 id
+            unset($skuPrice['goods_sku_temp_ids']);       // 前端临时规格 id,查找真实 id 用
+            unset($skuPrice['createtime'], $skuPrice['updatetime']);      // 删除时间
+
+            $skuPriceModel = new SkuPriceModel();
+            if (isset($skuPrice['id']) && $skuPrice['id']) {
+                // type == 'edit' 
+                unset($skuPrice['stock']);      // 编辑商品 不能编辑库存,只能通过补货
+                $skuPriceModel = $skuPriceModel->find($skuPrice['id']);
+            }
+
+            if ($skuPriceModel) {
+                $skuPriceModel->allowField(true)->save($skuPrice);
+
+                if ($type == 'add') {
+                    // 增加补货记录
+                    $this->addStockLog($skuPriceModel, 0, $skuPrice['stock'], 'add');      // 记录库存记录
+
+                    // 检测库存预警
+                    $this->checkStockWarning($skuPriceModel, $type);
+                }
+            }
+
+            if (is_null($min_key) && $min_price == $skuPrice['price']) {
+                $min_key = $key;
+            }
+        }
+
+        // 重新赋值最小价格和原价
+        $goods->original_price = $skuPrices[$min_key]['original_price'] ?? $min_original_price;  // 最小价格规格对应的原价
+        $goods->price = $min_price;
+        $goods->save();
+    }
+
+
+    /**
+     * 校验多规格是否填写完整
+     * 
+     * @param array $skus
+     * @param array $skuPrices
+     * @return void
+     */
+    private function checkMultSku($skus, $skuPrices) 
+    {
+        if (count($skus) < 1) {
+            error_stop('请填写规格列表');
+        }
+        foreach ($skus as $key => $sku) {
+            if (count($sku['children']) <= 0) {
+                error_stop('主规格至少要有一个子规格');
+            }
+
+            // 验证子规格不能为空
+            foreach ($sku['children'] as $k => $child) {
+                if (!isset($child['name']) || empty(trim($child['name']))) {
+                    error_stop('子规格不能为空');
+                }
+            }
+        }
+
+        if (count($skuPrices) < 1) {
+            error_stop('请填写规格价格');
+        }
+
+        foreach ($skuPrices as &$price) {
+            // 校验多规格属性
+            $this->svalidate($price, '.sku_params');
+        }
+    }
+
+
+    /**
+     * 根据前端临时 temp_id 获取真实的数据库 id
+     *
+     * @param array $newGoodsSkuIds
+     * @param array $allChildrenSku
+     * @return string
+     */
+    private function getRealSkuIds($newGoodsSkuIds, $allChildrenSku)
+    {
+        $newIdsArray = [];
+        foreach ($newGoodsSkuIds as $id) {
+            $newIdsArray[] = $allChildrenSku[$id];
+        }
+        return join(',', $newIdsArray);
+    }
+
+
+    /**
+     * 差异更新 规格规格项(多的删除,少的添加)
+     *
+     * @param GoodsModel $goods
+     * @param array $skus
+     * @param string $type
+     * @return array
+     */
+    private function saveSkus($goods, $skus, $type = 'add')
+    {
+        $allChildrenSku = [];
+
+        if ($type == 'edit') {
+            // 删除无用老规格
+            // 拿出需要更新的老规格
+            $oldSkuIds = [];
+            foreach ($skus as $key => $sku) {
+                $oldSkuIds[] = $sku['id'];
+
+                $childSkuIds = [];
+                if ($sku['children']) {
+                    // 子项 id
+                    $childSkuIds = array_column($sku['children'], 'id');
+                }
+
+                $oldSkuIds = array_merge($oldSkuIds, $childSkuIds);
+                $oldSkuIds = array_unique($oldSkuIds);
+            }
+
+            // 删除老的除了在基础上修改的规格项
+            SkuModel::where('goods_id', $goods->id)->whereNotIn('id', $oldSkuIds)->delete();
+        }
+
+        foreach ($skus as $s1 => &$k1) {
+            //添加主规格
+            $current_id = $k1['id'] ?? 0;
+            if ($k1['id']) {
+                // 编辑
+                SkuModel::where('id', $k1['id'])->update([
+                    'name' => $k1['name'],
+                ]);
+            } else {
+                // 新增
+                $k1Model = new SkuModel();
+                $k1Model->save([
+                    'name' => $k1['name'],
+                    'parent_id' => 0,
+                    'goods_id' => $goods->id
+                ]);
+                $k1['id'] = $current_id = $k1Model->id;
+            }
+
+            foreach ($k1['children'] as $s2 => &$k2) {
+                $current_child_id = $k2['id'] ?? 0;
+                if ($k2['id']) {
+                    // 编辑
+                    SkuModel::where('id', $k2['id'])->update([
+                        'name' => $k2['name'],
+                    ]);
+                } else {
+                    // 新增
+                    $k2Model = new SkuModel();
+                    $k2Model->save([
+                        'name' => $k2['name'],
+                        'parent_id' => $current_id,
+                        'goods_id' => $goods->id
+                    ]);
+                    $current_child_id = $k2Model->id;
+                }
+
+                $allChildrenSku[$k2['temp_id']] = $current_child_id;
+                $k2['id'] = $current_child_id;
+                $k2['parent_id'] = $current_id;
+            }
+        }
+
+        return $allChildrenSku;
+    }
+}

+ 33 - 0
addons/shopro/application/admin/controller/shopro/user/Coupon.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace app\admin\controller\shopro\user;
+
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\order\Order;
+use app\admin\model\shopro\user\Coupon as UserCouponModel;
+
+class Coupon extends Common
+{
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new UserCouponModel;
+    }
+
+    
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+        $coupon_id = $this->request->param('coupon_id');
+
+        $coupons = $this->model->sheepFilter()->with(['user', 'order'])
+            ->where('coupon_id', $coupon_id)
+            ->paginate($this->request->param('list_rows', 10));
+
+        $this->success('获取成功', null, $coupons);
+    }
+}

+ 27 - 0
addons/shopro/application/admin/controller/shopro/user/Group.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace app\admin\controller\shopro\user;
+
+use think\Db;
+use app\admin\controller\shopro\Common;
+use app\admin\model\UserGroup as GroupModel;
+
+class Group extends Common
+{
+    protected $noNeedRight = ['select'];
+
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new GroupModel;
+    }
+
+
+    public function select()
+    {
+        $list = \app\admin\model\UserGroup::field('id,name,status')->select();
+        $this->success('', null, $list);
+    }
+}

+ 152 - 0
addons/shopro/application/admin/controller/shopro/user/User.php

@@ -0,0 +1,152 @@
+<?php
+
+namespace app\admin\controller\shopro\user;
+
+use think\Db;
+use app\admin\controller\shopro\Common;
+use addons\shopro\service\Wallet as WalletService;
+use app\admin\model\shopro\user\User as UserModel;
+use app\admin\model\shopro\user\Coupon as UserCouponModel;
+
+class User extends Common
+{
+    protected $model = null;
+
+    protected $noNeedRight = ['select'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new UserModel;
+    }
+
+    /**
+     * 用户列表
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $data = $this->model->sheepFilter()->paginate($this->request->param('list_rows', 10));
+
+        $this->success('获取成功', null, $data);
+    }
+
+    /**
+     * 用户详情
+     *
+     * @param  $id
+     */
+    public function detail($id)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $user = $this->model->with(['third_oauth', 'parent_user'])->where('id', $id)->find();
+        if (!$user) {
+            $this->error(__('No Results were found'));
+        }
+
+        $this->success('获取成功', null, $user);
+    }
+
+    /**
+     * 更新用户
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function edit($id = null)
+    {
+        $params = $this->request->only(['username', 'nickname', 'mobile', 'password', 'avatar', 'gender', 'email', 'status']);
+
+        if (empty($params['password'])) unset($params['password']);
+        if (empty($params['username'])) unset($params['username']);
+
+        $params['id'] = $id;
+        $this->svalidate($params, '.edit');
+        unset($params['id']);
+
+        $user = $this->model->where('id', $id)->find();
+        $user->save($params);
+
+        $this->success('更新成功', null, $user);
+    }
+
+    /**
+     * 删除用户(支持批量)
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $id = explode(',', $id);
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list) {
+            $count = 0;
+            foreach ($list as $item) {
+                $count += $item->delete();
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('删除成功', null, $result);
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+
+    public function recharge()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only(['id', 'type', 'amount', 'memo']);
+        if (!in_array($params['type'], ['money', 'score'])) {
+            error_stop('参数错误');
+        }
+
+        $result = Db::transaction(function () use ($params) {
+            return WalletService::change($params['id'], $params['type'], $params['amount'], 'admin_recharge', [], $params['memo']);
+        });
+        if ($result) {
+            $this->success('充值成功');
+        }
+        $this->error('充值失败');
+    }
+
+
+    /**
+     * 用户优惠券列表
+     */
+    public function coupon($id)
+    {
+        $userCoupons = UserCouponModel::sheepFilter()->with('coupon')->where('user_id', $id)
+            ->order('id', 'desc')->paginate($this->request->param('list_rows', 10));
+
+        $this->success('获取成功', null, $userCoupons);
+    }
+
+
+    public function select()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $data = $this->model->sheepFilter()->paginate($this->request->param('list_rows', 10));
+
+        $this->success('获取成功', null, $data);
+    }
+}

+ 90 - 0
addons/shopro/application/admin/controller/shopro/user/WalletLog.php

@@ -0,0 +1,90 @@
+<?php
+
+namespace app\admin\controller\shopro\user;
+
+use app\admin\controller\shopro\Common;
+use app\admin\model\shopro\user\WalletLog as WalletLogModel;
+use addons\shopro\library\Operator;
+
+class WalletLog extends Common
+{
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new WalletLogModel;
+    }
+
+    /**
+     * 余额记录
+     */
+    public function money($id)
+    {
+        $list_rows = $this->request->param('list_rows', 10);
+        $walletLogs = WalletLogModel::where('user_id', $id)->money()->order('id', 'desc')->paginate($list_rows);
+
+        // 多态关联 oper
+        $morphs = [
+            'user' => \app\admin\model\shopro\user\User::class,
+            'admin' => \app\admin\model\Admin::class,
+            'system' => \app\admin\model\Admin::class,
+        ];
+        $walletLogs = morph_to($walletLogs, $morphs, ['oper_type', 'oper_id']);
+        $walletLogs = $walletLogs->toArray();
+
+        // 解析操作人信息
+        foreach ($walletLogs['data'] as &$log) {
+            $log['oper'] = Operator::info($log['oper_type'], $log['oper'] ?? null);
+        }
+        $this->success('', null, $walletLogs);
+    }
+
+    /**
+     * 积分记录
+     */
+    public function score($id)
+    {
+        $list_rows = $this->request->param('list_rows', 10);
+        $walletLogs = WalletLogModel::where('user_id', $id)->score()->order('id', 'desc')->paginate($list_rows);
+
+        // 多态关联 oper
+        $morphs = [
+            'user' => \app\admin\model\shopro\user\User::class,
+            'admin' => \app\admin\model\Admin::class,
+            'system' => \app\admin\model\Admin::class,
+        ];
+        $walletLogs = morph_to($walletLogs, $morphs, ['oper_type', 'oper_id']);
+        $walletLogs = $walletLogs->toArray();
+
+        // 解析操作人信息
+        foreach ($walletLogs['data'] as &$log) {
+            $log['oper'] = Operator::info($log['oper_type'], $log['oper'] ?? null);
+        }
+        $this->success('', null, $walletLogs);
+    }
+
+    /**
+     * 佣金记录
+     */
+    public function commission($id)
+    {
+        $list_rows = $this->request->param('list_rows', 10);
+        $walletLogs = WalletLogModel::where('user_id', $id)->commission()->order('id', 'desc')->paginate($list_rows);
+
+        // 多态关联 oper
+        $morphs = [
+            'user' => \app\admin\model\shopro\user\User::class,
+            'admin' => \app\admin\model\Admin::class,
+            'system' => \app\admin\model\Admin::class,
+        ];
+        $walletLogs = morph_to($walletLogs, $morphs, ['oper_type', 'oper_id']);
+        $walletLogs = $walletLogs->toArray();
+
+        // 解析操作人信息
+        foreach ($walletLogs['data'] as &$log) {
+            $log['oper'] = Operator::info($log['oper_type'], $log['oper'] ?? null);
+        }
+        $this->success('', null, $walletLogs);
+    }
+}

+ 137 - 0
addons/shopro/application/admin/controller/shopro/wechat/Admin.php

@@ -0,0 +1,137 @@
+<?php
+
+namespace app\admin\controller\shopro\wechat;
+
+use fast\Random;
+use addons\shopro\facade\Wechat;
+use app\admin\model\shopro\ThirdOauth;
+use app\admin\controller\shopro\Common;
+
+class Admin extends Common
+{
+
+    protected $wechat;
+    protected $noNeedRight = ['getQrcode', 'checkScan', 'unbind'];
+
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->wechat = Wechat::officialAccountManage();
+    }
+
+    // 获取公众号二维码
+    public function getQrcode()
+    {
+        $event = $this->request->param('event');
+
+        if (!in_array($event, ['bind'])) {
+            $this->error('参数错误');
+        }
+
+        $adminId = $this->auth->id;
+        $thirdOauth = ThirdOauth::where([
+            'provider' => 'wechat',
+            'platform' => 'admin',
+            'admin_id' => $adminId
+        ])->find();
+
+        if ($thirdOauth) {
+            error_stop('已绑定微信账号', -2, $thirdOauth);
+        }
+
+        // 二维码和缓存过期时间
+        $expireTime = 1 * 60;
+
+        // 事件唯一标识
+        $eventId = Random::uuid();
+
+        $cacheKey = "wechatAdmin.{$event}.{$eventId}";
+
+        cache($cacheKey, ['id' => 0], $expireTime);
+
+        try {
+            $result = $this->wechat->qrcode->temporary($cacheKey, $expireTime);
+            $qrcode = $this->wechat->qrcode->url($result['ticket']);
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+        
+        $this->success('', null, [
+            'url' => $qrcode,
+            'eventId' => $eventId
+        ]);
+    }
+
+    // 检查扫码结果
+    public function checkScan()
+    {
+        $event = $this->request->param('event');
+        $eventId = $this->request->param('eventId');
+
+        if (!in_array($event, ['bind'])) {
+            error_stop('参数错误');
+        }
+
+        $cacheKey = "wechatAdmin.{$event}.{$eventId}";
+
+        $cacheValue = cache($cacheKey);
+
+        if (empty($cacheValue)) {
+            error_stop('二维码已过期, 请重新扫码');
+        }
+
+        if ($cacheValue['id'] === 0) {
+            error_stop('等待扫码', -1);
+        }
+
+        if ($cacheValue['id'] !== 0) {
+            switch ($event) {
+                case 'bind':
+                    $adminId = $this->auth->id;
+
+                    $thirdOauth = ThirdOauth::where([
+                        'provider' => 'wechat',
+                        'platform' => 'admin',
+                        'openid' => $cacheValue['id'],
+                    ])->find();
+
+                    if ($thirdOauth && $thirdOauth->admin_id !== 0) {
+                        error_stop('该微信账号已被绑定');
+                    }
+
+                    if (!$thirdOauth) {
+                        $thirdOauth = ThirdOauth::create([
+                            'provider' => 'wechat',
+                            'platform' => 'admin',
+                            'openid' => $cacheValue['id'],
+                            'admin_id' => $adminId
+                        ]);
+                    } else {
+                        $thirdOauth->admin_id = $adminId;
+                        $thirdOauth->save();
+                    }
+                    break;
+            }
+            $this->success();
+        }
+    }
+
+    // 解绑
+    public function unbind()
+    {
+        $adminId = $this->auth->id;
+
+        $thirdOauth = ThirdOauth::where([
+            'provider' => 'wechat',
+            'platform' => 'admin',
+            'admin_id' => $adminId
+        ])->find();
+
+        if ($thirdOauth) {
+            $thirdOauth->admin_id = 0;
+            $thirdOauth->save();
+        }
+        $this->success();
+    }
+}

+ 30 - 0
addons/shopro/application/admin/controller/shopro/wechat/Config.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace app\admin\controller\shopro\wechat;
+
+use app\admin\model\shopro\Config as ShoproConfig;
+use app\admin\controller\shopro\Common;
+
+class Config extends Common
+{
+    /**
+     * 公众号配置
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        if ('GET' === $this->request->method()) {
+
+            $configs = ShoproConfig::getConfigs('wechat.officialAccount', false);
+            $configs['server_url'] = request()->domain() . '/addons/shopro/wechat.serve';
+        } elseif ('POST' === $this->request->method()) {
+
+            $configs = ShoproConfig::setConfigs('wechat.officialAccount', $this->request->param());
+        }
+
+        $this->success('操作成功', null, $configs);
+    }
+}

+ 178 - 0
addons/shopro/application/admin/controller/shopro/wechat/Material.php

@@ -0,0 +1,178 @@
+<?php
+
+namespace app\admin\controller\shopro\wechat;
+
+use app\admin\controller\shopro\Common;
+use addons\shopro\facade\Wechat;
+use think\Db;
+use addons\shopro\exception\ShoproException;
+
+class Material extends Common
+{
+
+    protected $wechat = null;
+    protected $model = null;
+    protected $noNeedRight = ['select'];
+
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->wechat = Wechat::officialAccountManage();
+        $this->model = new \app\admin\model\shopro\wechat\Material;
+    }
+
+    /**
+     * 素材列表
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $list_rows = intval($this->request->param('list_rows', 10));
+        $page = intval($this->request->param('page', 1));
+        $type = $this->request->param('type', 'news');
+        $offset = intval(($page - 1) * $list_rows);
+
+        if (in_array($type, ['text', 'link'])) {
+            $data = $this->model->sheepFilter()->where('type', $type)->paginate(request()->param('list_rows', 10));
+        } else {
+            // 使用微信远程素材列表
+            try {
+                $res = $this->wechat->material->list($type, $offset, $list_rows);
+            } catch (\Exception $e) {
+                $this->error($e->getMessage());
+            }
+          
+            $data = [
+                'current_page' => $page,
+                'data' => $res['item'],
+                'last_page' => intval(ceil($res['total_count'] / $list_rows)),
+                'per_page' => $list_rows,
+                'total' => intval($res['total_count']),
+            ];
+        }
+
+        $this->success('', null, $data);
+    }
+
+    /**
+     * 详情
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function detail($id)
+    {
+        $detail = $this->model->get($id);
+        if (!$detail) {
+            $this->error(__('No Results were found'));
+        }
+        $this->success('获取成功', null, $detail);
+    }
+
+    /**
+     * 添加
+     *
+     * @return \think\Response
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only(['type', 'content']);
+
+        if ($params['type'] === 'text') {
+            $params['content'] =  urldecode($params['content']);
+        }
+
+        Db::transaction(function () use ($params) {
+            $this->model->save($params);
+        });
+        $this->success('保存成功');
+    }
+
+    /**
+     * 编辑
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $material = $this->model->get($id);
+        $params = $this->request->only(['type', 'content']);
+        if ($params['type'] === 'text') {
+            $params['content'] =  urldecode($params['content']);
+        }
+
+        Db::transaction(function () use ($params, $material) {
+            $material->save($params);
+        });
+
+        $this->success('更新成功');
+    }
+
+
+    /**
+     * 删除
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function delete($id)
+    {
+        $is_real = $this->request->param('is_real', 0);
+        Db::transaction(function () use ($id, $is_real) {
+            $menu = $this->model->get($id);
+            if ($is_real) {
+                $menu->force()->delete();
+            } else {
+                $menu->delete();
+            }
+        });
+
+        $this->success('删除成功');
+    }
+
+    /**
+     * 菜单列表
+     *
+     * @return Response
+     */
+    public function select()
+    {
+        $list_rows = intval($this->request->param('list_rows', 10));
+        $page = intval($this->request->param('page', 1));
+        $type = $this->request->param('type', 'news');
+        $offset = intval(($page - 1) * $list_rows);
+
+        if (in_array($type, ['text', 'link'])) {
+            $data = $this->model->where('type', $type)->order('id desc')->paginate(request()->param('list_rows', 10));
+        } else {
+            // 使用微信远程素材列表
+            try {
+                $res = $this->wechat->material->list($type, $offset, $list_rows);
+            } catch (\Exception $e) {
+                $this->error($e->getMessage());
+            }
+
+            $data = [
+                'current_page' => $page,
+                'data' => $res['item'],
+                'last_page' => intval(ceil($res['total_count'] / $list_rows)),
+                'per_page' => $list_rows,
+                'total' => intval($res['total_count']),
+            ];
+        }
+        $this->success('获取成功', null, $data);
+    }
+}

+ 223 - 0
addons/shopro/application/admin/controller/shopro/wechat/Menu.php

@@ -0,0 +1,223 @@
+<?php
+
+namespace app\admin\controller\shopro\wechat;
+
+use app\admin\controller\shopro\Common;
+use addons\shopro\facade\Wechat;
+use think\Db;
+use addons\shopro\exception\ShoproException;
+
+class Menu extends Common
+{
+
+    protected $wechat = null;
+    protected $model = null;
+
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->wechat = Wechat::officialAccountManage();
+        $this->model = new \app\admin\model\shopro\wechat\Menu;
+    }
+
+    /**
+     * 公众号配置
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $current = $this->getCurrentMenu();
+        $data = $this->model->sheepFilter()->paginate(request()->param('list_rows', 10));
+
+        $this->success('操作成功', null, ['current' => $current, 'list' => $data]);
+    }
+
+    /**
+     * 详情
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function detail($id)
+    {
+        $detail = $this->model->get($id);
+        if (!$detail) {
+            $this->error(__('No Results were found'));
+        }
+        $this->success('获取成功', null, $detail);
+    }
+
+    /**
+     * 添加菜单
+     *
+     * @param  int $publish   发布状态:0=不发布,1=直接发布
+     * @return \think\Response
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $publish = $this->request->param('publish', 0);
+        $params = $this->request->only(['name', 'rules']);
+        // 参数校验
+        $this->svalidate($params, '.add');
+
+        Db::transaction(function () use ($params, $publish) {
+            $menu = $this->model->save($params);
+            if ($menu && $publish) {
+                $this->publishMenu($this->model->id);
+            }
+            return $menu;
+        });
+
+        $this->success('保存成功');
+    }
+
+    /**
+     * 编辑
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+
+        $menu = $this->model->get($id);
+
+        $publish = $this->request->param('publish', 0);
+        $params = $this->request->only(['name', 'rules']);
+
+        // 参数校验
+        $this->svalidate($params);
+
+        $menu = Db::transaction(function () use ($params, $menu, $publish) {
+            $menu->save($params);
+            if ($publish) {
+                $this->publishMenu($menu->id);
+            }
+            return $menu;
+        });
+
+        $this->success('更新成功');
+    }
+
+
+    /**
+     * 删除
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function delete($id)
+    {
+        Db::transaction(function () use ($id) {
+            $menu = $this->model->get($id);
+            $menu->delete();
+        });
+
+        $this->success('删除成功');
+    }
+
+
+    /**
+     * 发布菜单
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function publish($id)
+    {
+        Db::transaction(function () use ($id) {
+            return $this->publishMenu($id);
+        });
+
+        $this->success('发布成功');
+    }
+
+    /**
+     * 复制菜单
+     *
+     * @return Response
+     */
+    public function copy($id = 0)
+    {
+        if ($id == 0) {
+            $data = [
+                'name' => '复制 当前菜单',
+                'rules' => $this->getCurrentMenu(),
+            ];
+        } else {
+            $menu = $this->model->get($id);
+            $data = [
+                'name' => '复制 ' . $menu->name,
+                'rules' => $menu->rules
+            ];
+        }
+
+        $menu = $this->model->save($data);
+        $this->success('复制成功');
+    }
+
+    // 发布菜单
+    private function publishMenu($id)
+    {
+        $menu = $this->model->get($id);
+
+        if ($this->setCurrentMenu($menu->rules)) {
+            $this->model->where('id', '<>', $menu->id)->update(['status' => 0]);
+
+            return $menu->save([
+                'status' => 1,
+                'publishtime' => time()
+            ]);
+        }
+        return false;
+    }
+
+
+    // 获取当前菜单
+    private function getCurrentMenu()
+    {
+        try {
+            $currentMenu = $this->wechat->menu->current();
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+        if (isset($currentMenu['selfmenu_info']['button'])) {
+            $buttons = $currentMenu['selfmenu_info']['button'];
+            foreach ($buttons as &$button) {
+                if (isset($button['sub_button'])) {
+                    $button['sub_button'] = $button['sub_button']['list'];
+                }
+            }
+            return $buttons;
+        } else {
+            return [];
+        }
+    }
+
+    // 设置菜单
+    private function setCurrentMenu($rules)
+    {
+        try {
+            $result = $this->wechat->menu->create($rules);
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+
+        if (isset($result['errcode']) && $result['errcode'] === 0) {
+            return true;
+        } else {
+            $this->error($result['errmsg'] ?? '发布失败');
+        }
+    }
+}

+ 143 - 0
addons/shopro/application/admin/controller/shopro/wechat/Reply.php

@@ -0,0 +1,143 @@
+<?php
+
+namespace app\admin\controller\shopro\wechat;
+
+use app\admin\controller\shopro\Common;
+use think\Db;
+
+/**
+ * 常见问题
+ */
+class Reply extends Common
+{
+
+    /**
+     * Faq模型对象
+     * @var \app\admin\model\shopro\wechat\Reply
+     */
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new \app\admin\model\shopro\wechat\Reply;
+    }
+
+    /**
+     * 查看
+     *
+     * @return string|Json
+     * @throws \think\Exception
+     * @throws DbException
+     */
+    public function index()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $group = $this->request->param('group', 'keywords');
+        $data = $this->model->sheepFilter()->where('group', $group)->select(); 
+        $this->success('操作成功', null, $data);
+    }
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch();
+        }
+
+        $params = $this->request->only(['group', 'keywords', 'type', 'status', 'content']);
+
+        Db::transaction(function () use ($params) {
+            $result = $this->model->save($params);
+            if($result) {
+                $reply = $this->model;
+                if($reply->group !== 'keywords' && $reply->status === 'enable') {
+                    $this->model->where('group', $reply->group)->where('id', '<>', $reply->id)->enable()->update(['status' => 'disabled']);
+                }
+            }
+            return $result;
+        });
+
+        $this->success('保存成功');
+    }
+
+    /**
+     * 详情
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function detail($id)
+    {
+        $detail = $this->model->get($id);
+        if (!$detail) {
+            $this->error(__('No Results were found'));
+        }
+        $this->success('获取成功', null, $detail);
+    }
+
+    /**
+     * 编辑(支持批量)
+     */
+    public function edit($id = null)
+    {
+        if (!$this->request->isAjax()) {
+            return $this->view->fetch('add');
+        }
+        $reply = $this->model->get($id);
+        $params = $this->request->only(['keywords', 'type', 'status', 'content']);
+
+        // 参数校验
+        // $this->svalidate($params);
+
+        $result = Db::transaction(function () use ($params, $reply) {
+            $result = $reply->save($params);
+            if($result) {
+                if($reply->group !== 'keywords' && $reply->status === 'enable') {
+                    $this->model->where('group', $reply->group)->where('id', '<>', $reply->id)->enable()->update(['status' => 'disabled']);
+                }
+            }
+            return $result;
+        });
+
+        if ($result) {
+            $this->success('更新成功', null, $result);
+        } else {
+            $this->error('更新失败');
+        }
+    }
+
+    /**
+     * 删除(支持批量)
+     *
+     * @param  $id
+     * @return \think\Response
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error(__('Parameter %s can not be empty', 'id'));
+        }
+
+        $list = $this->model->where('id', 'in', $id)->select();
+        $result = Db::transaction(function () use ($list) {
+            $count = 0;
+            foreach ($list as $item) {
+                $count += $item->delete();
+            }
+
+            return $count;
+        });
+
+        if ($result) {
+            $this->success('删除成功', null, $result);
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+}

+ 53 - 0
addons/shopro/application/admin/model/shopro/Admin.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace app\admin\model\shopro;
+
+use app\admin\model\Admin as BaseAdmin;
+use addons\shopro\library\notify\traits\Notifiable;
+
+class Admin extends BaseAdmin
+{
+    use Notifiable;
+
+
+
+    /**
+     * 判断管理员是否由特定权限
+     *
+     * @param \think\Model $admin
+     * @param array $rules
+     * @return boolean
+     */
+    public function hasAccess(\think\Model $admin, array $rules = [])
+    {
+        $auth = \app\admin\library\Auth::instance();
+        $RuleIds = $auth->getRuleIds($admin->id);
+        $is_super = in_array('*', $RuleIds) ? 1 : 0;
+        if ($is_super) {
+            return true;
+        }
+
+        if ($auth->check(implode(',', $rules), $admin->id)) {
+            return true;
+        }
+
+        return false;
+    }
+
+
+    /**
+     * 是否是超级管理员
+     *
+     * @param \think\Model $admin
+     * @return boolean
+     */
+    public function isSuper(\think\Model $admin)
+    {
+        $auth = \app\admin\library\Auth::instance();
+        $RuleIds = $auth->getRuleIds($admin->id);
+
+        $is_super = in_array('*', $RuleIds) ? 1 : 0;
+        
+        return $is_super;
+    }
+}

+ 75 - 0
addons/shopro/application/admin/model/shopro/Cart.php

@@ -0,0 +1,75 @@
+<?php
+
+namespace app\admin\model\shopro;
+
+use app\admin\model\shopro\Common;
+use app\admin\model\shopro\goods\Goods;
+use app\admin\model\shopro\goods\SkuPrice;
+use addons\shopro\facade\Activity as ActivityFacade;
+
+class Cart extends Common
+{
+    protected $name = 'shopro_cart';
+
+    // 追加属性
+    protected $append = [
+    ];
+
+
+    /**
+     * 获取器获取所有活动
+     *
+     * @param string $value
+     * @param array $data
+     * @return array
+     */
+    public function getActivitiesAttr($value, $data)
+    {
+        $activities = ActivityFacade::getGoodsActivitys($data['id']);
+
+        return $activities;
+    }
+
+
+    public function getStatusAttr($value, $data)
+    {
+        $status = 'normal';
+        if (!$this->goods || !is_null($this->goods->deletetime) || !$this->sku_price) {
+            $status = 'deleted';        // 已删除
+        } else if ($this->goods->status == 'down' || $this->sku_price->status == 'down') {
+            $status = 'down';           // 已下架
+        } 
+
+        return $status;
+    }
+
+
+    public function getTagsAttr($value, $data) 
+    {
+        $tags = [
+            'activity' => [],
+        ];
+
+        $activities = $this->activities;
+        foreach ($activities as $activity) {
+            $tags['activity'][] = $activity['type_text'] . $activity['status_text'];
+        } 
+
+        if ($this->sku_price && $this->sku_price->price < $data['snapshot_price']) {
+            // 当前规格价格,低于加入购物车时候的价格,则提示商品比加入时降价
+            $tags['price'] = '距加入降 ¥ ' . bcsub($data['snapshot_price'], $this->sku_price->price, 2);
+        }
+    }
+
+
+    public function goods() 
+    {
+        return $this->belongsTo(Goods::class, 'goods_id');
+    }
+
+
+    public function skuPrice()
+    {
+        return $this->belongsTo(SkuPrice::class, 'goods_sku_price_id');
+    }
+}

+ 35 - 0
addons/shopro/application/admin/model/shopro/Category.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace app\admin\model\shopro;
+
+use app\admin\model\shopro\Common;
+
+class Category extends Common
+{
+    protected $name = 'shopro_category';
+
+    // 追加属性
+    protected $append = [
+        'status_text',
+    ];
+
+
+    public function getChildrenString($category)
+    {
+        $style = $category->style;
+        $string = 'children';
+        if (strpos($style, 'second') === 0) {
+            $string .= '.children';
+        } else if (strpos($style, 'third') === 0) {
+            $string .= '.children.children';
+        }
+
+        return $string;
+    }
+
+
+    public function children()
+    {
+        return $this->hasMany(self::class, 'parent_id', 'id')->normal()->order('weigh', 'desc')->order('id', 'asc');
+    }
+}

+ 80 - 0
addons/shopro/application/admin/model/shopro/Common.php

@@ -0,0 +1,80 @@
+<?php
+
+declare(strict_types=1);
+
+namespace app\admin\model\shopro;
+
+use think\Model;
+use addons\shopro\filter\BaseFilter;
+use think\db\Query;
+use app\admin\model\shopro\traits\ModelAttr;
+
+class Common extends Model
+{
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'integer';
+
+    protected $dateFormat = 'Y-m-d H:i:s';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    use ModelAttr;
+
+    /**
+     * 当前 model 对应的 filter 实例
+     *
+     * @return BaseFilter
+     */
+    public function filterInstance()
+    {
+        $filter_class = static::class;
+
+        $class = str_replace('app\admin\model\shopro', 'addons\shopro\filter',  $filter_class) . 'Filter';
+
+        if (!class_exists($class)) {
+            return new BaseFilter();
+        }
+        return new $class();
+    }
+
+
+    /**
+     * 查询范围 filter 搜索入口
+     *
+     * @param Query $query
+     * @return void
+     */
+    public function scopeSheepFilter($query, $sort = true, $filters = null)
+    {
+        $instance = $this->filterInstance();
+        $query = $instance->apply($query, $filters);
+        if ($sort) {
+            $query = $instance->filterOrder($query);
+        }
+
+        return $query;
+    }
+
+
+    /**
+     * 获取模型中文名
+     *
+     * @return string|null
+     */
+    // public function getModelName()
+    // {
+    //     if (isset($this->modelName)) {
+    //         $model_name = $this->modelName;
+    //     } else {
+    //         $tableComment = $this->tableComment();
+    //         $table_name = $this->getQuery()->getTable();
+    //         $model_name = $tableComment[$table_name] ?? null;
+    //     }
+
+    //     return $model_name;
+    // }
+}

+ 192 - 0
addons/shopro/application/admin/model/shopro/Config.php

@@ -0,0 +1,192 @@
+<?php
+
+namespace app\admin\model\shopro;
+
+class Config extends Common
+{
+    /**
+     * 主键
+     */
+    protected $pk = 'code';
+
+    protected $name = 'shopro_config';
+
+    protected $autoWriteTimestamp = false;
+
+    /**
+     * 获取配置组
+     *
+     * @param string $code
+     * @param boolean $cache
+     * @return array
+     */
+    public static function getConfigs($code, $cache = true)
+    {
+        // 从缓存中获取
+        if ($cache) {
+            $config = operate_disabled(false) ? cache("config:{$code}") : null;
+            if (empty($config)) {
+                $config = self::getConfigs($code, false);
+            }
+        }
+
+        // 从数据库中查找
+        if (!$cache) {
+            $row = self::where('code', $code)->find();
+            if(!$row) return null;
+            if($row['type'] !== 'group') {
+                $config = $row->value;
+            }else {
+                $config = [];
+                $list = self::where('parent_code', $code)->select();
+                foreach ($list as &$row) {
+                    if ($row['type'] === 'group') {
+                        $row->value = self::getConfigs($row->code, false);
+                    } else {
+                        cache("config:{$row->code}", $row->value);
+                    }
+                    $config[self::getShortCode($row)] = $row->value;
+                }
+            }
+            // 设置配置缓存
+            cache("config:{$code}", $config);
+        }
+
+        return $config;
+    }
+
+    /**
+     * 获取单一配置项
+     *
+     * @param string $code
+     * @param boolean $cache
+     * @return mixed
+     */
+    public static function getConfigField($code, $cache = true)
+    {
+        // 从缓存中获取
+        if ($cache) {
+            $config = cache("config:{$code}");
+            if (empty($config)) {
+                $config = self::getConfigField($code, false);
+            }
+        }
+
+        // 从数据库中查找
+        if (!$cache) {
+            $config = self::where('code', $code)->value('value');
+            // 设置配置缓存
+            cache("config:{$code}", $config);
+        }
+
+        return $config;
+    }
+
+    private static function getShortCode($config)
+    {
+        if (!empty($config['parent_code'])) {
+            return str_replace("{$config['parent_code']}.", "", $config['code']);
+        }
+        return $config['code'];
+    }
+
+    /**
+     * 更新配置
+     *
+     * @param string $code
+     * @param array $configParams
+     * @return void
+     */
+    public static function setConfigs(string $code, array $configParams)
+    {
+        operate_filter();
+        foreach ($configParams as $configKey => $configValue) {
+            self::setConfigField($code . '.' . $configKey, $configValue);
+        }
+        
+        self::getConfigs(explode('.', $code)[0], false);
+        return self::getConfigs($code);
+    }
+
+    /**
+     * 更新配置项
+     */
+    private static function setConfigField($code, $value)
+    {
+        $config = self::where('code', $code)->find();
+
+        if ($config) {
+            if ($config['type'] === 'group') {
+                foreach ($value as $k => $v) {
+                    self::setConfigField($code . '.' . $k, $v);
+                }
+            } else {
+                $config->value = $value;
+                $config->save();
+            }
+        }
+    }
+
+
+    /**
+     * 修改器 数据的保存格式
+     *
+     * @param string|array $value
+     * @param array $data
+     * @return string
+     */
+    public function setValueAttr($value, $data)
+    {
+        switch ($data['type']) {
+            case 'array':
+                $value = is_array($value) ? json_encode($value, JSON_UNESCAPED_UNICODE) : $value;
+                break;
+        }
+
+        return $value;
+    }
+
+
+    /**
+     * 获取器,选项
+     *
+     * @param string|array $value
+     * @param array $data
+     * @return array
+     */
+    public function getStoreRangeAttr($value, $data)
+    {
+        return $this->attrFormatJson($value, $data, 'store_range');
+    }
+
+
+
+    /**
+     * 获取器,返回的格式
+     *
+     * @param string|array $value
+     * @param array $data
+     * @return array
+     */
+    public function getValueAttr($value, $data)
+    {
+        $value = $value ?: ($data['value'] ?? null);
+
+        switch ($data['type']) {
+            case 'array':
+                $value = $this->attrFormatJson($value, $data, 'value', true);
+                break;
+            case 'boolean':
+                $value = intval($value) ? 1 : 0;
+                break;
+            case 'int':
+                $value = intval($value);
+                break;
+            case 'float':
+                $value = floatval($value);
+                break;
+        }
+
+        return config_show($value, $data);
+    }
+}

+ 320 - 0
addons/shopro/application/admin/model/shopro/Coupon.php

@@ -0,0 +1,320 @@
+<?php
+
+namespace app\admin\model\shopro;
+
+use app\admin\model\shopro\Common;
+use app\admin\model\shopro\user\Coupon as UserCouponModel;
+use app\admin\model\shopro\goods\Goods;
+use app\admin\model\shopro\Category;
+use traits\model\SoftDelete;
+
+class Coupon extends Common
+{
+    use SoftDelete;
+    protected $deleteTime = 'deletetime';
+    
+    protected $name = 'shopro_coupon';
+
+    protected $type = [
+        'get_start_time' => 'timestamp',
+        'get_end_time' => 'timestamp',
+        // 'use_start_time' => 'timestamp',
+        // 'use_end_time' => 'timestamp',
+    ];
+
+    // 追加属性
+    protected $append = [
+        'type_text',
+        'status_text',
+        'use_scope_text',
+        'amount_text',
+        'get_time_status',
+        'get_time_text'
+    ];
+
+
+    /**
+     * 默认类型列表
+     *
+     * @return array
+     */
+    public function typeList()
+    {
+        return [
+            'reduce' => '满减券',
+            'discount' => '折扣券'
+        ];
+    }
+    /**
+     * 可用范围列表
+     *
+     * @return array
+     */
+    public function useScopeList()
+    {
+        return [
+            'all_use' => '全场通用',
+            'goods' => '指定商品可用',
+            'disabled_goods' => '指定商品不可用',
+            'category' => '指定分类可用',
+        ];
+    }
+    /**
+     * 默认状态列表
+     *
+     * @return array
+     */
+    public function statusList()
+    {
+        return [
+            'normal' => '公开发放',
+            'hidden' => '后台发放',
+            'disabled' => '禁止使用',
+        ];
+    }
+
+
+    /**
+     * 优惠券领取状态
+     *
+     * @return void
+     */
+    public function getStatusList()
+    {
+        return [
+            'can_get' => '立即领取',
+            'cannot_get' => '已领取',
+            'get_over' => '已领完',
+
+            // 用户优惠券的状态
+            'used' => '已使用',
+            'can_use' => '立即使用',
+            'expired' => '已过期',
+            'cannot_use' => '暂不可用'
+        ];
+    }
+
+
+    public function scopeCanGet($query)
+    {
+        return $query->where('get_start_time', '<=', time())
+            ->where('get_end_time', '>=', time());
+    }
+
+
+    /**
+     * 查询指定商品满足的优惠券
+     *
+     * @param [type] $query
+     * @param [type] $goods
+     * @return void
+     */
+    public function scopeGoods($query, $goods)
+    {
+        $goods_id = $goods['id'];
+        $category_ids = $goods['category_ids'];
+
+        // 查询符合商品的优惠券
+        return $query->where(function ($query) use ($goods_id, $category_ids) {
+            $query->where('use_scope', 'all_use')
+            ->whereOr(function ($query) use ($goods_id) {
+                $query->where('use_scope', 'goods')->whereRaw("find_in_set($goods_id, items)");
+            })
+                ->whereOr(function ($query) use ($goods_id) {
+                    $query->where('use_scope', 'disabled_goods')->whereRaw("not find_in_set($goods_id, items)");
+                })
+                ->whereOr(function ($query) use ($goods_id, $category_ids) {
+                    $query->where('use_scope', 'category')->where(function ($query) use ($category_ids) {
+                        $category_ids = array_filter(explode(',', $category_ids));
+                        foreach ($category_ids as $key => $category_id) {
+                            $query->whereOrRaw("find_in_set($category_id, items)");
+                        }
+                    });
+                });
+        });
+    }
+
+
+
+        /**
+     * 开始使用时间获取器
+     *
+     * @param string $value
+     * @param array $data
+     * @return int
+     */
+    public function setUseStartTimeAttr($value, $data)
+    {
+        return $value ? strtotime($value) : (isset($data['use_start_time']) ? strtotime($data['use_start_time']) : 0);
+    }
+
+
+    /**
+     * 结束使用时间获取器
+     *
+     * @param string $value
+     * @param array $data
+     * @return int
+     */
+    public function setUseEndTimeAttr($value, $data)
+    {
+        return $value ? strtotime($value) : (isset($data['use_end_time']) ? strtotime($data['use_end_time']) : 0);
+    }
+
+
+    /**
+     * 可用范围获取器
+     *
+     * @param string $value
+     * @param array $data
+     * @return string
+     */
+    public function getUseScopeTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['use_scope'] ?? null);
+
+        $list = $this->useScopeList();
+        return isset($list[$value]) ? $list[$value] : '';
+    }
+
+
+    public function getAmountTextAttr($value, $data)
+    {
+        return '满' . $data['enough'] . '元,' . ($data['type'] == 'reduce' ? '减' . floatval($data['amount']) . '元' : '打' . floatval($data['amount']) . '折');
+    }
+
+    public function getItemsValueAttr($value, $data)
+    {
+        if (in_array($data['use_scope'], ['goods', 'disabled_goods'])) {
+            $items_value = Goods::whereIn('id', $data['items'])->select();
+            $items_value = collection($items_value);
+            $items_value = $items_value->each(function ($goods) {
+                // 前端要显示活动标签
+                $goods->promos = $goods->promos;
+            });
+        } else {
+            $items_value = Category::whereIn('id', $data['items'])->select();
+        }
+
+        return $items_value ?? [];
+    }
+
+    public function getGetStatusAttr($value, $data)
+    {
+        $limit_num = $data['limit_num'] ?? 0;
+        // 不限制领取次数,或者限制次数,领取数量还没达到最大值
+        $get_status = (!$limit_num || ($limit_num && $limit_num > count($this->user_coupons))) ? 'can_get' : 'cannot_get';
+
+        if ($get_status == 'can_get' && $data['stock'] <= 0) {
+            $get_status = 'get_over';       // 已领完
+        }
+
+        $user_coupon_id = request()->param('user_coupon_id', 0);
+        if ($user_coupon_id) {
+            // 从我领取的优惠券进详情,覆盖 状态
+            $user = auth_user();
+            $userCoupon = UserCouponModel::where('user_id', ($user ? $user->id : 0))->find($user_coupon_id);
+            if ($userCoupon) {
+                $get_status = $userCoupon->status;
+            }
+        }
+        return $get_status;
+    }
+
+
+    public function getGetStatusTextAttr($value, $data)
+    {
+        $list = $this->getStatusList();
+        return isset($list[$this->get_status]) ? $list[$this->get_status] : '';
+    }
+
+
+    /**
+     * 后端发放状态
+     *
+     * @return string
+     */
+    public function getGetTimeStatusAttr($value, $data) {
+        if ($data['get_start_time'] > time()) {
+            $time_text = 'nostart';        // 未开始
+        } else if ($data['get_start_time'] <= time() && $data['get_end_time'] >= time()) {
+            $time_text = 'ing';
+        } else if ($data['get_end_time'] < time()) {
+            $time_text = 'ended';
+        }
+
+        return $time_text;
+    }
+
+
+    /**
+     * 后端发放状态
+     *
+     * @return string
+     */
+    public function getGetTimeTextAttr($value, $data)
+    {
+        if ($this->get_time_status == 'nostart') {
+            $time_text = '未开始';        // 未开始
+        } else if ($this->get_time_status == 'ing') {
+            $time_text = '发放中';
+        } else if ($this->get_time_status == 'ended') {
+            $time_text = '已结束';
+        }
+
+        return $time_text;
+    }
+
+
+    public function getGetNumAttr($value, $data)
+    {
+        return UserCouponModel::where('coupon_id', $data['id'])->count();
+    }
+
+
+    public function getUseNumAttr($value, $data)
+    {
+        return UserCouponModel::where('coupon_id', $data['id'])->whereNotNull('use_time')->count();
+    }
+
+
+
+    public function getUseStartTimeAttr($value, $data)
+    {
+        $use_start_time = $value ? date('Y-m-d H:i:s', $value) : null;
+        $user_coupon_id = request()->param('user_coupon_id', 0);
+        if ($user_coupon_id && $data['use_time_type'] == 'days') {
+            // 从我领取的优惠券进详情,覆盖 状态
+            $user = auth_user();
+            $userCoupon = UserCouponModel::cache(60)->where('user_id', ($user ? $user->id : 0))->find($user_coupon_id);
+            if ($userCoupon) {
+                $use_start_time = date('Y-m-d H:i:s', $userCoupon->getData('createtime') + ($this->start_days * 86400));
+            }
+        }
+
+        return $use_start_time;
+    }
+
+    public function getUseEndTimeAttr($value, $data)
+    {
+        $use_end_time = $value ? date('Y-m-d H:i:s', $value) : null;
+        $user_coupon_id = request()->param('user_coupon_id', 0);
+        if ($user_coupon_id && $data['use_time_type'] == 'days') {
+            // 从我领取的优惠券进详情,覆盖 状态
+            $user = auth_user();
+            $userCoupon = UserCouponModel::cache(60)->where('user_id', ($user ? $user->id : 0))->find($user_coupon_id);
+            if ($userCoupon) {
+                $use_end_time = date('Y-m-d H:i:s', $userCoupon->getData('createtime') + (($this->start_days + $this->days) * 86400));
+            }
+        }
+
+        return $use_end_time;
+    }
+
+
+    public function userCoupons()
+    {
+        $user = auth_user();
+        return $this->hasMany(UserCouponModel::class, 'coupon_id')->where('user_id', ($user ? $user->id : 0));
+    }
+}

+ 36 - 0
addons/shopro/application/admin/model/shopro/Feedback.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace app\admin\model\shopro;
+
+use app\admin\model\shopro\Common;
+use app\admin\model\shopro\user\User;
+
+class Feedback extends Common
+{
+    protected $name = 'shopro_feedback';
+
+    protected $type = [
+        'images' => 'json'
+    ];
+    
+    protected $append = [
+        'status_text'
+    ];
+
+
+    /**
+     * 类型列表
+     *
+     * @return array
+     */
+    public function statusList()
+    {
+        return ['0' => '待处理', '1' => '已处理'];
+    }
+
+    public function user()
+    {
+        return $this->belongsTo(User::class, 'user_id', 'id')->field('id, nickname, avatar, mobile');
+    }
+
+}

+ 81 - 0
addons/shopro/application/admin/model/shopro/Pay.php

@@ -0,0 +1,81 @@
+<?php
+
+namespace app\admin\model\shopro;
+
+use app\admin\model\shopro\Common;
+
+class Pay extends Common
+{
+    protected $name = 'shopro_pay';
+
+    // 追加属性
+    protected $append = [
+        'pay_type_text',
+        'status_text'
+    ];
+
+    const PAY_STATUS_UNPAID = 'unpaid';
+    const PAY_STATUS_PAID = 'paid';
+    const PAY_STATUS_REFUND = 'refund';
+
+    public function statusList()
+    {
+        return [
+            self::PAY_STATUS_UNPAID => '未支付',
+            self::PAY_STATUS_PAID => '已支付',
+            self::PAY_STATUS_REFUND => '已退款'
+        ];
+    }
+
+
+    public function payTypeList()
+    {
+        return [
+            'wechat' => '微信支付',
+            'alipay' => '支付宝',
+            'money' => '钱包支付',
+            'score' => '积分支付',
+            'offline' => '货到付款',
+        ];
+    }
+
+
+    public function scopeTypeOrder($query)      // scopeOrder 调用时候,和 order 排序方法冲突了
+    {
+        return $query->where('order_type', 'order');
+    }
+
+
+    public function scopeTypeTradeOrder($query)
+    {
+        return $query->where('order_type', 'trade_order');
+    }
+
+
+    public function scopePaid($query)
+    {
+        return $query->where('status', self::PAY_STATUS_PAID);
+    }
+
+
+    public function scopeIsMoney($query)
+    {
+        return $query->whereIn('pay_type', ['wechat', 'alipay', 'money']);
+    }
+
+
+    /**
+     * 通用类型获取器
+     *
+     * @param string $value
+     * @param array $data
+     * @return string
+     */
+    public function getPayTypeTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['pay_type'] ?? null);
+
+        $list = $this->payTypeList();
+        return isset($list[$value]) ? $list[$value] : '';
+    }
+}

+ 46 - 0
addons/shopro/application/admin/model/shopro/PayConfig.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace app\admin\model\shopro;
+
+use app\admin\model\shopro\Common;
+use traits\model\SoftDelete;
+
+class PayConfig extends Common
+{
+    use SoftDelete;
+
+    protected $name = 'shopro_pay_config';
+
+    protected $deleteTime = 'deletetime';
+
+    protected $type = [
+        'params' => 'json',
+    ];
+
+    // 追加属性
+    protected $append = [
+        'type_text',
+        'status_text'
+    ];
+
+    protected $hidden = [
+        'params'
+    ];
+
+    public function statusList()
+    {
+        return [
+            'normal' => '正常',
+            'disabled' => '禁用',
+        ];
+    }
+
+
+    public function typeList()
+    {
+        return [
+            'wechat' => '微信支付',
+            'alipay' => '支付宝支付',
+        ];
+    }
+}

+ 38 - 0
addons/shopro/application/admin/model/shopro/Refund.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace app\admin\model\shopro;
+
+use app\admin\model\shopro\Common;
+
+class Refund extends Common
+{
+    protected $name = 'shopro_refund';
+
+    // 追加属性
+    protected $append = [
+        'status_text'
+    ];
+
+    const STATUS_ING = 'ing';
+    const STATUS_COMPLETED = 'completed';
+    const STATUS_FAIL = 'fail';
+
+    public function statusList()
+    {
+        return [
+            self::STATUS_ING => '退款中',
+            self::STATUS_COMPLETED => '退款完成',
+            self::STATUS_FAIL => '退款失败'
+        ];
+    }
+
+
+    public function refundTypeList()
+    {
+        return [
+            'back' => '原路退回',
+            'money' => '退回到余额'
+        ];
+    }
+
+}

+ 15 - 0
addons/shopro/application/admin/model/shopro/SearchHistory.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace app\admin\model\shopro;
+
+use app\admin\model\shopro\Common;
+
+class SearchHistory extends Common
+{
+    protected $name = 'shopro_search_history';
+    protected $type = [
+    ];
+    // 追加属性
+    protected $append = [
+    ];
+}

+ 142 - 0
addons/shopro/application/admin/model/shopro/Share.php

@@ -0,0 +1,142 @@
+<?php
+
+namespace app\admin\model\shopro;
+
+use app\admin\model\shopro\Common;
+use app\admin\model\shopro\user\User as UserModel;
+use app\admin\model\shopro\goods\Goods as GoodsModel;
+
+class Share extends Common
+{
+    protected $updateTime = false;
+    
+    protected $name = 'shopro_share';
+    
+    protected $type = [
+        'ext' => 'json'
+    ];
+    
+    protected $append = [
+        'platform_text',
+        'from_text'
+    ];
+
+    const FROM = ['forward' => '直接转发', 'poster' => '识别海报', 'link' => '分享链接'];
+
+    const PLATFORM = ['H5' => 'H5网页', 'WechatOfficialAccount' => '微信公众号网页', 'WechatMiniProgram' => '微信小程序', 'App' => 'APP'];
+
+    public function getPlatformTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['platform'] ?? null);
+
+        return (self::PLATFORM)[$value] ?? $value;
+    }
+
+    public function getFromTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['from'] ?? null);
+
+        return (self::FROM)[$value] ?? $value;
+    }
+
+    public static function log(Object $user, $params)
+    {
+
+        // 错误的分享参数
+        if (empty($params['spm'])) {
+            return false;
+        }
+
+        $shareId = $params['shareId'];
+        // 分享用户为空
+        if ($shareId <= 0) {
+            return false;
+        }
+
+        // 不能分享给本人
+        if ($shareId == $user->id) {
+            return false;
+        }
+
+        // 新用户不能分享给老用户 按需打开
+        // if($user->id < $shareId) {
+        //   return false;
+        // }
+
+        $shareUser = UserModel::where('id', $shareId)->find();
+        // 分享人不存在
+        if (!$shareUser) {
+            return false;
+        }
+
+        // 5分钟内相同的分享信息不保存,防止冗余数据
+        $lastShareLog = self::where([
+            'user_id' => $user->id
+        ])->where('createtime', '>', time() - 300)->order('id desc')->find();
+
+        if ($lastShareLog && $lastShareLog->spm === $params['spm']) {
+            return $lastShareLog;
+        }
+
+        $memoText = '通过' . (self::FROM)[$params['from']] . '访问了';
+        if ($params['page'] == '/pages/index/index') {
+            $memoText .= '首页';
+        }
+        if ($params['page'] === '/pages/goods/index') {
+            $memoText .= '商品';
+            $goodsId = $params['query']['id'];
+        }
+        if ($params['page'] === '/pages/goods/groupon') {
+            $memoText .= '拼团商品';
+            $goodsId = $params['query']['id'];
+        }
+        if ($params['page'] === '/pages/goods/seckill') {
+            $memoText .= '秒杀商品';
+            $goodsId = $params['query']['id'];
+        }
+        if ($params['page'] === '/pages/activity/groupon/detail') {
+            $memoText .= '拼团活动';
+        }
+
+        if (!empty($goodsId)) {
+            $goods = GoodsModel::find($goodsId);
+            if ($goods) {
+                $memoText .= "[{$goods->title}]";
+            }
+        }
+
+        $ext = [
+            'image' => $goods->image ?? "",
+            'memo' => $memoText
+        ];
+
+        $shareInfo = self::create([
+            'user_id' => $user->id,
+            'share_id' => $shareId,
+            'spm' => $params['spm'],
+            'page' => $params['page'],
+            'query' => http_build_query($params['query']),
+            'platform' => $params['platform'],
+            'from' => $params['from'],
+            'ext' => $ext
+        ]);
+
+        $data = ['shareInfo' => $shareInfo];
+        \think\Hook::listen('user_share_after', $data);
+
+        return $shareInfo;
+    }
+
+
+    // -- commission code start --
+    public function agent()
+    {
+        return $this->belongsTo(\app\admin\model\shopro\commission\Agent::class, 'share_id', 'user_id');
+    }
+    // -- commission code end --
+
+    public function user()
+    {
+        return $this->belongsTo(UserModel::class, 'user_id', 'id');
+    }
+}

+ 9 - 0
addons/shopro/application/admin/model/shopro/ThirdOauth.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace app\admin\model\shopro;
+
+class ThirdOauth extends Common
+{
+    protected $name = 'shopro_third_oauth';
+
+}

+ 83 - 0
addons/shopro/application/admin/model/shopro/Withdraw.php

@@ -0,0 +1,83 @@
+<?php
+
+namespace app\admin\model\shopro;
+
+use app\admin\model\shopro\Common;
+use app\admin\model\shopro\user\User;
+
+class Withdraw extends Common
+{
+    protected $name = 'shopro_withdraw';
+    protected $type = [
+        'withdraw_info' => 'json'
+    ];
+    // 追加属性
+    protected $append = [
+        'status_text',
+        'charge_rate_format',
+        'withdraw_info_hidden',
+        'withdraw_type_text'
+    ];
+
+    public function statusList()
+    {
+        return [
+            -1 => '已拒绝',
+            0 => '待审核',
+            1 => '处理中',
+            2 => '已处理'
+        ];
+    }
+
+
+    public function withdrawTypeList()
+    {
+        return [
+            'wechat' => '微信零钱',
+            'alipay' => '支付包账户',
+            'back' => '银行卡',
+        ];
+    }
+
+
+    /**
+     * 类型获取器
+     */
+    public function getWithdrawTypeTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['withdraw_type'] ?? null);
+
+        $list = $this->withdrawTypeList();
+        return isset($list[$value]) ? $list[$value] : '';
+    }
+
+
+
+    public function getChargeRateFormatAttr($value, $data)
+    {
+        $value = $value ?: ($data['charge_rate'] ?? null);
+
+        return bcmul((string)$value, '100', 1) . '%';
+    }
+
+    public function getWithdrawInfoHiddenAttr($value, $data) 
+    {
+        $withdraw_info = $value ?: ($this->withdraw_info ?? null);
+
+        foreach ($withdraw_info as $key => &$info) {
+            if (in_array($key, ['微信用户', '真实姓名'])) {
+                $info = string_hide($info, 2);
+            } elseif (in_array($key, ['银行卡号', '支付宝账户', '微信ID'])) {
+                $info = account_hide($info);
+            }
+        }
+
+        return $withdraw_info;
+    }
+
+
+    public function user()
+    {
+        return $this->belongsTo(User::class, 'user_id', 'id')->field('id, nickname, avatar, total_consume');
+    }
+}

+ 13 - 0
addons/shopro/application/admin/model/shopro/WithdrawLog.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace app\admin\model\shopro;
+
+use app\admin\model\shopro\Common;
+
+
+class WithdrawLog extends Common
+{
+    protected $name = 'shopro_withdraw_log';
+
+
+}

+ 407 - 0
addons/shopro/application/admin/model/shopro/activity/Activity.php

@@ -0,0 +1,407 @@
+<?php
+
+namespace app\admin\model\shopro\activity;
+
+use app\admin\model\shopro\Common;
+use traits\model\SoftDelete;
+use app\admin\model\shopro\goods\Goods;
+use addons\shopro\facade\Activity as ActivityFacade;
+use app\admin\model\shopro\activity\SkuPrice as ActivitySkuPriceModel;
+
+class Activity extends Common
+{
+    use SoftDelete;
+
+    protected $name = 'shopro_activity';
+
+    protected $deleteTime = 'deletetime';
+
+    protected $type = [
+        'rules' => 'json',
+        'prehead_time' => 'timestamp',
+        'start_time' => 'timestamp',
+        'end_time' => 'timestamp',
+    ];
+
+    // 追加属性
+    protected $append = [
+        'status',
+        'status_text',
+        'type_text',
+        // 'end_time_unix'     // 不需要了
+    ];
+
+
+    public function classifies()
+    {
+        return [
+            'activity' => [
+                'groupon' => '拼团',
+                'groupon_ladder' => '阶梯拼团',
+                // 'groupon_lucky' => '幸运拼团',
+                'seckill' => '秒杀',
+            ],
+            'promo' => [
+                'full_reduce' => '满减',
+                'full_discount' => '满折',
+                'full_gift' => '满赠',
+                'free_shipping' => '满邮',
+            ],
+            'app' => [
+                'signin' => '签到'
+            ]
+        ];
+    }
+
+    public function typeList()
+    {
+        return [
+            'groupon' => '拼团',
+            'groupon_ladder' => '阶梯拼团',
+            // 'groupon_lucky' => '幸运拼团',
+            'seckill' => '秒杀',
+            'full_reduce' => '满减',
+            'full_discount' => '满折',
+            'full_gift' => '满赠',
+            'free_shipping' => '满邮',
+            'signin' => '签到',
+        ];
+    }
+
+
+    /**
+     * 获取活动的互斥活动
+     *
+     * @param string $current_activity_type
+     * @return array
+     */
+    public function getMutexActivityTypes($current_activity_type)
+    {
+        $activityTypes = [];
+        switch ($current_activity_type) {
+            case 'groupon':
+                $activityTypes = ['groupon'];
+                break;
+            case 'groupon_ladder':
+                $activityTypes = ['groupon_ladder'];
+                break;
+            case 'groupon_lucky':
+                $activityTypes = ['groupon_lucky'];
+                break;
+            case 'seckill':
+                $activityTypes = ['seckill'];
+                break;
+            case 'full_reduce':
+                $activityTypes = ['full_reduce', 'full_discount'];
+                break;
+            case 'full_discount':
+                $activityTypes = ['full_reduce', 'full_discount'];
+                break;
+            case 'free_shipping':
+                $activityTypes = ['free_shipping'];
+                break;
+            case 'full_gift':
+                $activityTypes = ['full_gift'];
+                break;
+            case 'signin':
+                $activityTypes = ['signin'];
+                break;
+        }
+
+        return $activityTypes;
+    }
+
+
+    /**
+     * 根据类型获取 classify
+     *
+     * @param string $type
+     * @return string
+     */
+    public function getClassify($type)
+    {
+        $classifys = $this->classifies();
+        $activitys = array_keys($classifys['activity']);
+        $promos = array_keys($classifys['promo']);
+        $apps = array_keys($classifys['app']);
+
+        $classify = null;
+        if (in_array($type, $activitys)) {
+            $classify = 'activity';
+        } else if (in_array($type, $promos)) {
+            $classify = 'promo';
+        } else if (in_array($type, $apps)) {
+            $classify = 'app';
+        }
+
+        return $classify;
+    }
+
+
+
+    /**
+     * status 组合 (在thinkphp5 where Closure 中,不能直接使用 scope,特殊场景下用来代替下面的 scopeNostart scopePrehead 等)
+     *
+     * @param [type] $query
+     * @param [type] $status
+     * @return void
+     */
+    public function scopeStatusComb($query, $status)
+    {
+        return $query->where(function ($query) use ($status) {
+            foreach ($status as $st) {
+                $query->whereOr(function ($query) use ($st) {
+                    switch($st) {
+                        case 'nostart':
+                            $query->where('start_time', '>', time());
+                            break;
+                        case 'prehead':
+                            $query->where('prehead_time', '<=', time())->where('start_time', '>', time());
+                            break;
+                        case 'ing':
+                            $query->where('start_time', '<=', time())->where('end_time', '>=', time());
+                            break;
+                        case 'show':
+                            $query->where('prehead_time', '<=', time())->where('end_time', '>=', time());
+                            break;
+                        case 'ended':
+                            $query->where('end_time', '<', time());
+                            break;
+                        default:
+                            error_stop('status 状态错误');
+                    }
+                });
+            }
+        });
+    }
+
+
+    /**
+     * 未开始的活动
+     *
+     * @param think\query\Query $query
+     * @return void
+     */
+    public function scopeNostart($query)
+    {
+        return $query->where('start_time', '>', time());
+    }
+
+
+    /**
+     * 预售的活动
+     *
+     * @param think\query\Query $query
+     * @return void
+     */
+    public function scopePrehead($query)
+    {
+        return $query->where('prehead_time', '<=', time())->where('start_time', '>', time());
+    }
+
+    /**
+     * 进行中的活动
+     *
+     * @param think\query\Query $query
+     * @return void
+     */
+    public function scopeIng($query)
+    {
+        return $query->where('start_time', '<=', time())->where('end_time', '>=', time());
+    }
+
+    /**
+     * 已经开始预售,并且没有结束的活动
+     *
+     * @param think\query\Query $query
+     * @return void
+     */
+    public function scopeShow($query)
+    {
+        return $query->where('prehead_time', '<=', time())->where('end_time', '>=', time());
+    }
+
+    /**
+     * 已经结束的活动
+     *
+     * @param think\query\Query $query
+     * @return void
+     */
+    public function scopeEnded($query)
+    {
+        return $query->where('end_time', '<', time());
+    }
+
+
+
+    /**
+     * 修改器 classify
+     *
+     * @param string $value
+     * @param array $data
+     * @return integer|null
+     */
+    public function setClassifyAttr($value, $data)
+    {
+        $classify = $value ?: ($data['classify'] ?? null);
+        if (!$classify) {
+            $type = $data['type'] ?? null;        // 活动类型
+
+            $classify = $this->getClassify($type);
+        }
+
+        return $classify;
+    }
+
+
+    /**
+     * 修改器 预热时间
+     *
+     * @param string $value
+     * @param array $data
+     * @return integer|null
+     */
+    public function setPreheadTimeAttr($value, $data)
+    {
+        // promo 类型 prehead_time 永远等于 start_time
+        $value = (isset($data['classify']) && $data['classify'] == 'promo') ? $data['start_time'] : ($value ?: $data['start_time']);
+        return $this->attrFormatUnix($value);
+    }
+
+    /**
+     * 修改器 开始时间
+     *
+     * @param string $value
+     * @return integer|null
+     */
+    public function setStartTimeAttr($value)
+    {
+        return $this->attrFormatUnix($value);
+    }
+
+    /**
+     * 修改器 结束时间
+     *
+     * @param string $value
+     * @return integer|null
+     */
+    public function setEndTimeAttr($value)
+    {
+        return $this->attrFormatUnix($value);
+    }
+
+
+    public function getStatusAttr($value, $data)
+    {
+        return $this->getStatusCode($data['prehead_time'], $data['start_time'], $data['end_time']);
+    }
+
+
+    public function getStatusTextAttr($value, $data)
+    {
+        return $this->getStatusText($this->status);
+    }
+
+
+    public function getGoodsListAttr($value, $data)
+    {
+        if ($data['goods_ids']) {
+            
+            $goods = Goods::field('id,title,price,sales,image,status')->whereIn('id', $data['goods_ids'])->select();
+            $goods = collection($goods)->toArray();             // 全部转数组
+            
+            $goodsIds = array_column($goods, 'id');
+            $activitySkuPrices = ActivitySkuPriceModel::where('activity_id', $data['id'])->whereIn('goods_id', $goodsIds)->order('id', 'asc')->select();
+            $activitySkuPrices = collection($activitySkuPrices)->toArray();
+            
+            // 后台编辑活动时,防止不编辑规格无法提交问题
+            foreach ($goods as &$gd) {
+                // 处理 $gd['activity_sku_prices']
+                $gd['activity_sku_prices'] = [];
+                foreach ($activitySkuPrices as $skuPrice) {
+                    if ($skuPrice['goods_id'] == $gd['id']) {
+                        $gd['activity_sku_prices'][] = $skuPrice;
+                    }
+                }
+
+                // 处理活动规格,数据
+                foreach ($gd['activity_sku_prices'] as $key => $skuPrice) {
+                    $skuPrice = ActivityFacade::showSkuPrice($data['type'], $skuPrice);
+                    $gd['activity_sku_prices'][$key] = $skuPrice;
+                }
+            }
+        }
+
+        return $goods ?? [];
+    }
+
+
+    public function getRulesAttr($value, $data)
+    {
+        $rules = $data['rules'] ? json_decode($data['rules'], true) : [];
+        $type = $data['type'];
+
+        // 获取各个活动规则相关的特殊数据
+        $rules = ActivityFacade::rulesInfo($type, $rules);
+
+        return $rules;
+    }
+
+
+    /**
+     * 通过时间判断活动状态
+     *
+     * @param integer $prehead_time 预热时间
+     * @param integer $start_time    开始时间
+     * @param integer $end_time      结束时间
+     * @return string
+     */
+    public static function getStatusCode($prehead_time, $start_time, $end_time)
+    {
+        // 转为时间戳,(从 redis 中取出来的是 时间格式)
+        if (($prehead_time && $prehead_time > time()) || (!$prehead_time && $start_time > time())) {
+            $status = 'nostart';        // 未开始
+        } else if ($prehead_time && $prehead_time < time() && $start_time > time()) {
+            $status = 'prehead';        // 预热
+        } else if ($start_time < time() && $end_time > time()) {
+            $status = 'ing';
+        } else if ($end_time < time()) {
+            $status = 'ended';
+        }
+
+        return $status ?? 'ended';
+    }
+
+    /**
+     * 判断活动状态中文
+     *
+     * @param string $status      活动状态
+     * @return string
+     */
+    public static function getStatusText($status)
+    {
+        if ($status == 'nostart') {
+            $status_text = '未开始';
+        } elseif ($status == 'prehead') {
+            $status_text = '预热中';
+        } elseif ($status == 'ing') {
+            $status_text = '进行中';
+        } elseif ($status == 'ended') {
+            $status_text = '已结束';
+        }
+
+        return $status_text ?? '已结束';
+    }
+
+
+    public function getEndTimeUnixAttr($value, $data)
+    {
+        return isset($data['end_time']) ? $this->getData('end_time') : 0;
+    }
+
+
+    public function activitySkuPrices()
+    {
+        return $this->hasMany(SkuPrice::class, 'activity_id');
+    }
+}

+ 73 - 0
addons/shopro/application/admin/model/shopro/activity/GiftLog.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace app\admin\model\shopro\activity;
+
+use app\admin\model\shopro\Common;
+
+class GiftLog extends Common
+{
+    protected $name = 'shopro_activity_gift_log';
+
+    protected $type = [
+        'rules' => 'json',
+        'errors' => 'json'
+    ];
+
+    // 追加属性
+    protected $append = [
+        'type_text',
+        'status_text'
+    ];
+
+
+    /**
+     * 默认状态列表
+     *
+     * @return array
+     */
+    public function typeList()
+    {
+        return [
+            'coupon' => '优惠券',
+            'score' => '积分',
+            'money' => '余额',
+            'goods' => '商品',
+        ];
+    }
+
+
+    /**
+     * 默认状态列表
+     *
+     * @return array
+     */
+    public function statusList()
+    {
+        return [
+            'waiting' => '等待赠送',
+            'finish' => '赠送完成',
+            'fail' => '赠送失败',
+        ];
+    }
+
+
+    public function scopeWaiting($query)
+    {
+        return $query->where('status', 'waiting');
+    }
+
+    public function scopeOpered($query)
+    {
+        return $query->whereIn('status', ['finish', 'fail']);
+    }
+
+    public function scopeFinish($query)
+    {
+        return $query->where('status', 'finish');
+    }
+
+    public function scopeFail($query)
+    {
+        return $query->where('status', 'fail');
+    }
+}

+ 98 - 0
addons/shopro/application/admin/model/shopro/activity/Groupon.php

@@ -0,0 +1,98 @@
+<?php
+
+namespace app\admin\model\shopro\activity;
+
+use app\admin\model\shopro\Common;
+use app\admin\model\shopro\user\User;
+use app\admin\model\shopro\goods\Goods;
+use app\admin\model\shopro\order\OrderItem;
+
+class Groupon extends Common
+{
+    protected $name = 'shopro_activity_groupon';
+
+    protected $type = [
+        'finish_time' => 'timestamp',
+        'expire_time' => 'timestamp'
+    ];
+
+    // 追加属性
+    protected $append = [
+        'status_text',
+    ];
+
+
+    public function statusList()
+    {
+        return [
+            'invalid' => '拼团失败',
+            'ing' => '进行中',
+            'finish' => '已成团',
+            'finish_fictitious' => '虚拟成团',
+        ];
+    }
+
+    /**
+     * 查询正在进行中的团
+     */
+    public function scopeIng($query)
+    {
+        return $query->where('status', 'ing');
+    }
+
+
+    public function getStatusTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['status'] ?? '');
+
+        $list = $this->statusList();
+        $value = ($value == 'finish_fictitious' && strpos(request()->url(),  'addons/shopro') !== false) ? 'finish' : $value;
+        return isset($list[$value]) ? $list[$value] : '';
+    }
+
+
+    public function getExpireTimeUnixAttr($value, $data)
+    {
+        return isset($data['expire_time']) ? $this->getData('expire_time') : 0;
+    }
+
+
+    public function user()
+    {
+        return $this->belongsTo(User::class, 'user_id', 'id');
+    }
+
+
+    public function goods()
+    {
+        return $this->belongsTo(Goods::class, 'goods_id', 'id');
+    }
+
+    public function activity()
+    {
+        return $this->belongsTo(Activity::class, 'activity_id', 'id');
+    }
+
+    public function grouponLogs()
+    {
+        return $this->hasMany(GrouponLog::class, 'groupon_id', 'id');
+    }
+
+
+    public function leader()
+    {
+        return $this->hasOne(GrouponLog::class, 'groupon_id', 'id')->where('is_leader', 1);
+    }
+
+
+    /**
+     * 前端拼团详情用
+     *
+     * @return void
+     */
+    public function my()
+    {
+        $user = auth_user();
+        return $this->hasOne(GrouponLog::class, 'groupon_id', 'id')->where('user_id', ($user ? $user->id : 0))->where('is_fictitious', 0);
+    }
+}

+ 43 - 0
addons/shopro/application/admin/model/shopro/activity/GrouponLog.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace app\admin\model\shopro\activity;
+
+use app\admin\model\shopro\Common;
+use app\admin\model\shopro\user\User;
+use app\admin\model\shopro\goods\Goods;
+use app\admin\model\shopro\order\Order;
+use app\admin\model\shopro\order\OrderItem;
+
+class GrouponLog extends Common
+{
+    protected $name = 'shopro_activity_groupon_log';
+
+    public function getNicknameAttr($value, $data)
+    {
+        $value = $value ?: ($data['nickname'] ?? '');
+        return $value ? string_hide($value, 2) : $value;
+    }
+
+
+    public function order()
+    {
+        return $this->belongsTo(Order::class, 'order_id');
+    }
+
+
+    public function groupon()
+    {
+        return $this->belongsTo(Groupon::class, 'groupon_id');
+    }
+
+    public function goods()
+    {
+        return $this->belongsTo(Goods::class, 'goods_id');
+    }
+
+
+    public function orderItem()
+    {
+        return $this->hasOne(OrderItem::class, 'order_id', 'order_id');
+    }
+}

+ 93 - 0
addons/shopro/application/admin/model/shopro/activity/Order.php

@@ -0,0 +1,93 @@
+<?php
+
+namespace app\admin\model\shopro\activity;
+
+use app\admin\model\shopro\Common;
+use addons\shopro\facade\Activity as ActivityFacade;
+
+class Order extends Common
+{
+
+    protected $name = 'shopro_activity_order';
+
+    protected $type = [
+        'ext' => 'json',
+    ];
+
+    // 追加属性
+    protected $append = [
+        'status_text',
+        'activity_type_text',
+        'discount_text'
+    ];
+
+    const STATUS_UNPAID = 'unpaid';
+    const STATUS_PAID = 'paid';
+
+    public function statusList()
+    {
+        return [
+            'unpaid' => '未支付',
+            'paid' => '已支付',
+        ];
+    }
+
+    // 未支付
+    public function scopeUnpaid($query)
+    {
+        return $query->where('status', self::STATUS_UNPAID);
+    }
+
+    // 已支付
+    public function scopePaid($query)
+    {
+        return $query->whereIn('status', [self::STATUS_PAID]);
+    }
+
+
+
+    public function getActivityTypeTextAttr($value, $data) 
+    {
+        $value = $value ?: ($data['activity_type'] ?? null);
+        $ext = $this->ext;
+
+        $list = (new Activity)->typeList();
+        $text = isset($list[$value]) ? $list[$value] : '';
+
+        if (in_array($value, ['groupon', 'groupon_ladder'])) {
+            if (in_array($data['status'], [self::STATUS_PAID]) && (!isset($ext['groupon_id']) || !$ext['groupon_id'])) {
+                // 已支付,并且没有团 id,就是单独购买
+                $text .= '-单独购买';
+            }
+        }
+
+        return $text;
+    }
+
+
+    public function getDiscountTextAttr($value, $data) 
+    {
+        $ext = $this->ext;
+        $discount_text = '';
+        if ($ext && isset($ext['rules']) && $ext['rules']) {
+            $tags = ActivityFacade::formatRuleTags([
+                'type' => $ext['rules']['rule_type'],
+                'discounts' => [$ext['rules']['discount_rule']]
+            ], $data['activity_type']);
+
+            $discount_text = $tags[0] ?? '';
+        }
+
+        return $discount_text ?: $this->activity_type_text;
+    }
+
+
+
+    public function getGoodsIdsAttr($value, $data)
+    {
+        return $this->attrFormatComma($value, $data, 'goods_ids', true);
+    }
+    
+
+
+}

+ 21 - 0
addons/shopro/application/admin/model/shopro/activity/Signin.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace app\admin\model\shopro\activity;
+
+use app\admin\model\shopro\Common;
+
+class Signin extends Common
+{
+
+    protected $name = 'shopro_activity_signin';
+
+    protected $type = [
+        'rules' => 'json'
+    ];
+
+    // 追加属性
+    protected $append = [
+        
+    ];
+
+}

+ 128 - 0
addons/shopro/application/admin/model/shopro/activity/SkuPrice.php

@@ -0,0 +1,128 @@
+<?php
+
+namespace app\admin\model\shopro\activity;
+
+use app\admin\model\shopro\Common;
+
+class SkuPrice extends Common
+{
+    protected $name = 'shopro_activity_sku_price';
+
+    protected $type = [
+        'ext' => 'json',
+    ];
+
+    // 追加属性
+    protected $append = [
+        'status_text',
+    ];
+
+
+    /**
+     * 普通拼团,团长价
+     *
+     * @param string $value
+     * @param array $data
+     * @return string
+     */
+    public function getLeaderPriceAttr($value, $data)
+    {
+        $ext = $data['ext'];
+        $is_leader_discount = $ext['is_leader_discount'] ?? 0;
+        $leader_price = $ext['leader_price'] ?? 0;
+
+        $leader_price = $is_leader_discount ? $leader_price : $data['price'];
+        return $leader_price;
+    }
+
+
+
+    private function currentLadder($data, $level, $is_leader = false)
+    {
+        $ext = $data['ext'];
+        $is_leader_discount = $ext['is_leader_discount'] ?? 0;
+        $ladders = $ext['ladders'] ?? [];
+        $ladders = array_column($ladders, null, 'ladder_level');
+        $currentLadder = $ladders[$level] ?? [];       // 当前阶梯的 价格数据
+
+        $key = ($is_leader && $is_leader_discount) ? 'leader_ladder_price' : 'ladder_price';
+        return $currentLadder[$key] ?? 0;
+    }
+
+    /**
+     * 阶梯拼团,第一阶梯价
+     *
+     * @param string $value
+     * @param array $data
+     * @return string
+     */
+    public function getLadderOneAttr($value, $data)
+    {
+        return $this->currentLadder($data, 'ladder_one');
+    }
+
+
+    /**
+     * 阶梯拼团,第二阶梯价
+     *
+     * @param string $value
+     * @param array $data
+     * @return string
+     */
+    public function getLadderTwoAttr($value, $data)
+    {
+        return $this->currentLadder($data, 'ladder_two');
+    }
+
+
+    /**
+     * 阶梯拼团,第二阶梯价
+     *
+     * @param string $value
+     * @param array $data
+     * @return string
+     */
+    public function getLadderThreeAttr($value, $data)
+    {
+        return $this->currentLadder($data, 'ladder_three');
+    }
+
+
+    /**
+     * 阶梯拼团,第一阶梯团长价
+     *
+     * @param string $value
+     * @param array $data
+     * @return string
+     */
+    public function getLadderOneLeaderAttr($value, $data)
+    {
+        return $this->currentLadder($data, 'ladder_one', true);
+    }
+
+
+    /**
+     * 阶梯拼团,第二阶团长梯价
+     *
+     * @param string $value
+     * @param array $data
+     * @return string
+     */
+    public function getLadderTwoLeaderAttr($value, $data)
+    {
+        return $this->currentLadder($data, 'ladder_two', true);
+    }
+
+
+    /**
+     * 阶梯拼团,第二阶团长梯价
+     *
+     * @param string $value
+     * @param array $data
+     * @return string
+     */
+    public function getLadderThreeLeaderAttr($value, $data)
+    {
+        return $this->currentLadder($data, 'ladder_three', true);
+    }
+}

+ 63 - 0
addons/shopro/application/admin/model/shopro/app/ScoreSkuPrice.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace app\admin\model\shopro\app;
+
+use app\admin\model\shopro\Common;
+use traits\model\SoftDelete;
+use app\admin\model\shopro\goods\SkuPrice;
+
+class ScoreSkuPrice extends Common
+{
+    use SoftDelete;
+    protected $name = 'shopro_score_sku_price';
+
+    protected $deleteTime = 'deletetime';
+
+    protected $type = [
+    ];
+
+    // 追加属性
+    protected $append = [
+
+    ];
+
+    public function getGoodsSkuIdsAttr($value, $data) 
+    {
+        $skuPrice = $this->sku_price;
+        return $skuPrice ? $skuPrice->goods_sku_ids : '';
+    }
+
+
+    public function getGoodsSkuTextAttr($value, $data)
+    {
+        $skuPrice = $this->sku_price;
+        return $skuPrice ? $skuPrice->goods_sku_text : '';
+    }
+
+    public function getImageAttr($value, $data)
+    {
+        $skuPrice = $this->sku_price;
+        return $skuPrice ? $skuPrice->image : '';
+    }
+
+
+    /**
+     * 积分加现金
+     *
+     * @param string $value
+     * @param array $data
+     * @return string
+     */
+    public function getScorePriceAttr($value, $data)
+    {
+        $score = $data['score'] ?? 0;
+        $price = $data['price'] ?? 0;
+
+        return $score . '积分' . ($price ? '+¥' . $price : '');
+    }
+
+
+    public function skuPrice() {
+        return $this->belongsTo(SkuPrice::class, 'goods_sku_price_id');
+    }
+}

+ 53 - 0
addons/shopro/application/admin/model/shopro/app/mplive/Goods.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace app\admin\model\shopro\app\mplive;
+
+use app\admin\model\shopro\Common;
+
+class Goods extends Common
+{
+
+    protected $name = 'shopro_mplive_goods';
+
+    protected $append = [
+        'audit_status_text',
+        'type_text',
+        'price_type_text',
+    ];
+
+    /**
+     * 类型列表
+     *
+     * @return array
+     */
+    public function typeList()
+    {
+        return [0 => '我的小程序', 1 => '其他小程序'];
+    }
+
+    public function auditStatusList()
+    {
+        return [0 => '未审核', 1 => '审核中', 2 => '审核通过', 3 => '审核失败'];
+    }
+
+    public function priceTypeList()
+    {
+        return [1 => '一口价', 2 => '价格区间', 3 => '折扣价'];
+    }
+
+    public function getPriceTypeTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['price_type'] ?? null);
+
+        $list = $this->priceTypeList();
+        return isset($list[$value]) ? $list[$value] : '';
+    }
+
+    public function getAuditStatusTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['audit_status'] ?? null);
+
+        $list = $this->auditStatusList();
+        return isset($list[$value]) ? $list[$value] : '';
+    }
+}

+ 99 - 0
addons/shopro/application/admin/model/shopro/app/mplive/Room.php

@@ -0,0 +1,99 @@
+<?php
+
+namespace app\admin\model\shopro\app\mplive;
+
+use app\admin\model\shopro\Common;
+
+class Room extends Common
+{
+
+    protected $name = 'shopro_mplive_room';
+
+    protected $append = [
+        'status_text',
+        'type_text',
+    ];
+
+    const ERR_CODE = [
+        -1 => '系统错误',
+        1 => '未创建直播间',
+        1003 => '商品 id 不存在',
+        47001 => '入参格式不符合规范',
+        200002 => '入参错误',
+        300001 => '禁止创建/更新商品 或 禁止编辑/更新房间',
+        300002 => '名称长度不符合规则',
+        300003 => '价格输入不合规(如:现价比原价大、传入价格非数字等)',
+        300004 => '商品名称存在违规违法内容',
+        300005 => '商品图片存在违规违法内容',
+        300006 => ' 图片上传失败(如:mediaID过期)',
+        300007 => '线上小程序版本不存在该链接',
+        300008 => '添加商品失败',
+        300009 => '商品审核撤回失败',
+        300010 => '商品审核状态不对(如:商品审核中)',
+        300011 => '操作非法(API不允许操作非 API 创建的商品)',
+        300012 => '没有提审额度(每天500次提审额度)',
+        300013 => '提审失败',
+        300014 => '审核中,无法删除(非零代表失败)',
+        300017 => '商品未提审',
+        300018 => '商品图片尺寸过大',
+        300021 => '商品添加成功,审核失败',
+        300022 => '此房间号不存在',
+        300023 => '房间状态 拦截(当前房间状态不允许此操作)',
+        300024 => '商品不存在',
+        300025 => '商品审核未通过',
+        300026 => '房间商品数量已经满额',
+        300027 => '导入商品失败',
+        300028 => '房间名称违规',
+        300029 => '主播昵称违规',
+        300030 => '主播微信号不合法',
+        300031 => '直播间封面图不合规',
+        300032 => '直播间分享图违规',
+        300033 => '添加商品超过直播间上限',
+        300034 => '主播微信昵称长度不符合要求',
+        300035 => '主播微信号不存在',
+        300036 => '主播微信号未实名认证',
+        300037 => '购物直播频道封面图不合规',
+        300038 => '未在小程序管理后台配置客服',
+        300039 => '主播副号微信号不合法',
+        300040 => '名称含有非限定字符(含有特殊字符)',
+        300041 => '创建者微信号不合法',
+        300042 => '推流中禁止编辑房间',
+        300043 => '每天只允许一场直播开启关注',
+        300044 => '商品没有讲解视频',
+        300045 => '讲解视频未生成',
+        300046 => '讲解视频生成失败',
+        300047 => '已有商品正在推送,请稍后再试',
+        300048 => '拉取商品列表失败',
+        300049 => '商品推送过程中不允许上下架',
+        300050 => '排序商品列表为空',
+        300051 => '解析 JSON 出错',
+        300052 => '已下架的商品无法推送',
+        300053 => '直播间未添加此商品',
+        500001 => '副号不合规',
+        500002 => '副号未实名',
+        500003 => '已经设置过副号了,不能重复设置',
+        500004 => '不能设置重复的副号',
+        500005 => '副号不能和主号重复',
+        600001 => '用户已被添加为小助手',
+        600002 => '找不到用户',
+        9410000 => '直播间列表为空',
+        9410001 => '获取房间失败',
+        9410002 => '获取商品失败',
+        9410003 => '获取回放失败',
+    ];
+
+    /**
+     * 类型列表
+     *
+     * @return array
+     */
+    public function typeList()
+    {
+        return [0 => '手机直播', 1 => '推流'];
+    }
+
+    public function statusList()
+    {
+        return [101 => '直播中', 102 => '未开始', 103 => '已结束', 104 => '禁播', 105 => '暂停', 106 => '异常', 107 => '已过期'];
+    }
+}

+ 23 - 0
addons/shopro/application/admin/model/shopro/chat/CommonWord.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace app\admin\model\shopro\chat;
+
+use app\admin\model\shopro\Common;
+use app\admin\model\shopro\chat\traits\ChatCommon;
+
+class CommonWord extends Common
+{
+    use ChatCommon;
+
+    protected $name = 'shopro_chat_common_word';
+
+    protected $append = [
+        'status_text',
+        'room_name'
+    ];
+
+    public function scopeRoomId($query, $room_id)
+    {
+        return $query->where('room_id', $room_id);
+    }
+}

+ 57 - 0
addons/shopro/application/admin/model/shopro/chat/CustomerService.php

@@ -0,0 +1,57 @@
+<?php
+
+namespace app\admin\model\shopro\chat;
+
+use app\admin\model\shopro\Common;
+use app\admin\model\shopro\chat\Record;
+use app\admin\model\shopro\chat\CustomerServiceUser;
+use app\admin\model\shopro\chat\traits\ChatCommon;
+
+class CustomerService extends Common
+{
+    use ChatCommon;
+
+    protected $name = 'shopro_chat_customer_service';
+
+    protected $append = [
+        'auth_model',
+        'auth_text',
+        'status_text',
+        'room_name'
+    ];
+
+    // 自动数据类型转换
+    protected $type = [
+        'last_time' => 'timestamp',
+    ];
+
+
+    public function statusList()
+    {
+        return ['offline' => '离线', 'online' => '在线', 'busy' => '忙碌'];
+    }
+
+
+    public function getAuthModelAttr($value, $data) 
+    {
+        return $this->customer_service_user['auth_model'] ?? null;
+    }
+
+    public function getAuthTextAttr($value, $data)
+    {
+        return $this->customer_service_user['auth_text'] ?? null;
+    }
+
+
+    public function customerService()
+    {
+        return $this->morphMany(Record::class, ['sender_identify', 'sender_id'], 'customer_service');
+    }
+
+    public function customerServiceUser()
+    {
+        return $this->HasOne(CustomerServiceUser::class, 'customer_service_id');
+    }
+
+    
+}

+ 62 - 0
addons/shopro/application/admin/model/shopro/chat/CustomerServiceUser.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace app\admin\model\shopro\chat;
+
+use app\admin\model\Admin;
+use app\admin\model\shopro\Common;
+use app\admin\model\shopro\chat\CustomerService;
+use app\admin\model\shopro\user\User as ShopUser;
+
+class CustomerServiceUser extends Common
+{
+    protected $name = 'shopro_chat_customer_service_user';
+
+    
+    protected $append = [
+        'auth_model',
+        'auth_text'
+    ];
+
+    public static $authType = [
+        'admin' => ['name' => '管理员', 'value' => 'admin'],
+        'user' => ['name' => '用户', 'value' => 'user'],
+    ];
+
+    public function scopeAuthAdmin($query, $admin_id)
+    {
+        return $query->where('auth', 'admin')->where('auth_id', $admin_id);
+    }
+
+
+    public function scopeAuthUser($query, $user_id)
+    {
+        return $query->where('auth', 'user')->where('auth_id', $user_id);
+    }
+
+
+    public function getAuthModelAttr($value, $data)
+    {
+        return $this->{$data['auth']};
+    }
+
+    public function getAuthTextAttr($value, $data)
+    {
+        return self::$authType[$data['auth']]['name'] ?? '';
+    }
+
+
+    public function admin()
+    {
+        return $this->belongsTo(Admin::class, 'auth_id');
+    }
+
+    public function customerService()
+    {
+        return $this->belongsTo(CustomerService::class, 'customer_service_id');
+    }
+
+    public function user()
+    {
+        return $this->belongsTo(ShopUser::class, 'auth_id');
+    }
+}

+ 23 - 0
addons/shopro/application/admin/model/shopro/chat/Question.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace app\admin\model\shopro\chat;
+
+use app\admin\model\shopro\Common;
+use app\admin\model\shopro\chat\traits\ChatCommon;
+
+class Question extends Common
+{
+    use ChatCommon;
+
+    protected $name = 'shopro_chat_question';
+
+    protected $append = [
+        'status_text',
+        'room_name'
+    ];
+
+    public function scopeRoomId($query, $room_id)
+    {
+        return $query->where('room_id', $room_id);
+    }
+}

+ 94 - 0
addons/shopro/application/admin/model/shopro/chat/Record.php

@@ -0,0 +1,94 @@
+<?php
+
+namespace app\admin\model\shopro\chat;
+
+use app\admin\model\shopro\Common;
+use app\admin\model\shopro\chat\traits\ChatCommon;
+
+class Record extends Common
+{
+    use ChatCommon;
+
+    protected $name = 'shopro_chat_record';
+
+    protected $append = [
+        'room_name'
+    ];
+
+    // 不格式化创建更新时间
+    protected $dateFormat = false;
+
+    public function scopeCustomer($query)
+    {
+        return $query->where('sender_identify', 'customer');
+    }
+
+
+    public function scopeCustomerService($query)
+    {
+        return $query->where('sender_identify', 'customer_service');
+    }
+
+    public function scopeNoRead($query)
+    {
+        return $query->whereNull('read_time');
+    }
+
+
+    public function setMessageAttr($value, $data)
+    {
+        switch ($data['message_type']) {
+            case 'order':
+            case 'goods':
+                $value = is_array($value) ? json_encode($value) : $value;
+                break;
+            default :
+                $value = $value;
+        }
+
+        return $value;
+    }
+
+
+    /**
+     * 处理消息
+     *
+     * @param string $value
+     * @param array $data
+     * @return string
+     */
+    public function getMessageAttr($value, $data)
+    {
+        switch($data['message_type']) {
+            case 'order':
+            case 'goods':
+                $message = json_decode($value, true);
+
+                break;
+            default :
+                $message = $value;
+                break;
+        }
+
+
+        // if ($data['message_type'] == 'image') {
+        //     $message = Online::cdnurl($value);
+        // } else if (in_array($data['message_type'], ['order', 'goods'])) {
+        //     $messageArr = json_decode($value, true);
+        //     if (isset($messageArr['image']) && $messageArr['image']) {
+        //         $messageArr['image'] = Online::cdnurl($messageArr['image']);
+        //     }
+
+        //     $message = json_encode($messageArr);
+        // } else if ($data['message_type'] == 'text') {
+        //     // 全文匹配图片拼接 cdnurl
+        //     $url = Online::cdnurl('/uploads');
+        //     $message = str_replace("<img src=\"/uploads", "<img style=\"width: 100%;!important\" src=\"" . $url, $value);
+        // } else {
+        //     $message = $value;
+        // }
+
+        return $message;
+    }
+   
+}

+ 22 - 0
addons/shopro/application/admin/model/shopro/chat/ServiceLog.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace app\admin\model\shopro\chat;
+
+use app\admin\model\shopro\Common;
+use app\admin\model\shopro\chat\traits\ChatCommon;
+
+class ServiceLog extends Common
+{
+    use ChatCommon;
+
+    protected $name = 'shopro_chat_service_log';
+
+    protected $append = [
+        'room_name'
+    ];
+
+    public function chatUser() 
+    {
+        return $this->belongsTo(User::class, 'chat_user_id');
+    }
+}

+ 33 - 0
addons/shopro/application/admin/model/shopro/chat/User.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace app\admin\model\shopro\chat;
+
+use app\admin\model\shopro\Common;
+use app\admin\model\shopro\user\User as ShopUser;
+
+
+class User extends Common
+{
+    protected $name = 'shopro_chat_user';
+
+    // 自动数据类型转换
+    protected $type = [
+        'last_time' => 'timestamp',
+    ];
+
+    public function customer()
+    {
+        return $this->morphMany(Record::class, ['sender_identify', 'sender_id'], 'customer');
+    }
+
+    public function user() 
+    {
+        return $this->belongsTo(ShopUser::class, 'auth_id');
+    }
+
+
+    public function customerService() 
+    {
+        return $this->belongsTo(CustomerService::class, 'customer_service_id');
+    }
+}

+ 31 - 0
addons/shopro/application/admin/model/shopro/chat/traits/ChatCommon.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace app\admin\model\shopro\chat\traits;
+
+trait ChatCommon
+{
+
+    /**
+     * 默认房间
+     *
+     * @var array
+     */
+    public static function defaultRooms()
+    {
+        return [
+            ['name' => '总后台', 'value' => 'admin'],
+            // ['name' => '官网', 'value' => 'official'],
+            // ['name' => '商城', 'value' => 'shop']
+        ];
+    }
+
+
+    public function getRoomNameAttr($value, $data)
+    {
+        $value = $value ?: ($data['room_id'] ?? null);
+
+        $list = array_column(self::defaultRooms(), null, 'value');
+        return isset($list[$value]) ? $list[$value]['name'] : $value;
+    }
+
+}

+ 83 - 0
addons/shopro/application/admin/model/shopro/commission/Agent.php

@@ -0,0 +1,83 @@
+<?php
+
+namespace app\admin\model\shopro\commission;
+
+use app\admin\model\shopro\Common;
+use app\admin\model\shopro\user\User;
+
+class Agent extends Common
+{
+    protected $pk = 'user_id';
+
+    protected $name = 'shopro_commission_agent';
+
+    protected $type = [
+        'become_time' => 'timestamp',
+        'apply_info' => 'json',
+        'child_agent_level_1' => 'json',
+        'child_agent_level_all' => 'json',
+    ];
+    protected $append = [
+        'status_text',
+        'pending_reward'
+    ];
+
+    // 分销商状态 AGENT_STATUS
+    const AGENT_STATUS_NORMAL = 'normal';       // 正常 
+    const AGENT_STATUS_PENDING = 'pending';     // 审核中 不分佣、不打款、没有团队信息
+    const AGENT_STATUS_FREEZE = 'freeze';       // 冻结 正常记录分佣、不打款,记录业绩和团队信息 冻结解除后立即打款
+    const AGENT_STATUS_FORBIDDEN = 'forbidden'; // 禁用 不分佣、不记录业绩和团队信息
+    const AGENT_STATUS_NEEDINFO = 'needinfo';   // 需要完善表单资料 临时状态
+    const AGENT_STATUS_REJECT = 'reject';       // 审核驳回, 重新修改   临时状态
+    const AGENT_STATUS_NULL = NULL;             // 未满足成为分销商条件
+
+
+    // 分销商升级锁 UPGRADE_LOCK
+    const UPGRADE_LOCK_OPEN = 1;  // 禁止分销商升级
+    const UPGRADE_LOCK_CLOSE = 0;  // 允许分销商升级
+
+    public function statusList()
+    {
+        return [
+            'normal' => '正常',
+            'pending' => '审核中',
+            'freeze' => '冻结',
+            'forbidden' => '禁用',
+            'reject' => '拒绝'
+        ];
+    }
+
+    /**
+     * 可用分销商
+     */
+    public function scopeAvaliable($query)
+    {
+        return $query->where('status', 'in', [self::AGENT_STATUS_NORMAL, self::AGENT_STATUS_FREEZE]);
+    }
+
+    public function user()
+    {
+        return $this->belongsTo(User::class, 'user_id', 'id')->field('id, nickname, avatar, mobile, total_consume, parent_user_id');
+    }
+
+    public function levelInfo()
+    {
+        return $this->belongsTo(Level::class, 'level', 'level')->field(['level', 'name', 'image', 'commission_rules']);
+    }
+
+    public function getPendingRewardAttr($value, $data)
+    {
+        $amount = Reward::pending()->where('agent_id', $data['user_id'])->sum('commission');
+        return number_format($amount, 2, '.', '');
+    }
+
+    public function levelStatusInfo()
+    {
+        return $this->belongsTo(Level::class, 'level_status', 'level');
+    }
+
+    public function upgradeLevel()
+    {
+        return $this->belongsTo(Level::class, 'level_status', 'level');
+    }
+}

+ 47 - 0
addons/shopro/application/admin/model/shopro/commission/CommissionGoods.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace app\admin\model\shopro\commission;
+
+use app\admin\model\shopro\Common;
+use app\admin\model\shopro\goods\Goods as GoodsModel;
+
+class CommissionGoods extends Common
+{
+    protected $pk = 'goods_id';
+
+    protected $name = 'shopro_commission_goods';
+    
+    protected $autoWriteTimestamp = false;
+
+    // 分销状态
+    const GOODS_COMMISSION_STATUS_OFF = 0;     // 商品不参与分佣
+    const GOODS_COMMISSION_STATUS_ON = 1;     // 商品参与分佣
+    const GOODS_COMMISSION_RULES_DEFAULT = 0;       // 默认分销规则  只看系统分销商等级规则
+    const GOODS_COMMISSION_RULES_SELF = 1;          // 独立分销规则  等级规则对应多种规格规则
+    const GOODS_COMMISSION_RULES_BATCH = 2;         // 批量分销规则  只看保存的各分销商等级规则
+
+    protected $type = [
+        'commission_rules' => 'json'
+    ];
+    protected $append = [
+        'status_text'
+    ];
+
+    public function statusList()
+    {
+        return [
+            0 => '不参与',
+            1 => '参与中'
+        ];
+    }
+
+    public function getCommissionConfigAttr($value, $data)
+    {
+        return json_decode($value, true);
+    }
+
+    public function goods()
+    {
+        return $this->belongsTo(GoodsModel::class, 'goods_id', 'id');
+    }
+}

+ 20 - 0
addons/shopro/application/admin/model/shopro/commission/Level.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace app\admin\model\shopro\commission;
+
+use app\admin\model\shopro\Common;
+
+class Level extends Common
+{
+    protected $pk = 'level';
+
+    protected $name = 'shopro_commission_level';
+    
+    protected $autoWriteTimestamp = false;
+
+    protected $type = [
+        'commission_rules' => 'json',
+        'upgrade_rules' => 'json'
+    ];
+
+}

+ 232 - 0
addons/shopro/application/admin/model/shopro/commission/Log.php

@@ -0,0 +1,232 @@
+<?php
+
+namespace app\admin\model\shopro\commission;
+
+use app\admin\model\shopro\Common;
+use addons\shopro\library\Operator;
+use app\admin\model\shopro\user\User as UserModel;
+
+class Log extends Common
+{
+    protected $name = 'shopro_commission_log';
+
+    protected $updateTime = false;
+
+    protected $append = [
+        'event_text',
+        'oper_type_text'
+    ];
+
+    /**
+     * 添加分销记录
+     * 
+     * @param object      $agentId   分销商ID
+     * @param string      $event     事件类型
+     * @param array       $ext       扩展信息
+     * @param object      $oper      操作人
+     * @param string      $remark    自定义备注
+     * 
+     */
+    public static function add($agentId, $event, $ext = [], $oper = NULL, $remark = '')
+    {
+        if ($remark === '') {
+            switch ($event) {
+                case 'agent':
+                    $remark = self::setAgentEvent($ext);
+                    break;
+                case 'share':
+                    $remark = self::setShareEvent($ext);
+                    break;
+                case 'bind':
+                    $remark = self::setBindEvent($ext);
+                    break;
+                case 'order':
+                    $remark = self::setOrderEvent($ext);
+                    break;
+                case 'reward':
+                    $remark = self::setRewardEvent($ext);
+                    break;
+            }
+        }
+        if ($remark !== '') {
+            $oper = Operator::get($oper);
+            $log = [
+                'agent_id' => $agentId,
+                'event' => $event,
+                'remark' => $remark,
+                'oper_type' => $oper['type'],
+                'oper_id' => $oper['id'],
+                'createtime' => time()
+            ];
+            return self::create($log);
+        }
+        return NULL;
+    }
+
+    public static function setAgentEvent($ext)
+    {
+        switch ($ext['type']) {
+            case 'status':  // 变更状态
+                switch ($ext['value']) {
+                    case Agent::AGENT_STATUS_PENDING:
+                        $remark = "您的资料已提交,等待管理员审核";
+                        break;
+                    case Agent::AGENT_STATUS_FORBIDDEN:
+                        $remark = "您的账户已被禁用";
+                        break;
+                    case Agent::AGENT_STATUS_NORMAL:
+                        $remark = "恭喜您成为分销商";
+                        break;
+                    case Agent::AGENT_STATUS_FREEZE:
+                        $remark = "您的账户已被冻结";
+                        break;
+                    case Agent::AGENT_STATUS_REJECT:
+                        $remark = "您的申请已被拒绝,请重新申请";
+                        break;
+                }
+                break;
+            case 'level': // 变更等级
+                $remark = "您的等级已变更为[{$ext['level']['name']}]";
+                break;
+            case 'apply_info':
+                $remark = '您的分销商资料信息已更新';
+                break;
+        }
+        return $remark ?? "";
+    }
+
+    public static function setShareEvent($ext)
+    {
+        $remark = "您已成为用户[{$ext['user']['nickname']}]的推荐人";
+        return $remark;
+    }
+
+    public static function setBindEvent($ext)
+    {
+        $remark = "";
+        if ($ext['user']) {
+            $remark = "用户[{$ext['user']['nickname']}]已绑定为您的推荐人";
+        }
+        return $remark;
+    }
+
+    public static function setOrderEvent($ext)
+    {
+        switch ($ext['type']) {
+            case 'paid':
+                $goodsName = $ext['item']['goods_title'];
+                if (mb_strlen($goodsName) > 9) {
+                    $goodsName = mb_substr($goodsName, 0, 5) . '...' . mb_substr($goodsName, -3);
+                }
+                if ($ext['order']['self_buy'] == 1) {
+                    $remark = "您购买了{$goodsName},为您新增业绩{$ext['order']['amount']}元, +1分销订单";
+                } else {
+                    $remark = "用户{$ext['buyer']['nickname']}购买了{$goodsName},为您新增业绩{$ext['order']['amount']}元, +1分销订单";
+                }
+                break;
+            case 'refund':
+                $remark = "用户{$ext['buyer']['nickname']}已退款,扣除业绩{$ext['order']['amount']}元, -1分销订单";
+                break;
+            case 'admin':
+                $remark = "扣除业绩{$ext['order']['amount']}元, -1分销订单";
+                break;
+        }
+        return $remark;
+    }
+
+    public static function setRewardEvent($ext)
+    {
+        $actionStr = '';
+        $remark = '';
+        switch ($ext['type']) {
+            case 'paid':
+                $actionStr = '支付成功';
+                break;
+            case 'confirm':
+                $actionStr = '已确认收货';
+                break;
+            case 'finish':
+                $actionStr = '已完成订单';
+                break;
+        }
+        if ($actionStr !== '') {
+            $remark = "用户{$actionStr}, ";
+        }
+        switch ($ext['reward']['status']) {
+            case Reward::COMMISSION_REWARD_STATUS_PENDING:
+                $rewardStatus = '待入账';
+                break;
+            case Reward::COMMISSION_REWARD_STATUS_ACCOUNTED:
+                $rewardStatus = '已入账';
+                break;
+            case Reward::COMMISSION_REWARD_STATUS_BACK:
+                $rewardStatus = '已扣除';
+                break;
+            case Reward::COMMISSION_REWARD_STATUS_CANCEL:
+                $rewardStatus = '已取消';
+                break;
+        }
+        $remark .= "您有{$ext['reward']['commission']}元佣金{$rewardStatus}";
+
+        return $remark;
+    }
+
+
+    public function eventList()
+    {
+        return [
+            'agent' => '分销商',
+            'order' => '订单',
+            'reward' => '佣金',
+            'share' => '推荐',
+            'bind' => '绑定',
+        ];
+    }
+
+
+    public function operTypeList()
+    {
+        return [
+            'user' => '用户',
+            'admin' => '管理员',
+            'system' => '系统',
+        ];
+    }
+
+
+    /**
+     * 事件类型
+     *
+     * @param string $value
+     * @param array $data
+     * @return string
+     */
+    public function getEventTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['event'] ?? null);
+
+        $list = $this->eventList();
+        return isset($list[$value]) ? $list[$value] : '-';
+    }
+
+
+    /**
+     * 操作人类型
+     *
+     * @param string $value
+     * @param array $data
+     * @return string
+     */
+    public function getOperTypeTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['oper_type'] ?? null);
+
+        $list = $this->operTypeList();
+        return isset($list[$value]) ? $list[$value] : '';
+    }
+
+    public function agent()
+    {
+        return $this->belongsTo(UserModel::class, 'agent_id', 'id')->field('id, username, nickname, avatar, gender');
+    }
+}

+ 116 - 0
addons/shopro/application/admin/model/shopro/commission/Order.php

@@ -0,0 +1,116 @@
+<?php
+
+namespace app\admin\model\shopro\commission;
+
+use app\admin\model\shopro\Common;
+use app\admin\model\shopro\user\User as UserModel;
+use app\admin\model\shopro\order\Order as OrderModel;
+use app\admin\model\shopro\order\OrderItem as OrderItemModel;
+
+class Order extends Common
+{
+    const COMMISSION_ORDER_STATUS_NO = 0;  // 不计入
+    const COMMISSION_ORDER_STATUS_YES = 1;  // 已计入
+    const COMMISSION_ORDER_STATUS_CANCEL = -1;  // 已取消
+    const COMMISSION_ORDER_STATUS_BACK = -2;  // 已扣除
+
+    protected $name = 'shopro_commission_order';
+
+    protected $type = [
+        'commission_rules' => 'json',
+        'commission_time' => 'timestamp'
+    ];
+
+    protected $append = [
+        'reward_event_text',
+        'reward_type_text',
+        'commission_order_status_text',
+        'commission_reward_status_text'
+    ];
+
+    public function getRewardEventTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['reward_event'] ?? '');
+        $eventMap = [
+            'paid' => '支付后结算',
+            'confirm' => '收货后结算',
+            'finish' => '订单完成结算',
+            'admin' => '手动结算'
+        ];
+        return isset($eventMap[$value]) ? $eventMap[$value] : '-';
+    }
+
+    public function getRewardTypeTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['reward_type'] ?? '');
+        $eventMap = [
+            'goods_price' => '商品价',
+            'pay_price' => '实际支付价'
+        ];
+        return isset($eventMap[$value]) ? $eventMap[$value] : '-';
+    }
+
+    public function getCommissionOrderStatusTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['commission_order_status'] ?? '');
+        $eventMap = [
+            -2 => '已扣除',
+            -1 => '已取消',
+            0 => '不计入',
+            1 => '已计入'
+        ];
+        return isset($eventMap[$value]) ? $eventMap[$value] : '-';
+    }
+
+    public function getCommissionRewardStatusTextAttr($value, $data)
+    {
+        $value = $value ?: ($data['commission_reward_status'] ?? '');
+        $eventMap = [
+            -2 => '已退回',
+            -1 => '已取消',
+            0 => '未结算',
+            1 => '已结算'
+        ];
+        return isset($eventMap[$value]) ? $eventMap[$value] : '-';
+    }
+
+    public function scopeBack($query)
+    {
+        return $query->where('commission_order_status', self::COMMISSION_ORDER_STATUS_BACK);
+    }
+
+    public function scopeYes($query)
+    {
+        return $query->where('commission_order_status', self::COMMISSION_ORDER_STATUS_YES);
+    }
+
+    public function scopeCancel($query)
+    {
+        return $query->where('commission_order_status', self::COMMISSION_ORDER_STATUS_CANCEL);
+    }
+
+    public function buyer()
+    {
+        return $this->belongsTo(UserModel::class, 'buyer_id', 'id')->field('id, nickname, avatar, mobile');
+    }
+
+    public function agent()
+    {
+        return $this->belongsTo(UserModel::class, 'agent_id', 'id')->field('id, nickname, avatar, mobile');
+    }
+
+    public function order()
+    {
+        return $this->belongsTo(OrderModel::class, 'order_id', 'id');
+    }
+
+    public function orderItem()
+    {
+        return $this->belongsTo(OrderItemModel::class, 'order_item_id', 'id');
+    }
+
+    public function rewards()
+    {
+        return $this->hasMany(Reward::class, 'commission_order_id', 'id');
+    }
+}

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä