Files
amlhc/application/admin/model/History.php
T
916117771 efdef3798e feat(history): 新增特码冷热查询功能 — 选定某一期向前y期判定冷热号
在history页面添加「特码冷热」按钮,用户可选择指定期号并设定向前期数
系统统计该期特码在向前范围内的出现频率,与平均值对比判定冷/温/热号
2026-04-24 20:07:00 +08:00

635 lines
22 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)
];
}
/**
* 查询指定期号特码在向前y期范围内的冷热状态
* @param string|int $expect 指定期号
* @param int $lookback 向前推算期数
* @return array|false 冷热状态数据,未找到返回false
*/
public function getSpecialHotColdByExpect($expect, $lookback = 30)
{
// 查询指定期号的数据
$target = $this->where('expect', $expect)->field('expect,num7,openTime')->find();
if (!$target) {
return false;
}
$specialNum = (int)$target['num7'];
// 查询该期往前lookback期的数据(按openTime排序,取目标期之前的lookback条)
$history = $this
->field('expect,num7,openTime')
->where('openTime', '<', $target['openTime'])
->order('openTime', 'desc')
->limit($lookback)
->select();
$totalPeriods = count($history);
if ($totalPeriods === 0) {
return [
'expect' => (string)$expect,
'specialNum' => $specialNum,
'lookback' => $lookback,
'count' => 0,
'avgCount' => 0,
'status' => 'cold',
'rank' => 0,
'totalPeriods' => 0,
'allStats' => []
];
}
// 统计lookback范围内每个特码的出现次数
$count = array_fill(1, 49, 0);
foreach ($history as $row) {
$num = (int)$row['num7'];
if ($num >= 1 && $num <= 49) {
$count[$num]++;
}
}
// 计算目标特码的出现次数
$targetCount = $count[$specialNum];
// 计算平均出现次数(49个号码,totalPeriods期)
$avgCount = $totalPeriods / 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;
}
}
// 构建所有号码的统计(只返回top和bottom用于展示)
$hotNums = array_slice($sorted, 0, 5);
$coldNums = array_slice($sorted, -5);
$coldNums = array_reverse($coldNums);
return [
'expect' => (string)$expect,
'specialNum' => $specialNum,
'lookback' => $lookback,
'count' => $targetCount,
'avgCount' => round($avgCount, 2),
'status' => $status,
'rank' => $rank,
'totalPeriods' => $totalPeriods,
'hotNums' => $hotNums,
'coldNums' => $coldNums
];
}
/**
* 特码热力图数据
* @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) // 号码列表
];
}
}