三三矩阵分销逻辑及php结合eachart实现树形图展示
数据库设计
参考之前发布的一篇文章:Closure Table无限极目录树关系设计详解 ,此篇中详细讲解如何设计数据库且进行找点及点对点之间的路线。
同时,任何设计方式都有优缺点,那么此方式设计的缺点就在于数据库空间占用太大,仅适合有限层级的模式。
能做到什么?
当我们知道了数据库设计原理后,那么如何快速落点就成为一个难点。其次,就是全部落点成功后如何输出eachart图的数据也是一个难点。本篇主要解决这两处难点,其实理解数据结构的你相信不会很难。
简单使用了ThinkPHP框架部分功能,完全只是为了方便,查询插入基本都是原生sql,自己可随意拆解搭建到自己业务上。
用图说话
我们先看下整体下来的逻辑关系图,这里需要注意的是所有点都是跟10001有关系的,然后每个人下方最多有三个人。每个落点的位置是存在”跳点“的,这样会存在一定程度的公平性。
下面附上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数据啦
进阶,将各个节点替换为用户的图片,并在鼠标滑到对应点的时候展示此点的详细信息
ok,现在来玩点高级点的吧,这部分参考下eachart的api配置项,也很容易达到的。还是先看效果图说话
首先,我们继续上一步整合数组的过程,我们需要在加点元素,比如说用户的头像、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>
结束语
到此为止所有工作就全部都完成啦,整个工作下来只要理解了数据结构那么就很容易了,希望对各位有所帮助。
评论