feat(history): 新增历史开奖数据分析功能

- 添加History控制器和模型实现开奖数据统计分析
- 实现遗漏号码、走势图、冷热号码等基础分析功能
- 添加波色分析、生肖分析、奇偶分析等专项统计
- 集成尾号转移矩阵和首号转移矩阵概率分析
- 在后台仪表板展示综合统计数据表格
This commit is contained in:
2026-04-30 22:45:03 +08:00
parent d18c23cd57
commit 6a782c2d65
4 changed files with 212 additions and 5 deletions
+31 -1
View File
@@ -22,7 +22,7 @@ class History extends Backend
* 无需额外权限检查的方法(但仍在 admin 模块内,需要 admin 登录)
* @var array
*/
protected $noNeedRight = ['missingNum', 'trendData', 'hotColdNumbers', 'colorWaveAnalysis', 'zodiacAnalysis', 'oddEvenAnalysis', 'bigSmallAnalysis', 'specialTrend', 'consecutiveNumbers', 'tailNumbers', 'dashboard', 'specialHeatmap', 'specialHotColdAction', 'zoneTransition', 'colorWaveTransition', 'zoneToColorTransition', 'zodiacTransition'];
protected $noNeedRight = ['missingNum', 'trendData', 'hotColdNumbers', 'colorWaveAnalysis', 'zodiacAnalysis', 'oddEvenAnalysis', 'bigSmallAnalysis', 'specialTrend', 'consecutiveNumbers', 'tailNumbers', 'dashboard', 'specialHeatmap', 'specialHotColdAction', 'zoneTransition', 'colorWaveTransition', 'zoneToColorTransition', 'zodiacTransition', 'tailNumberTransition', 'headNumberTransition'];
public function _initialize()
{
@@ -358,5 +358,35 @@ class History extends Backend
}
}
/**
* 尾号转移概率统计
*/
public function tailNumberTransition()
{
if ($this->request->isAjax()) {
$periods = $this->request->get('periods', 100, 'intval');
if ($periods < 10 || $periods > 500) {
$this->error('期数范围必须在 10-500 之间');
}
$result = $this->model->getTailNumberTransition($periods);
$this->success('查询成功', null, $result);
}
}
/**
* 首号转移概率统计
*/
public function headNumberTransition()
{
if ($this->request->isAjax()) {
$periods = $this->request->get('periods', 100, 'intval');
if ($periods < 10 || $periods > 500) {
$this->error('期数范围必须在 10-500 之间');
}
$result = $this->model->getHeadNumberTransition($periods);
$this->success('查询成功', null, $result);
}
}
}
+124 -1
View File
@@ -468,7 +468,9 @@ class History extends Model
'zonetransition' => $this->getZoneTransition($periods),
'colorwavetransition' => $this->getColorWaveTransition($periods),
'zonetocolortransition' => $this->getZoneToZoneColor($periods),
'zodiactransition' => $this->getZodiacTransition($periods)
'zodiactransition' => $this->getZodiacTransition($periods),
'tailnumbertransition' => $this->getTailNumberTransition($periods),
'headnumbertransition' => $this->getHeadNumberTransition($periods)
];
}
@@ -861,6 +863,127 @@ class History extends Model
];
}
/**
* 尾号转移概率统计
* 统计特码从一个尾号转移到另一个尾号的概率(尾号0-9)
* @param int $periods 查询最近多少期
* @return array {tails: [], matrix: [], probabilities: [], row_totals: [], total_transitions: int}
*/
public function getTailNumberTransition($periods = 100)
{
$history = $this
->field('expect,num7,openTime')
->order('openTime', 'desc')
->limit($periods)
->select();
if (empty($history) || count($history) < 2) {
return ['tails' => ['尾0','尾1','尾2','尾3','尾4','尾5','尾6','尾7','尾8','尾9'], 'matrix' => [], 'probabilities' => [], 'row_totals' => [], 'total_transitions' => 0];
}
$history = array_reverse($history);
$tailLabels = ['尾0', '尾1', '尾2', '尾3', '尾4', '尾5', '尾6', '尾7', '尾8', '尾9'];
$numTails = 10;
$matrix = array_fill(0, $numTails, array_fill(0, $numTails, 0));
$rowTotals = array_fill(0, $numTails, 0);
$getTail = function ($num) {
return $num % 10;
};
$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 = $getTail($currentNum);
$to = $getTail($nextNum);
$matrix[$from][$to]++;
$rowTotals[$from]++;
$totalTransitions++;
}
$probabilities = array_fill(0, $numTails, array_fill(0, $numTails, 0));
for ($i = 0; $i < $numTails; $i++) {
if ($rowTotals[$i] > 0) {
for ($j = 0; $j < $numTails; $j++) {
$probabilities[$i][$j] = round($matrix[$i][$j] / $rowTotals[$i] * 100, 1);
}
}
}
return [
'tails' => $tailLabels,
'matrix' => $matrix,
'probabilities' => $probabilities,
'row_totals' => $rowTotals,
'total_transitions' => $totalTransitions
];
}
/**
* 首号转移概率统计
* 统计特码从一个首号转移到另一个首号的概率(首号0-4)
* 首号定义:1-9首号=010-19首号=120-29首号=230-39首号=340-49首号=4
* @param int $periods 查询最近多少期
* @return array {heads: [], matrix: [], probabilities: [], row_totals: [], total_transitions: int}
*/
public function getHeadNumberTransition($periods = 100)
{
$history = $this
->field('expect,num7,openTime')
->order('openTime', 'desc')
->limit($periods)
->select();
if (empty($history) || count($history) < 2) {
return ['heads' => ['首号0','首号1','首号2','首号3','首号4'], 'matrix' => [], 'probabilities' => [], 'row_totals' => [], 'total_transitions' => 0];
}
$history = array_reverse($history);
$headLabels = ['首号0', '首号1', '首号2', '首号3', '首号4'];
$numHeads = 5;
$matrix = array_fill(0, $numHeads, array_fill(0, $numHeads, 0));
$rowTotals = array_fill(0, $numHeads, 0);
$getHead = function ($num) {
return intval(floor($num / 10));
};
$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 = $getHead($currentNum);
$to = $getHead($nextNum);
$matrix[$from][$to]++;
$rowTotals[$from]++;
$totalTransitions++;
}
$probabilities = array_fill(0, $numHeads, array_fill(0, $numHeads, 0));
for ($i = 0; $i < $numHeads; $i++) {
if ($rowTotals[$i] > 0) {
for ($j = 0; $j < $numHeads; $j++) {
$probabilities[$i][$j] = round($matrix[$i][$j] / $rowTotals[$i] * 100, 1);
}
}
}
return [
'heads' => $headLabels,
'matrix' => $matrix,
'probabilities' => $probabilities,
'row_totals' => $rowTotals,
'total_transitions' => $totalTransitions
];
}
/**
* 区域→区域转移矩阵,单元格内显示波色概率
* 统计上一期特码所在区域后,下一期特码在各区域的分布,以及每个区域内的波色占比
+11 -3
View File
@@ -4,6 +4,7 @@ namespace app\timetask\controller;
use app\common\controller\Api;
use think\Db;
use think\exception\HttpResponseException;
class Index extends Api
{
@@ -38,12 +39,19 @@ class Index extends Api
}
}
$this->success('获取成功');
} catch (\Exception $e) {
$this->error($e->getMessage());
} catch (HttpResponseException $e) {
// 这是框架正常的响应异常,直接抛出让它继续执行
throw $e;
} catch (\Throwable $e) {
// 捕获其他异常和错误
$errorMsg = $e->getMessage() ?: get_class($e);
$file = $e->getFile();
$line = $e->getLine();
\think\Log::write("get_history 异常: {$errorMsg} at {$file}:{$line}", 'error');
$this->error('异常:' . $errorMsg . ' [' . $file . ':' . $line . ']');
}
} else {
$this->error('获取失败');
}
+46
View File
@@ -176,6 +176,52 @@ define(['jquery'], function ($) {
}
html += '</tbody></table>';
}
// 尾号转移矩阵
var tnt = data.tailnumbertransition;
if (tnt && tnt.matrix && tnt.matrix.length > 0) {
html += '<div style="margin-top:20px;font-size:12px;color:#999;margin-bottom:8px;">尾号转移(共 ' + tnt.total_transitions + ' 次)</div>';
html += '<table class="table table-bordered table-condensed text-center" style="max-width:600px;margin:0 auto;"><thead><tr><th style="width:50px;">特码</th>';
for (var z = 0; z < tnt.tails.length; z++) {
html += '<th>' + tnt.tails[z] + '</th>';
}
html += '</tr></thead><tbody>';
for (var r = 0; r < tnt.tails.length; r++) {
html += '<tr><td style="font-weight:bold;">' + tnt.tails[r] + '</td>';
for (var c = 0; c < tnt.tails.length; c++) {
var pct = tnt.probabilities[r][c];
var cnt = tnt.matrix[r][c];
var bg = pct > 20 ? '#e74c3c' : pct > 15 ? '#f39c12' : pct > 10 ? '#3498db' : pct > 0 ? '#95a5a6' : '#f5f5f5';
var txt = pct > 10 ? '#fff' : '#333';
html += '<td style="background-color:' + bg + ';color:' + txt + ';">' + cnt + '次<br>' + pct + '%</td>';
}
html += '</tr>';
}
html += '</tbody></table>';
}
// 首号转移矩阵
var hnt = data.headnumbertransition;
if (hnt && hnt.matrix && hnt.matrix.length > 0) {
html += '<div style="margin-top:20px;font-size:12px;color:#999;margin-bottom:8px;">首号转移(共 ' + hnt.total_transitions + ' 次)</div>';
html += '<table class="table table-bordered table-condensed text-center" style="max-width:400px;margin:0 auto;"><thead><tr><th style="width:60px;">特码</th>';
for (var z = 0; z < hnt.heads.length; z++) {
html += '<th>' + hnt.heads[z] + '</th>';
}
html += '</tr></thead><tbody>';
for (var r = 0; r < hnt.heads.length; r++) {
html += '<tr><td style="font-weight:bold;">' + hnt.heads[r] + '</td>';
for (var c = 0; c < hnt.heads.length; c++) {
var pct = hnt.probabilities[r][c];
var cnt = hnt.matrix[r][c];
var bg = pct > 30 ? '#e74c3c' : pct > 20 ? '#f39c12' : pct > 10 ? '#3498db' : pct > 0 ? '#95a5a6' : '#f5f5f5';
var txt = pct > 10 ? '#fff' : '#333';
html += '<td style="background-color:' + bg + ';color:' + txt + ';">' + cnt + '次<br>' + pct + '%</td>';
}
html += '</tr>';
}
html += '</tbody></table>';
}
html += '</div>';
}