三三矩阵分销逻辑及php结合eachart实现树形图展示

流氓凡 技术分享 2021-04-11 460 0

数据库设计

参考之前发布的一篇文章:Closure Table无限极目录树关系设计详解 ,此篇中详细讲解如何设计数据库且进行找点及点对点之间的路线。

同时,任何设计方式都有优缺点,那么此方式设计的缺点就在于数据库空间占用太大,仅适合有限层级的模式。

能做到什么?

当我们知道了数据库设计原理后,那么如何快速落点就成为一个难点。其次,就是全部落点成功后如何输出eachart图的数据也是一个难点。本篇主要解决这两处难点,其实理解数据结构的你相信不会很难。

简单使用了ThinkPHP框架部分功能,完全只是为了方便,查询插入基本都是原生sql,自己可随意拆解搭建到自己业务上。

用图说话

我们先看下整体下来的逻辑关系图,这里需要注意的是所有点都是跟10001有关系的,然后每个人下方最多有三个人。每个落点的位置是存在”跳点“的,这样会存在一定程度的公平性。


image.png

下面附上php实现落点的代码部分示例,因为有“跳点”存在,因此我们需要用到冒泡排序。

大致的原理是,从上往下逐层扫描,如果空缺那么就在此层逐个区域扫描,确认区域确认后,在区域内逐点扫描最后实现落点。(非递归)

区域:每个点下方有三个点,我称之为区域命名为A、B、C三个区域(不会实际显示,自己知道就好了)

    // 落点测试api接口 上级ID、要落点的ID
    public function fall_test($referee_id, $user_id)
    {
        $model = new PromoteAccessModel;
        $tableName = $model->getTable();
        if (!$model->where(['ancestor' => $referee_id])->find()) {
            $sql = "INSERT INTO {$tableName}(ancestor,descendant,distance,uniacid) (SELECT ancestor,{$referee_id},distance+1 FROM {$tableName} WHERE descendant=0)";
            Db::query($sql);
            $sql = "INSERT INTO {$tableName}(ancestor,descendant,distance,uniacid) VALUES({$referee_id},{$referee_id},0)";
            Db::query($sql);
        }
        $level = 1; // 应该在第几排落点
        $endLevel = $model->where(['ancestor' => $referee_id, 'distance' => ['>', 0]])->max('distance');
        for ($i = 0; $i < $endLevel; $i++) {
            $count = $model->where(['ancestor' => $referee_id, 'distance' => $level])->count();
            if ($count >= pow(3, $level)) {
                $level++;
            }
        }
        // 计算上一层排序
        $upList = $model->where(['ancestor' => $referee_id, 'distance' => $level - 1])->select();
        $sortArr = [];
        foreach ($upList as $value) {
            $res = Db::query("SELECT ancestor FROM {$tableName}  WHERE descendant={$value['descendant']} ORDER BY distance DESC");
            $sortArr[] = array_column($res, 'ancestor');
        }
        $sortArr = bubbleSort($sortArr); // 计算排序后的数组(路径)
        // 计算分区总人数
        $areaTotal = pow(3, ($level - 1));
        $arr = [];
        $list = $model->where(['ancestor' => $referee_id, 'distance' => $level])->select();
        foreach ($list as $value) {
            $res = Db::query("SELECT ancestor FROM {$tableName}  WHERE descendant={$value['descendant']} ORDER BY distance DESC");
            $arr[] = array_column($res, 'ancestor');
        }
        if ($arr) {
            $arr = bubbleSort($arr);
            $i = $this->CalculateTheGroupValue($arr);
            $groupArray = $this->dataGroup($arr, $i);
        } else {
            $groupArray = [];
        }
        if (count($groupArray) < $areaTotal) {
            // 放在A区,移除数组最后一个元素
            $aa = $this->removeEndArr($arr);
            $path = $this->diff($sortArr, $aa);
            $fall_id = end($path);
        } else {
            $fall_key = [];
            foreach ($groupArray as $key => $value) {
                if (!isset($value[0])) {
                    // 检查A区是否满员
                    $fall_key = $value[0];
                    break;
                }
            }
            if (!$fall_key) {
                foreach ($groupArray as $key => $value) {
                    if (!isset($value[1])) {
                        // A区全满检查B区是否满员
                        $fall_key = $value;
                        break;
                    }
                }
            }
            if (!$fall_key) {
                // b区全满检查c区是否满员
                foreach ($groupArray as $key => $value) {
                    if (!isset($value[2])) {
                        $fall_key = $value;
                        break;
                    }
                }
            }
            $fall_id = $fall_key;
        }
        if (is_array($fall_id)) {
            $fall_id = $fall_id[0];
            array_pop($fall_id);
            $fall_id = end($fall_id);
        }
        return $model->insertInto($fall_id, $user_id, $referee_id);
    }
    
    
        /**
     * 得到向量i
     * @param $arr
     * @return false|int|string
     */
    protected function CalculateTheGroupValue($arr)
    {
        $arr = $arr[0];
        $value = $arr[count($arr) - 2];
        return array_search($value, $arr);
    }
    
    /**
     * 计算二维数组差集
     * @param $sortArr
     * @param $arr
     * @return array|mixed
     */
    protected function diff($sortArr, $arr)
    {
        foreach ($sortArr as $key => $value) {
            if (!in_array($value, $arr)) {
                return $value;
            }
        }
        return [];
    }
    
    /**
     * 移除二维数组最后一个元素
     * @param $arr
     * @return mixed
     */
    protected function removeEndArr(&$arr)
    {
        foreach ($arr as &$value) {
            array_pop($value);
        }
        return $arr;
    }
    
    /**
     * 二维数组按指定key分组
     * @param array $dataArr
     * @param string $keyStr
     * @return array
     */
    protected function dataGroup(array $dataArr, string $keyStr): array
    {
        $newArr = [];
        foreach ($dataArr as $k => $val) {
            $newArr[$val[$keyStr]][] = $val;
        }
        return $newArr;
    }
    
     /**
     * 冒泡排序(公共方法)
     * @param $arr
     * @return mixed
     */
    function bubbleSort($arr)
    {
        for ($i = 0; $i < count($arr); $i++) {
            for ($j = 0; $j <count($arr) - $i - 1; $j++) {
                if ($arr[$j] > $arr[$j + 1]) {
                    $tmp = $arr[$j];
                    $arr[$j] = $arr[$j + 1];
                    $arr[$j + 1] = $tmp;
                }
            }
        }
        return $arr;
    }
    
    /**
     * 插入树(新增节点)
     * @param $referee_id
     * @param $user_id
     * @return bool
     * @throws BaseException
     */
    public function insertInto($fall_id, $user_id)
    {
        // 启动事务
        Db::startTrans();
        try {
            // 查询数据是否被插入过
            $result = $this->where(['descendant' => $user_id, 'distance' => 1])->find();
            if ($result) {
                return $result['ancestor'];
            }
            $tableName = $this->getTable();
            $sql = "INSERT INTO {$tableName}(ancestor,descendant,distance,uniacid) (SELECT ancestor,{$user_id},distance+1 FROM {$tableName} WHERE descendant={$fall_id})";
            Db::query($sql);
            $sql = "INSERT INTO {$tableName}(ancestor,descendant,distance,uniacid) VALUES({$user_id},{$user_id},0)";
            Db::query($sql);
            // 提交事务
            Db::commit();
            return $fall_id;
        } catch (\Exception $e) {
            // 回滚事务
            Db::rollback();
            throw new BaseException(['msg' => $e->getMessage()]);
        }
    }


这时候我们就继续访问接口进行插入点测试就好了。那么如何将点取出来就是一个很恶心的事情,毕竟这种数据结构不是很好理解。那么接下来我们换种方式,我们将其表数据变成我们常规的分类数据结构,此时的数组应该是这样的:

$arr=array(
    array('id'=>1,'pid'=>0,'name'=>'浙江'),
    array('id'=>10,'pid'=>1,'name'=>'宁波'),
    array('id'=>13,'pid'=>1,'name'=>'金华'),
    array('id'=>4,'pid'=>0,'name'=>'上海'),
    array('id'=>5,'pid'=>4,'name'=>'闵行'),
    array('id'=>6,'pid'=>10,'name'=>'宁海'),
);

这样的数组平常都会用到的对吧,那么我们先将数组转成类似的数组:

    /**
     * 查询所有根下子节点
     * @return \think\response\Json
     * @throws \think\db\exception\BindParamException
     * @throws \think\exception\PDOException
     */
    public function children()
    {
        $model = new PromoteAccessModel;
        $tableName = $model->getTable();
        $list = $model->query("SELECT descendant FROM {$tableName} WHERE ancestor=0 AND distance>0");
        $data = [];
        foreach ($list as $value) {
            $pid = $model->where(['descendant' => $value['descendant'], 'distance' => 1])->value('ancestor');
            $data[] = ['id' => $value['descendant'], 'pid' => $pid, 'name' => $value['descendant']];
        }
        var_dump($data);
    }

随后我们将整理的数据放入递归方法,整合成eachart需要的数据格式:

    
    /**
     * 递归生成树状图
     * @param $a
     * @param $pid
     * @return array
     */
    public function get_attr($a, $pid)
    {
        $tree = array();
        foreach ($a as $v) {
            if ($v['pid'] == $pid) {
                $v['children'] = $this->get_attr($a, $v['id']);
                if ($v['children'] == null) {
                    unset($v['children']);
                    unset($v['id']);
                    unset($v['pid']);
                } else {
                    unset($v['id']);
                    unset($v['pid']);
                }
                $tree[] = $v;
            }
        }
        return $tree;
    }
    
    $list = $this->get_attr($data,0);
    return json($list[0]);

这里我们会得到一个很大的json数据啦

image.png

进阶,将各个节点替换为用户的图片,并在鼠标滑到对应点的时候展示此点的详细信息

ok,现在来玩点高级点的吧,这部分参考下eachart的api配置项,也很容易达到的。还是先看效果图说话

image.png

首先,我们继续上一步整合数组的过程,我们需要在加点元素,比如说用户的头像、id、昵称,示例如下:

$arr=array(
    array('id'=>1,'pid'=>0,'name'=>'浙江','user_id'=>1,'nickName'=>'昵称a','pic'=>'https://baidu.com/aaa.png'),
    array('id'=>10,'pid'=>1,'name'=>'宁波','user_id'=>10,'nickName'=>'昵称b','pic'=>'https://baidu.com/aaa.png'),
    array('id'=>13,'pid'=>1,'name'=>'金华','user_id'=>13,'nickName'=>'昵称c','pic'=>'https://baidu.com/aaa.png'),
    array('id'=>4,'pid'=>0,'name'=>'上海','user_id'=>4,'nickName'=>'昵称d','pic'=>'https://baidu.com/aaa.png'),
    array('id'=>5,'pid'=>4,'name'=>'闵行','user_id'=>5,'nickName'=>'昵称e','pic'=>'https://baidu.com/aaa.png'),
    array('id'=>6,'pid'=>10,'name'=>'宁海','user_id'=>6,'nickName'=>'昵称f','pic'=>'https://baidu.com/aaa.png'),
);

然后我们改变下eachart的tree示例代码,增加一处图像节点还有一处鼠标滑动触发回调内容,完整代码如下(部分参数值可能不对,仅做示例参考):

<div id="container" style="height: 100vh"></div>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>

<script type="text/javascript">
    var dom = document.getElementById("container");
    var myChart = echarts.init(dom);
    var app = {};
    var option;
    var url = "<?= siteUrl('statistics.promote/children') ?>";
    myChart.showLoading();
    $.get(url, function (data) {
        myChart.hideLoading();

        myChart.setOption(option = {
            tooltip: {
                trigger: 'item',
                triggerOn: 'mousemove',
                enterable:true,//鼠标是否可进入提示框浮层中
                formatter:formatterHover,//修改鼠标悬停显示的内容
            },
            series:[
                {
                    type: 'tree',

                    data: [data],

                    left: '2%',
                    right: '2%',
                    top: '8%',
                    bottom: '20%',
                    symbolSize:30,
                    symbol: 'emptyCircle',

                    orient: 'vertical',

                    expandAndCollapse: true,
                    initialTreeDepth:100, //默认:2,树图初始展开的层级(深度)。根节点是第 0 层,然后是第 1 层、第 2 层,... ,直到叶子节点
                    label: {
                        position: 'top',
                        verticalAlign: 'middle',
                        align: 'center',
                        fontSize: 10,
                        fontWeight:500,
                    },

                    itemStyle:{
                        color: {
                            type: 'linear',
                            x: 0,
                            y: 0,
                            x2: 0,
                            y2: 1,
                            colorStops: [{
                                offset: 0, color: 'red' // 0% 处的颜色
                            }, {
                                offset: 1, color: 'blue' // 100% 处的颜色
                            }],
                            global: false // 缺省为 false
                        },
                        shadowColor: 'rgba(0, 0, 0, 0.5)',
                        shadowBlur: 5,
                    },

                    leaves: {
                        label: {
                            position: 'bottom',
                            verticalAlign: 'middle',
                            align: 'center',
                            fontSize: 10,
                            fontWeight:500,
                        }
                    },

                    animationDurationUpdate: 750
                }
            ]
        });
    });

    /**
     * 鼠标悬停时显示详情
     */
    function formatterHover(params){
        var imgPath = params.data.symbol;
        var imgPathSrc = imgPath.split("image://")[1];
        return "<img src='"+imgPathSrc+" ' width='30px' height='30px'>" + '<span style="position: relative;top: -10px;padding: 0 5px;">用户昵称:'+ params.data.nickName+'</span>'+ '<br>'
            + '<span style="padding-left:5px;height:30px;line-height:30px;display: inline-block;">用户ID:'+ params.data.user_id+'</span>'+ '<br>'
            + '<span style="padding-left:5px;height:30px;line-height:30px;display: inline-block;">用户等级:'+ params.data.gradeName+'</span>'+ '<br>'
            + '<span style="padding-left:5px;height:30px;line-height:30px;display: inline-block;">直推上级ID:'+ params.data.referee_id+'</span>';
    }
    if (option && typeof option === 'object') {
        /**
         * 解决echarts图片首次加载不显示的问题
         */
        setTimeout(function(){
            $(myChart).resize();
        },200)
        /**
         * 解决点击父节点合并或展开后子节点图片不显示的问题
         */
        $(window).on('click',function(){
            $(myChart).resize();
        })
        myChart.setOption(option);
    }

</script>

结束语

到此为止所有工作就全部都完成啦,整个工作下来只要理解了数据结构那么就很容易了,希望对各位有所帮助。

评论