Files
amlhc/application/admin/model/History.php
T
916117771 78e7233bc0 feat(history): 添加历史数据管理功能和数据分析图表
- 在addons.php中添加example模块路由配置
- 新增application/config.php配置文件,包含应用设置、数据库配置等
- 实现dashboard.js仪表盘功能,包含冷热号码分析、比例分析图表
- 添加history.js历史数据管理功能,支持号码查询和统计分析
- 集成echarts图表库实现数据可视化展示
- 添加号码颜色映射和生肖映射功能
- 实现号码球样式格式化显示
- 添加遗漏号码、走势图、冷热分析等数据分析功能
2026-04-25 22:35:24 +08:00

729 lines
24 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)
];
}
/**
* 批量查询所有期号特码相对于前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];
}
/**
* 特码热力图数据
* @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) // 号码列表
];
}
}