Files
amlhc/application/admin/model/History.php
T
916117771 8d9161eb1a feat(dashboard): 添加后台仪表板功能并优化数据分析
- 新增 dashboard.js 文件实现前端图表展示功能
- 添加彩球渲染和颜色映射功能
- 实现数据加载和渲染逻辑
- 添加多种统计图表包括冷热号码、比例分析、生肖排名等
- 重构区域转移动态数据方法
- 更新区域到区域颜色转移概率计算逻辑
- 优化转移概率数据结构和显示方式
- 添加热力图和各种统计图表的响应式支持
2026-04-26 00:14:50 +08:00

937 lines
32 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\admin\model;
use think\Model;
use app\admin\model\Num;
class History extends Model
{
// 表名
protected $name = 'history';
// 自动写入时间戳字段
protected $autoWriteTimestamp = false;
// 定义时间戳字段名
protected $createTime = false;
protected $updateTime = false;
protected $deleteTime = false;
// 追加属性
protected $append = [
];
/**
* 获取走势图数据
* @param int $periods 查询最近多少期
* @param string $type 查询类型 all=全部号码 special=仅特码
* @return array {expects: [], data: [[num1,...], ...], colorMap: []}
*/
public function getTrendData($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,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),
'zonetocolortransition' => $this->getZoneToZoneColor($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 {zones: [], matrix: [[zone_count]], color_probs: [[{red, blue, green}]]}
*/
public function getZoneToZoneColor($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' => [], 'color_probs' => []];
}
$history = array_reverse($history);
$num_model = new Num();
$colorMap = $num_model->column('color', 'num');
$zoneLabels = ['1-10', '11-20', '21-30', '31-40', '41-49'];
$matrix = array_fill(0, 5, array_fill(0, 5, 0));
$colorCounts = [];
for ($i = 0; $i < 5; $i++) {
for ($j = 0; $j < 5; $j++) {
$colorCounts[$i][$j] = ['red' => 0, 'blue' => 0, 'green' => 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;
};
for ($i = 0; $i < count($history) - 1; $i++) {
$currentNum = (int)$history[$i]['num7'];
$nextNum = (int)$history[$i + 1]['num7'];
$from = $getZone($currentNum);
$to = $getZone($nextNum);
$color = $colorMap[$nextNum] ?? '';
$matrix[$from][$to]++;
if (strpos($color, '红') !== false) $colorCounts[$from][$to]['red']++;
elseif (strpos($color, '蓝') !== false) $colorCounts[$from][$to]['blue']++;
elseif (strpos($color, '绿') !== false) $colorCounts[$from][$to]['green']++;
}
// 计算每个单元格波色概率
$colorProbs = array_fill(0, 5, array_fill(0, 5, ['red' => 0, 'blue' => 0, 'green' => 0]));
for ($i = 0; $i < 5; $i++) {
for ($j = 0; $j < 5; $j++) {
$total = $matrix[$i][$j];
if ($total > 0) {
$colorProbs[$i][$j] = [
'red' => round($colorCounts[$i][$j]['red'] / $total * 100, 1),
'blue' => round($colorCounts[$i][$j]['blue'] / $total * 100, 1),
'green' => round($colorCounts[$i][$j]['green'] / $total * 100, 1),
];
}
}
}
return [
'zones' => $zoneLabels,
'matrix' => $matrix,
'color_probs' => $colorProbs
];
}
/**
* 特码热力图数据
* @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 = 号码-10到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) // 号码列表
];
}
}