feat(history): 新增特码冷热查询功能 — 选定某一期向前y期判定冷热号

在history页面添加「特码冷热」按钮,用户可选择指定期号并设定向前期数
系统统计该期特码在向前范围内的出现频率,与平均值对比判定冷/温/热号
This commit is contained in:
2026-04-24 19:58:35 +08:00
parent f4c67bd102
commit efdef3798e
7 changed files with 354 additions and 2 deletions
+2 -1
View File
@@ -28,7 +28,7 @@ See: .planning/PROJECT.md (updated 2026-04-21)
Phase: 01 (omitted-number-analysis) — COMPLETE Phase: 01 (omitted-number-analysis) — COMPLETE
Plan: 3 of 3 Plan: 3 of 3
Status: Phase 1 complete, ready to plan next phase Status: Phase 1 complete, ready to plan next phase
Last activity: 2026-04-22 -- Completed quick task 260422-vep: 特码热力图功能 Last activity: 2026-04-24 -- Completed quick task 260424-roj: 在history页面新增特码冷热查询功能
Progress: [████░░░░░░] 10% Progress: [████░░░░░░] 10%
@@ -74,6 +74,7 @@ None yet.
| # | Description | Date | Commit | Directory | | # | Description | Date | Commit | Directory |
|---|-------------|------|--------|-----------| |---|-------------|------|--------|-----------|
| 260422-vep | 在控制台增加特码热力图功能 | 2026-04-22 | 73e7403 | [260422-vep](./quick/260422-vep/) | | 260422-vep | 在控制台增加特码热力图功能 | 2026-04-22 | 73e7403 | [260422-vep](./quick/260422-vep/) |
| 260424-roj | 在history页面新增特码冷热查询功能 | 2026-04-24 | 2513bbb | [260424-roj](./quick/260424-roj-history-y/) |
## Deferred Items ## Deferred Items
@@ -0,0 +1,48 @@
---
description: 在history页面新增特码冷热号查询功能 — 选定某一期,向前推算y期,判断该期特码属于冷号还是热号
tasks: 3
must_haves:
- 后端接口接收 expect(期号) 和 lookback(向前期数) 参数
- 计算逻辑: 从指定期号往前lookback期, 统计该期特码在lookback范围内的出现频率, 判定冷热
- 前端弹窗: 选择期号 + 输入向前期数 + 展示冷热判定结果
plan_model: quick
---
# Quick Plan: 在history页面新增特码冷热号查询功能
## Task 1: 后端 Model — 添加 getSpecialHotColdByExpect 方法
**Files:** `application/admin/model/History.php`
**Action:** 新增方法 `getSpecialHotColdByExpect($expect, $lookback)`
- 根据指定期号 `expect` 查询到该期数据,获取该期特码 `num7`
- 从该期往前数 `lookback` 期(不包含该期本身),统计这期间每个号码的出现次数
- 计算该特码在 lookback 范围内的出现次数和频率
- 根据频率分布判定冷热:将该号码的出现次数与所有号码的平均值比较
- 高于平均值 1.5 倍以上 → 热号
- 低于平均值 0.5 倍以下 → 冷号
- 介于之间 → 温号
- 返回结构化数据:`{expect, specialNum, lookback, count, avgCount, status, rank, totalPeriods}`
## Task 2: 后端 Controller — 添加 specialHotColdAction 方法
**Files:** `application/admin/controller/History.php`
**Action:** 新增 `specialHotColdAction()` 方法
- 接收 AJAX GET 参数:`expect`(期号,必填), `lookback`(向前期数,默认30,范围10-100)
- 参数校验后调用 Model 方法
- 返回 JSON 响应
## Task 3: 前端 JS — 添加按钮、弹窗和渲染
**Files:** `application/admin/view/history/index.html`, `public/assets/js/backend/history.js`
**Action:**
-`index.html` 的 toolbar 添加一个「特码冷热」按钮
-`history.js``index` 方法中绑定点击事件
-`api` 对象中添加:
- `showSpecialHotColdDialog()` — 展示弹窗,包含:当前最新期号显示、期号选择下拉框、向前期数输入框、查询按钮、结果展示区
- `querySpecialHotCold(expect, lookback, layero)` — AJAX 请求后端接口
- `renderSpecialHotCold(data, layero)` — 渲染冷热判定结果,用颜色区分冷/温/热
@@ -0,0 +1,35 @@
---
description: 在history页面新增特码冷热查询功能 — 选定某一期,向前推算y期,判定该期特码属于冷号还是热号
status: complete
date: 2026-04-24
---
# Quick Task Summary: 特码冷热查询
## What was built
新增「特码冷热查询」功能,允许用户选择任意历史期号,设定向前追溯期数(10-100期),系统自动判定该期特码在追溯范围内属于冷号、温号还是热号。
## Changes made
### Backend — Model (`application/admin/model/History.php`)
- 新增 `getSpecialHotColdByExpect($expect, $lookback)` 方法
- 逻辑:根据指定期号找到该期特码,向前取 lookback 期数据,统计49个号码各自的出现次数
- 判定标准:出现次数 > 平均值×1.5 → 热号;< 平均值×0.5 → 冷号;其余为温号
- 返回包含:特码值、出现次数、平均值、冷热状态、频率排名、热号Top5、冷号Top5
### Backend — Controller (`application/admin/controller/History.php`)
- 新增 `specialHotColdAction()` 接口方法
- 接收 `expect`(期号,必填)和 `lookback`(向前期数,默认30,范围10-100)
- 已加入 `noNeedRight` 白名单
### Frontend — View (`application/admin/view/history/index.html`)
- 在 toolbar 新增「特码冷热」按钮(红色主题,fa-fire 图标)
### Frontend — JS (`public/assets/js/backend/history.js`)
- 新增 `showSpecialHotColdDialog()` — 弹窗包含:期号下拉选择(加载最近50期)、向前期数输入框、查询按钮
- 新增 `querySpecialHotCold()` — AJAX 请求后端
- 新增 `renderSpecialHotCold()` — 卡片式渲染结果:大号球显示特码、冷热状态标签、统计数据、热号/冷号Top5球
## Commit
`2513bbb`
+23 -1
View File
@@ -22,7 +22,7 @@ class History extends Backend
* 无需额外权限检查的方法(但仍在 admin 模块内,需要 admin 登录) * 无需额外权限检查的方法(但仍在 admin 模块内,需要 admin 登录)
* @var array * @var array
*/ */
protected $noNeedRight = ['missingNum', 'trendData', 'hotColdNumbers', 'colorWaveAnalysis', 'zodiacAnalysis', 'oddEvenAnalysis', 'bigSmallAnalysis', 'specialTrend', 'consecutiveNumbers', 'tailNumbers', 'dashboard', 'specialHeatmap']; protected $noNeedRight = ['missingNum', 'trendData', 'hotColdNumbers', 'colorWaveAnalysis', 'zodiacAnalysis', 'oddEvenAnalysis', 'bigSmallAnalysis', 'specialTrend', 'consecutiveNumbers', 'tailNumbers', 'dashboard', 'specialHeatmap', 'specialHotColdAction'];
public function _initialize() public function _initialize()
{ {
@@ -236,6 +236,28 @@ class History extends Backend
} }
} }
/**
* 特码冷热查询(指定期号向前y期判定)
*/
public function specialHotColdAction()
{
if ($this->request->isAjax()) {
$expect = $this->request->get('expect', '');
if (empty($expect)) {
$this->error('请输入期号');
}
$lookback = $this->request->get('lookback', 30, 'intval');
if ($lookback < 10 || $lookback > 100) {
$this->error('向前期数范围必须在 10-100 之间');
}
$result = $this->model->getSpecialHotColdByExpect($expect, $lookback);
if ($result === false) {
$this->error('未找到该期号数据');
}
$this->success('查询成功', null, $result);
}
}
/** /**
* 特码热力图 * 特码热力图
*/ */
+100
View File
@@ -468,6 +468,106 @@ class History extends Model
]; ];
} }
/**
* 查询指定期号特码在向前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 查询最近多少期 * @param int $periods 查询最近多少期
@@ -17,6 +17,7 @@
<a href="javascript:;" class="btn btn-success btn-sumchart" title="{:__('Sum Chart')}"><i class="fa fa-line-chart"></i> {:__('Sum Chart')}</a> <a href="javascript:;" class="btn btn-success btn-sumchart" title="{:__('Sum Chart')}"><i class="fa fa-line-chart"></i> {:__('Sum Chart')}</a>
<a href="javascript:;" class="btn btn-warning btn-consecutive" title="{:__('Consecutive')}"><i class="fa fa-link"></i> {:__('Consecutive')}</a> <a href="javascript:;" class="btn btn-warning btn-consecutive" title="{:__('Consecutive')}"><i class="fa fa-link"></i> {:__('Consecutive')}</a>
<a href="javascript:;" class="btn btn-default btn-tailnums" title="{:__('Tail Numbers')}"><i class="fa fa-list-ol"></i> {:__('Tail Numbers')}</a> <a href="javascript:;" class="btn btn-default btn-tailnums" title="{:__('Tail Numbers')}"><i class="fa fa-list-ol"></i> {:__('Tail Numbers')}</a>
<a href="javascript:;" class="btn btn-danger btn-specialhotcold" title="{:__('Special Hot/Cold')}"><i class="fa fa-fire"></i> {:__('Special Hot/Cold')}</a>
<!-- <a href="javascript:;" class="btn btn-success btn-dashboard" title="{:__('Dashboard')}"><i class="fa fa-tachometer"></i> {:__('Dashboard')}</a>--> <!-- <a href="javascript:;" class="btn btn-success btn-dashboard" title="{:__('Dashboard')}"><i class="fa fa-tachometer"></i> {:__('Dashboard')}</a>-->
<!-- <a href="javascript:;" class="btn btn-success btn-add {:$auth->check('history/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>--> <!-- <a href="javascript:;" class="btn btn-success btn-add {:$auth->check('history/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>-->
</div> </div>
+145
View File
@@ -87,6 +87,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
Controller.api.showAnalysisDialog('tailNumbers'); Controller.api.showAnalysisDialog('tailNumbers');
}); });
// 特码冷热按钮事件
$(document).off('click', '.btn-specialhotcold').on('click', '.btn-specialhotcold', function () {
Controller.api.showSpecialHotColdDialog();
});
// 综合统计面板按钮事件 // 综合统计面板按钮事件
$(document).off('click', '.btn-dashboard').on('click', '.btn-dashboard', function () { $(document).off('click', '.btn-dashboard').on('click', '.btn-dashboard', function () {
Controller.api.showDashboard(); Controller.api.showDashboard();
@@ -584,6 +589,146 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
$('#missing-result', layero).html('').append(container); $('#missing-result', layero).html('').append(container);
}, },
/**
* 特码冷热查询(指定期号向前y期判定)
*/
showSpecialHotColdDialog: function () {
var html = '<div style="padding:20px;">' +
'<div class="form-group">' +
' <label>期号:</label>' +
' <select id="shc-expect" class="form-control" style="width:200px;display:inline-block;"></select>' +
'</div>' +
'<div class="form-group">' +
' <label>向前期数:</label>' +
' <input type="number" id="shc-lookback" class="form-control" value="30" min="10" max="100" style="width:120px;display:inline-block;">' +
' <button class="btn btn-primary" id="btn-shc-query" style="margin-left:10px;"><i class="fa fa-search"></i> ' + __('Query') + '</button>' +
'</div>' +
'<div id="shc-result" style="margin-top:15px;"></div>' +
'</div>';
Layer.open({
type: 1,
title: '特码冷热查询',
area: ['650px', '550px'],
content: html,
shadeClose: true,
success: function (layero, index) {
// 加载最近50期期号供选择
$.ajax({
url: 'history/specialTrend',
type: 'GET',
data: {periods: 50},
dataType: 'json',
success: function (ret) {
if (ret.code == 1 && ret.data.expects) {
var options = '';
for (var i = 0; i < ret.data.expects.length; i++) {
options += '<option value="' + ret.data.expects[i] + '">' + ret.data.expects[i] + '</option>';
}
$('#shc-expect', layero).html(options);
}
}
});
$('#btn-shc-query', layero).on('click', function () {
var expect = $('#shc-expect', layero).val();
var lookback = parseInt($('#shc-lookback', layero).val()) || 30;
Controller.api.querySpecialHotCold(expect, lookback, layero);
});
}
});
},
querySpecialHotCold: function (expect, lookback, layero) {
var $btn = $('#btn-shc-query', layero);
$btn.prop('disabled', true);
$('#shc-result', layero).html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> ' + __('Loading') + '</div>');
$.ajax({
url: 'history/specialHotColdAction',
type: 'GET',
data: {expect: expect, lookback: lookback},
dataType: 'json',
success: function (ret) {
if (ret.code == 1) {
Controller.api.renderSpecialHotCold(ret.data, layero);
} else {
$('#shc-result', layero).html('<div class="alert alert-danger">' + (ret.msg || __('Query failed')) + '</div>');
}
},
error: function () {
$('#shc-result', layero).html('<div class="alert alert-danger">' + __('Query failed') + '</div>');
},
complete: function () {
$btn.prop('disabled', false);
}
});
},
renderSpecialHotCold: function (data, layero) {
var getColor = function (num) {
return Controller.api.getColorByNum(num);
};
var statusConfig = {
'hot': {label: '🔥 热号', color: '#e74c3c', bg: '#fce4ec', desc: '出现频率高于平均值的1.5倍'},
'cold': {label: '❄️ 冷号', color: '#3498db', bg: '#e3f2fd', desc: '出现频率低于平均值的0.5倍'},
'normal': {label: '➡️ 温号', color: '#f39c12', bg: '#fff8e1', desc: '出现频率在正常范围内'}
};
var cfg = statusConfig[data.status] || statusConfig['normal'];
var html = '<div style="padding:15px;">';
// 主信息卡片
html += '<div style="padding:20px;border-radius:8px;background:' + cfg.bg + ';margin-bottom:15px;">';
html += '<div style="display:flex;align-items:center;justify-content:space-between;">';
html += '<div>' +
'<div style="font-size:14px;color:#666;">期号 <b>' + data.expect + '</b> 的特码</div>' +
'<div style="margin-top:8px;">' +
'<span style="display:inline-block;width:48px;height:48px;line-height:48px;text-align:center;border-radius:50%;color:#fff;background-color:' + getColor(data.specialNum) + ';font-size:20px;font-weight:bold;">' + data.specialNum + '</span>' +
'</div>' +
'</div>';
html += '<div style="text-align:right;">' +
'<div style="font-size:24px;font-weight:bold;color:' + cfg.color + ';">' + cfg.label + '</div>' +
'<div style="font-size:12px;color:#999;margin-top:4px;">' + cfg.desc + '</div>' +
'</div>';
html += '</div></div>';
// 统计数据
html += '<div style="display:flex;gap:10px;margin-bottom:15px;">';
html += '<div style="flex:1;text-align:center;padding:10px;background:#f5f5f5;border-radius:6px;">' +
'<div style="font-size:22px;font-weight:bold;">' + data.count + '</div>' +
'<div style="font-size:12px;color:#666;">近' + data.lookback + '期出现次数</div></div>';
html += '<div style="flex:1;text-align:center;padding:10px;background:#f5f5f5;border-radius:6px;">' +
'<div style="font-size:22px;font-weight:bold;">' + data.avgCount + '</div>' +
'<div style="font-size:12px;color:#666;">平均出现次数</div></div>';
html += '<div style="flex:1;text-align:center;padding:10px;background:#f5f5f5;border-radius:6px;">' +
'<div style="font-size:22px;font-weight:bold;">第' + data.rank + '名</div>' +
'<div style="font-size:12px;color:#666;">频率排名 (共' + data.totalPeriods + '期)</div></div>';
html += '</div>';
// 热号/冷号参考
if (data.hotNums && data.hotNums.length > 0) {
html += '<div style="margin-bottom:10px;"><b style="color:#e74c3c;">🔥 热号 Top5</b><div style="display:flex;gap:6px;margin-top:5px;">';
for (var i = 0; i < data.hotNums.length; i++) {
var item = data.hotNums[i];
html += '<span style="display:inline-block;width:32px;height:32px;line-height:32px;text-align:center;border-radius:50%;color:#fff;background-color:' + getColor(item.num) + ';font-weight:bold;font-size:14px;" title="' + item.count + '次">' + item.num + '</span>';
}
html += '</div></div>';
}
if (data.coldNums && data.coldNums.length > 0) {
html += '<div><b style="color:#3498db;">❄️ 冷号 Top5</b><div style="display:flex;gap:6px;margin-top:5px;">';
for (var i = 0; i < data.coldNums.length; i++) {
var item = data.coldNums[i];
html += '<span style="display:inline-block;width:32px;height:32px;line-height:32px;text-align:center;border-radius:50%;color:#fff;background-color:' + getColor(item.num) + ';font-weight:bold;font-size:14px;" title="' + item.count + '次">' + item.num + '</span>';
}
html += '</div></div>';
}
html += '</div>';
$('#shc-result', layero).html(html);
},
bindevent: function () { bindevent: function () {
Form.api.bindevent($("form[role=form]")); Form.api.bindevent($("form[role=form]"));
}, },