column('color', 'num'); $history = $this ->field('expect,num1,num2,num3,num4,num5,num6,num7,openTime') ->order('openTime', 'desc') ->limit($periods) ->select(); if (empty($history)) { return ['expects' => [], 'data' => [], 'colorMap' => []]; } $expects = []; $data = []; foreach ($history as $row) { $expects[] = (string)$row['expect']; if ($type === 'special') { $data[] = ['num7' => (int)$row['num7']]; } else { $row_data = []; for ($i = 1; $i <= 7; $i++) { $row_data['num' . $i] = (int)$row['num' . $i]; } $data[] = $row_data; } } // 反转数组,使最远的数据在左边,最近的数据在右边(从左往右,从远到近) $expects = array_reverse($expects); $data = array_reverse($data); return [ 'expects' => $expects, 'data' => $data, 'colorMap' => $colorMap ]; } /** * 获取冷热号码 * @param int $periods 查询最近多少期 * @param string $type 查询类型 all=全部号码 special=仅特码 * @return array {hot: [], cold: [], all: []} */ public function getHotColdNumbers($periods = 30, $type = 'all') { $num_model = new Num(); $colorMap = $num_model->column('color', 'num'); $animalMap = $num_model->column('animal', 'num'); $history = $this ->field('expect,num1,num2,num3,num4,num5,num6,num7,openTime') ->order('openTime', 'desc') ->limit($periods) ->select(); if (empty($history)) { return ['hot' => [], 'cold' => [], 'all' => []]; } $fields = ($type === 'special') ? ['num7'] : ['num1', 'num2', 'num3', 'num4', 'num5', 'num6', 'num7']; // 统计每个号码的出现次数 $count = array_fill(1, 49, 0); $totalAppearances = 0; foreach ($history as $row) { foreach ($fields as $field) { $num = (int)$row[$field]; if ($num >= 1 && $num <= 49) { $count[$num]++; $totalAppearances++; } } } $all = []; for ($num = 1; $num <= 49; $num++) { $percent = $totalAppearances > 0 ? round($count[$num] / $totalAppearances * 100, 1) : 0; $all[] = [ 'num' => $num, 'count' => $count[$num], 'percent' => $percent, 'color' => $colorMap[$num] ?? '—', 'animal' => $animalMap[$num] ?? '—' ]; } // 按出现次数降序排序 $sorted = $all; usort($sorted, function ($a, $b) { return $b['count'] - $a['count']; }); // 热号: top 10, 冷号: bottom 10 $hot = array_slice($sorted, 0, 10); $cold = array_slice($sorted, -10); $cold = array_reverse($cold); return ['hot' => $hot, 'cold' => $cold, 'all' => $all]; } /** * 计算遗漏号码 * @param int $periods 查询最近多少期 * @param string $type 查询类型 all=全部号码 special=仅特码 * @return array [{num: int, omit: int, color: string}, ...] */ public function getMissingNumbers($periods = 10, $type = 'all') { // 查询最近 $periods 期开奖数据 if ($type === 'special') { $history = $this ->field('expect,num7') ->order('openTime', 'desc') ->limit($periods) ->select(); } else { $history = $this ->field('expect,num1,num2,num3,num4,num5,num6,num7') ->order('openTime', 'desc') ->limit($periods) ->select(); } // 收集最近 $periods 期出现过的号码 $appeared = []; foreach ($history as $row) { if ($type === 'special') { $fields = ['num7']; } else { $fields = ['num1', 'num2', 'num3', 'num4', 'num5', 'num6', 'num7']; } foreach ($fields as $field) { if ($row[$field] !== null && $row[$field] !== '') { $appeared[(int)$row[$field]] = true; } } } // 获取遗漏号码(1-49中未出现的) $missing = []; for ($num = 1; $num <= 49; $num++) { if (!isset($appeared[$num])) { $missing[] = $num; } } // 查询更多历史数据用于计算遗漏期数 if ($type === 'special') { $allHistory = $this ->field('num7') ->order('openTime', 'desc') ->limit(500) ->select(); } else { $allHistory = $this ->field('num1,num2,num3,num4,num5,num6,num7') ->order('openTime', 'desc') ->limit(500) ->select(); } // 查询波色映射 $num_model = new Num(); $colorMap = $num_model->column('color', 'num'); // 计算遗漏期数并组装结果 $result = []; foreach ($missing as $num) { $omitCount = $this->calcOmitCount($num, $allHistory, $type); $result[] = [ 'num' => $num, 'omit' => $omitCount, 'color' => $colorMap[$num] ?? '—' ]; } // 按遗漏期数降序排序 usort($result, function ($a, $b) { return $b['omit'] - $a['omit']; }); return $result; } /** * 计算某个号码的遗漏期数 * @param int $num 号码 * @param array $allHistory 历史数据(已按openTime DESC排序) * @param string $type 查询类型 all=全部号码 special=仅特码 * @return int 遗漏期数 */ private function calcOmitCount($num, $allHistory, $type = 'all') { foreach ($allHistory as $idx => $row) { if ($type === 'special') { if ((int)$row['num7'] === $num) { return $idx; } } else { for ($i = 1; $i <= 7; $i++) { if ((int)$row['num' . $i] === $num) { return $idx; } } } } return count($allHistory); } /** * 波色分析 */ public function getColorWaveAnalysis($periods = 30, $type = 'all') { $num_model = new Num(); $colorMap = $num_model->column('color', 'num'); $history = $this->field('expect,num1,num2,num3,num4,num5,num6,num7')->order('openTime', 'desc')->limit($periods)->select(); if (empty($history)) return ['red' => 0, 'blue' => 0, 'green' => 0, 'red_pct' => 0, 'blue_pct' => 0, 'green_pct' => 0, 'details' => []]; $fields = ($type === 'special') ? ['num7'] : ['num1','num2','num3','num4','num5','num6','num7']; $colors = ['红' => 0, '蓝' => 0, '绿' => 0]; $total = 0; foreach ($history as $row) { foreach ($fields as $f) { $num = (int)$row[$f]; $color = $colorMap[$num] ?? ''; if (strpos($color, '红') !== false) { $colors['红']++; $total++; } elseif (strpos($color, '蓝') !== false) { $colors['蓝']++; $total++; } elseif (strpos($color, '绿') !== false) { $colors['绿']++; $total++; } } } return [ 'red' => $colors['红'], 'blue' => $colors['蓝'], 'green' => $colors['绿'], 'red_pct' => $total ? round($colors['红']/$total*100,1) : 0, 'blue_pct' => $total ? round($colors['蓝']/$total*100,1) : 0, 'green_pct' => $total ? round($colors['绿']/$total*100,1) : 0, 'total' => $total ]; } /** * 生肖分析 */ public function getZodiacAnalysis($periods = 30, $type = 'all') { $num_model = new Num(); $animalMap = $num_model->column('animal', 'num'); $colorMap = $num_model->column('color', 'num'); $history = $this->field('expect,num1,num2,num3,num4,num5,num6,num7')->order('openTime', 'desc')->limit($periods)->select(); if (empty($history)) return ['list' => []]; $fields = ($type === 'special') ? ['num7'] : ['num1','num2','num3','num4','num5','num6','num7']; $counts = []; foreach ($history as $row) { foreach ($fields as $f) { $num = (int)$row[$f]; $animal = $animalMap[$num] ?? '未知'; if (!isset($counts[$animal])) $counts[$animal] = ['animal' => $animal, 'count' => 0, 'color' => $colorMap[$num] ?? '—']; $counts[$animal]['count']++; } } $list = array_values($counts); usort($list, function ($a, $b) { return $b['count'] - $a['count']; }); $total = array_sum(array_column($list, 'count')); foreach ($list as &$item) { $item['percent'] = $total ? round($item['count']/$total*100, 1) : 0; } return ['list' => $list]; } /** * 奇偶分析 */ public function getOddEvenAnalysis($periods = 30, $type = 'all') { $history = $this->field('expect,num1,num2,num3,num4,num5,num6,num7')->order('openTime', 'desc')->limit($periods)->select(); if (empty($history)) return ['odd' => 0, 'even' => 0, 'odd_pct' => 0, 'even_pct' => 0, 'per_period' => []]; $fields = ($type === 'special') ? ['num7'] : ['num1','num2','num3','num4','num5','num6','num7']; $odd = 0; $even = 0; $perPeriod = []; foreach ($history as $row) { $p_odd = 0; $p_even = 0; foreach ($fields as $f) { $num = (int)$row[$f]; if ($num % 2 == 0) { $even++; $p_even++; } else { $odd++; $p_odd++; } } $perPeriod[] = ['expect' => $row['expect'], 'odd' => $p_odd, 'even' => $p_even]; } $total = $odd + $even; return [ 'odd' => $odd, 'even' => $even, 'odd_pct' => $total ? round($odd/$total*100, 1) : 0, 'even_pct' => $total ? round($even/$total*100, 1) : 0, 'per_period' => $perPeriod ]; } /** * 大小分析(1-24为小,25-49为大) */ public function getBigSmallAnalysis($periods = 30, $type = 'all') { $history = $this->field('expect,num1,num2,num3,num4,num5,num6,num7')->order('openTime', 'desc')->limit($periods)->select(); if (empty($history)) return ['big' => 0, 'small' => 0, 'big_pct' => 0, 'small_pct' => 0, 'per_period' => []]; $fields = ($type === 'special') ? ['num7'] : ['num1','num2','num3','num4','num5','num6','num7']; $big = 0; $small = 0; $perPeriod = []; foreach ($history as $row) { $p_big = 0; $p_small = 0; foreach ($fields as $f) { $num = (int)$row[$f]; if ($num >= 25) { $big++; $p_big++; } else { $small++; $p_small++; } } $perPeriod[] = ['expect' => $row['expect'], 'big' => $p_big, 'small' => $p_small]; } $total = $big + $small; return [ 'big' => $big, 'small' => $small, 'big_pct' => $total ? round($big/$total*100, 1) : 0, 'small_pct' => $total ? round($small/$total*100, 1) : 0, 'per_period' => $perPeriod ]; } /** * 特码走势 */ public function getSpecialTrend($periods = 30) { $num_model = new Num(); $colorMap = $num_model->column('color', 'num'); // 先取最近 $periods 条数据 $history = $this->field('expect,num7,openTime')->order('openTime', 'desc')->limit($periods)->select(); if (empty($history)) return ['expects' => [], 'specials' => [], 'colors' => []]; // 反转,使数据从左到右为从远到近 $history = array_reverse($history); $expects = []; $specials = []; $colors = []; foreach ($history as $row) { $expects[] = (string)$row['expect']; $num = (int)$row['num7']; $specials[] = $num; $colors[] = $colorMap[$num] ?? ''; } return ['expects' => $expects, 'specials' => $specials, 'colors' => $colors]; } /** * 连号分析 */ public function getConsecutiveNumbers($periods = 30) { $history = $this->field('expect,num1,num2,num3,num4,num5,num6,num7')->order('openTime', 'desc')->limit($periods)->select(); if (empty($history)) return ['pairs' => [], 'triples' => []]; $pairCount = []; $tripleCount = []; foreach ($history as $row) { $nums = []; for ($i = 1; $i <= 7; $i++) $nums[] = (int)$row['num' . $i]; sort($nums); // 找连号对 for ($i = 0; $i < count($nums) - 1; $i++) { if ($nums[$i + 1] - $nums[$i] === 1) { $pair = $nums[$i] . '-' . $nums[$i + 1]; $pairCount[$pair] = isset($pairCount[$pair]) ? $pairCount[$pair] + 1 : 1; } } // 找连号三连 for ($i = 0; $i < count($nums) - 2; $i++) { if ($nums[$i + 1] - $nums[$i] === 1 && $nums[$i + 2] - $nums[$i + 1] === 1) { $triple = $nums[$i] . '-' . $nums[$i + 1] . '-' . $nums[$i + 2]; $tripleCount[$triple] = isset($tripleCount[$triple]) ? $tripleCount[$triple] + 1 : 1; } } } arsort($pairCount); arsort($tripleCount); return ['pairs' => $pairCount, 'triples' => $tripleCount]; } /** * 尾数分析 */ public function getTailNumbers($periods = 30, $type = 'all') { $num_model = new Num(); $colorMap = $num_model->column('color', 'num'); $animalMap = $num_model->column('animal', 'num'); $history = $this->field('expect,num1,num2,num3,num4,num5,num6,num7')->order('openTime', 'desc')->limit($periods)->select(); if (empty($history)) return ['tails' => [], 'all' => []]; $fields = ($type === 'special') ? ['num7'] : ['num1','num2','num3','num4','num5','num6','num7']; $tailCount = array_fill(0, 10, 0); $all = []; foreach ($history as $row) { foreach ($fields as $f) { $num = (int)$row[$f]; $tail = $num % 10; $tailCount[$tail]++; } } $total = array_sum($tailCount); for ($t = 0; $t <= 9; $t++) { $all[] = ['tail' => $t, 'count' => $tailCount[$t], 'percent' => $total ? round($tailCount[$t]/$total*100, 1) : 0]; } usort($all, function ($a, $b) { return $b['count'] - $a['count']; }); return ['all' => $all]; } /** * 综合统计面板 */ public function getDashboardData($periods = 30, $type = 'all') { return [ 'hotcold' => $this->getHotColdNumbers($periods, 'special'), 'colorwave' => $this->getColorWaveAnalysis($periods, 'special'), 'zodiac' => $this->getZodiacAnalysis($periods, 'special'), 'oddeven' => $this->getOddEvenAnalysis($periods, 'special'), 'bigsmall' => $this->getBigSmallAnalysis($periods, 'special'), 'special' => $this->getSpecialTrend($periods), 'tailnumbers' => $this->getTailNumbers($periods, 'special'), 'heatmap' => $this->getSpecialHeatmap($periods), 'zonetransition' => $this->getZoneTransition($periods), 'colorwavetransition' => $this->getColorWaveTransition($periods) ]; } /** * 批量查询所有期号特码相对于前N期的冷热状态 * @param int $lookback 向前推算期数 * @param int $limit 查询总期数(从最新往前取) * @return array [{expect, specialNum, count, avgCount, status, rank, totalPeriods}, ...] */ public function getSpecialHotColdList($lookback = 30, $limit = 100) { // 查询最近 $limit 期数据,按时间倒序(最新在前) $history = $this ->field('expect,num7,openTime') ->order('openTime', 'desc') ->limit($limit) ->select(); if (empty($history)) { return []; } // 查询更多历史数据用于统计 $allHistory = $this ->field('expect,num7,openTime') ->order('openTime', 'desc') ->limit($limit + 200) ->select(); // 按openTime排序(确保顺序) $historySorted = []; foreach ($history as $row) { $historySorted[] = $row; } $allHistorySorted = []; foreach ($allHistory as $row) { $allHistorySorted[] = $row; } $result = []; // 对每一期,计算它前面lookback期的冷热状态 for ($i = 0; $i < count($historySorted); $i++) { $row = $historySorted[$i]; $specialNum = (int)$row['num7']; // 找到该期在全量数据中的位置 $targetTime = $row['openTime']; $count = array_fill(1, 49, 0); $periodCount = 0; // 往前统计lookback期(跳过该期本身) for ($j = 0; $j < count($allHistorySorted); $j++) { $checkRow = $allHistorySorted[$j]; $checkTime = $checkRow['openTime']; // 只统计比该期更早的数据 if ($checkTime >= $targetTime) { continue; } if ($periodCount >= $lookback) { break; } $num = (int)$checkRow['num7']; if ($num >= 1 && $num <= 49) { $count[$num]++; } $periodCount++; } if ($periodCount === 0) { $result[] = [ 'expect' => (string)$row['expect'], 'specialNum' => $specialNum, 'count' => 0, 'avgCount' => 0, 'status' => 'unknown', 'rank' => 0 ]; continue; } $targetCount = $count[$specialNum]; $avgCount = $periodCount / 49; $status = 'normal'; if ($avgCount > 0) { if ($targetCount > $avgCount * 1.5) { $status = 'hot'; } elseif ($targetCount < $avgCount * 0.5) { $status = 'cold'; } } // 计算排名 $sorted = []; for ($num = 1; $num <= 49; $num++) { $sorted[] = ['num' => $num, 'count' => $count[$num]]; } usort($sorted, function ($a, $b) { return $b['count'] - $a['count']; }); $rank = 0; foreach ($sorted as $idx => $item) { if ($item['num'] === $specialNum) { $rank = $idx + 1; break; } } $result[] = [ 'expect' => (string)$row['expect'], 'specialNum' => $specialNum, 'count' => $targetCount, 'avgCount' => round($avgCount, 2), 'status' => $status, 'rank' => $rank ]; } // 基于最新一期(result[0])的lookback窗口,计算当前所有49个号码的冷热状态 $current = $this->_computeCurrentHotCold($allHistorySorted, $historySorted[0], $lookback); return [ 'list' => $result, 'current' => $current ]; } /** * 基于指定期的lookback窗口,计算所有49个号码的冷热状态 * @param array $allHistorySorted 全量历史数据 * @param mixed $targetRow 目标期数据 * @param int $lookback 向前期数 * @return array {hot: [{num, count}], cold: [{num, count}], warm: [{num, count}]} */ private function _computeCurrentHotCold($allHistorySorted, $targetRow, $lookback) { $targetTime = $targetRow['openTime']; $count = array_fill(1, 49, 0); $periodCount = 0; for ($j = 0; $j < count($allHistorySorted); $j++) { $checkRow = $allHistorySorted[$j]; $checkTime = $checkRow['openTime']; if ($checkTime >= $targetTime) { continue; } if ($periodCount >= $lookback) { break; } $num = (int)$checkRow['num7']; if ($num >= 1 && $num <= 49) { $count[$num]++; } $periodCount++; } $avgCount = $periodCount > 0 ? $periodCount / 49 : 0; $hot = []; $cold = []; $warm = []; for ($num = 1; $num <= 49; $num++) { $status = 'warm'; if ($avgCount > 0) { if ($count[$num] > $avgCount * 1.5) { $status = 'hot'; } elseif ($count[$num] < $avgCount * 0.5) { $status = 'cold'; } } $item = ['num' => $num, 'count' => $count[$num]]; if ($status === 'hot') { $hot[] = $item; } elseif ($status === 'cold') { $cold[] = $item; } else { $warm[] = $item; } } // 热号按次数降序,冷号按次数升序 usort($hot, function ($a, $b) { return $b['count'] - $a['count']; }); usort($cold, function ($a, $b) { return $a['count'] - $b['count']; }); usort($warm, function ($a, $b) { return $b['count'] - $a['count']; }); return ['hot' => $hot, 'cold' => $cold, 'warm' => $warm]; } /** * 区域转移概率统计 * 将1-49分为5个区域,统计特码从一个区域转移到另一个区域的概率 * 区域1: 1-10, 区域2: 11-20, 区域3: 21-30, 区域4: 31-40, 区域5: 41-49 * @param int $periods 查询最近多少期 * @return array {zones: [], matrix: [], probabilities: [], total_transitions: int} */ public function getZoneTransition($periods = 100) { $history = $this ->field('expect,num7,openTime') ->order('openTime', 'desc') ->limit($periods) ->select(); if (empty($history) || count($history) < 2) { return ['zones' => ['1-10','11-20','21-30','31-40','41-49'], 'matrix' => [], 'probabilities' => [], 'total_transitions' => 0]; } // 反转为升序,从旧到新 $history = array_reverse($history); $zoneLabels = ['1-10', '11-20', '21-30', '31-40', '41-49']; $matrix = array_fill(0, 5, array_fill(0, 5, 0)); $rowTotals = array_fill(0, 5, 0); $getZone = function ($num) { if ($num <= 10) return 0; if ($num <= 20) return 1; if ($num <= 30) return 2; if ($num <= 40) return 3; return 4; }; $totalTransitions = 0; for ($i = 0; $i < count($history) - 1; $i++) { $currentNum = (int)$history[$i]['num7']; $nextNum = (int)$history[$i + 1]['num7']; if ($currentNum < 1 || $currentNum > 49 || $nextNum < 1 || $nextNum > 49) continue; $from = $getZone($currentNum); $to = $getZone($nextNum); $matrix[$from][$to]++; $rowTotals[$from]++; $totalTransitions++; } // 计算概率矩阵 $probabilities = array_fill(0, 5, array_fill(0, 5, 0)); for ($i = 0; $i < 5; $i++) { if ($rowTotals[$i] > 0) { for ($j = 0; $j < 5; $j++) { $probabilities[$i][$j] = round($matrix[$i][$j] / $rowTotals[$i] * 100, 1); } } } return [ 'zones' => $zoneLabels, 'matrix' => $matrix, 'probabilities' => $probabilities, 'row_totals' => $rowTotals, 'total_transitions' => $totalTransitions ]; } /** * 波色转移概率统计 * 统计特码从一种波色转移到另一种波色的概率(红/蓝/绿) * @param int $periods 查询最近多少期 * @return array {colors: [], matrix: [], probabilities: [], row_totals: [], total_transitions: int} */ public function getColorWaveTransition($periods = 100) { $history = $this ->field('expect,num7,openTime') ->order('openTime', 'desc') ->limit($periods) ->select(); if (empty($history) || count($history) < 2) { return ['colors' => ['红波','蓝波','绿波'], 'matrix' => [], 'probabilities' => [], 'row_totals' => [], 'total_transitions' => 0]; } $history = array_reverse($history); $num_model = new Num(); $colorMap = $num_model->column('color', 'num'); $colorLabels = ['红波', '蓝波', '绿波']; $matrix = array_fill(0, 3, array_fill(0, 3, 0)); $rowTotals = array_fill(0, 3, 0); $getColorIdx = function ($num) use ($colorMap) { $color = $colorMap[$num] ?? ''; if (strpos($color, '红') !== false) return 0; if (strpos($color, '蓝') !== false) return 1; if (strpos($color, '绿') !== false) return 2; return -1; }; $totalTransitions = 0; for ($i = 0; $i < count($history) - 1; $i++) { $currentNum = (int)$history[$i]['num7']; $nextNum = (int)$history[$i + 1]['num7']; $from = $getColorIdx($currentNum); $to = $getColorIdx($nextNum); if ($from < 0 || $to < 0) continue; $matrix[$from][$to]++; $rowTotals[$from]++; $totalTransitions++; } $probabilities = array_fill(0, 3, array_fill(0, 3, 0)); for ($i = 0; $i < 3; $i++) { if ($rowTotals[$i] > 0) { for ($j = 0; $j < 3; $j++) { $probabilities[$i][$j] = round($matrix[$i][$j] / $rowTotals[$i] * 100, 1); } } } return [ 'colors' => $colorLabels, 'matrix' => $matrix, 'probabilities' => $probabilities, 'row_totals' => $rowTotals, 'total_transitions' => $totalTransitions ]; } /** * 特码热力图数据 * @param int $periods 查询最近多少期 * @return array {expects: [], heatmap: [[x, y, value]], colors: [号码对应颜色]} */ public function getSpecialHeatmap($periods = 30) { $num_model = new Num(); $colorMap = $num_model->column('color', 'num'); // 查询最近 N 期特码数据 $history = $this ->field('expect,num7,openTime') ->order('openTime', 'desc') ->limit($periods) ->select(); if (empty($history)) { return ['expects' => [], 'heatmap' => [], 'colors' => []]; } // 反转数组,使数据从左到右为从远到近 $history = array_reverse($history); $expects = []; $heatmap = []; // 构建热力图数据:[x_index, y_index, value] // x_index = 期号索引(0到periods-1) // y_index = 号码-1(0到48,号码1-49) // value = 1(出现)或 0(未出现) foreach ($history as $idx => $row) { $expects[] = (string)$row['expect']; $specialNum = (int)$row['num7']; // 标记该期特码号码出现 if ($specialNum >= 1 && $specialNum <= 49) { $heatmap[] = [$idx, $specialNum - 1, 1]; } } // 补充号码颜色映射(索引0对应号码1) $colors = []; for ($num = 1; $num <= 49; $num++) { $color = $colorMap[$num] ?? ''; if (strpos($color, '红') !== false) { $colors[] = '#e74c3c'; } elseif (strpos($color, '蓝') !== false) { $colors[] = '#3498db'; } elseif (strpos($color, '绿') !== false) { $colors[] = '#2ecc71'; } else { $colors[] = '#95a5a6'; } } return [ 'expects' => $expects, 'heatmap' => $heatmap, 'colors' => $colors, 'nums' => range(1, 49) // 号码列表 ]; } }