diff --git a/.planning/STATE.md b/.planning/STATE.md index a48d539..3a51fa8 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -28,7 +28,7 @@ See: .planning/PROJECT.md (updated 2026-04-21) Phase: 01 (omitted-number-analysis) — COMPLETE Plan: 3 of 3 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% @@ -74,6 +74,7 @@ None yet. | # | Description | Date | Commit | Directory | |---|-------------|------|--------|-----------| | 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 diff --git a/.planning/quick/260424-roj-history-y/260424-roj-PLAN.md b/.planning/quick/260424-roj-history-y/260424-roj-PLAN.md new file mode 100644 index 0000000..d88bf48 --- /dev/null +++ b/.planning/quick/260424-roj-history-y/260424-roj-PLAN.md @@ -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)` — 渲染冷热判定结果,用颜色区分冷/温/热 diff --git a/.planning/quick/260424-roj-history-y/260424-roj-SUMMARY.md b/.planning/quick/260424-roj-history-y/260424-roj-SUMMARY.md new file mode 100644 index 0000000..d462d6f --- /dev/null +++ b/.planning/quick/260424-roj-history-y/260424-roj-SUMMARY.md @@ -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` diff --git a/application/admin/controller/History.php b/application/admin/controller/History.php index bc2166e..9bd44dd 100644 --- a/application/admin/controller/History.php +++ b/application/admin/controller/History.php @@ -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']; + protected $noNeedRight = ['missingNum', 'trendData', 'hotColdNumbers', 'colorWaveAnalysis', 'zodiacAnalysis', 'oddEvenAnalysis', 'bigSmallAnalysis', 'specialTrend', 'consecutiveNumbers', 'tailNumbers', 'dashboard', 'specialHeatmap', 'specialHotColdAction']; 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); + } + } + /** * 特码热力图 */ diff --git a/application/admin/model/History.php b/application/admin/model/History.php index 1ab210b..ebeffdb 100644 --- a/application/admin/model/History.php +++ b/application/admin/model/History.php @@ -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 查询最近多少期 diff --git a/application/admin/view/history/index.html b/application/admin/view/history/index.html index 8754be8..b2c275d 100644 --- a/application/admin/view/history/index.html +++ b/application/admin/view/history/index.html @@ -17,6 +17,7 @@ {:__('Sum Chart')} {:__('Consecutive')} {:__('Tail Numbers')} + {:__('Special Hot/Cold')} diff --git a/public/assets/js/backend/history.js b/public/assets/js/backend/history.js index 99a4350..8dad69f 100644 --- a/public/assets/js/backend/history.js +++ b/public/assets/js/backend/history.js @@ -87,6 +87,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin 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 () { Controller.api.showDashboard(); @@ -584,6 +589,146 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin $('#missing-result', layero).html('').append(container); }, + /** + * 特码冷热查询(指定期号向前y期判定) + */ + showSpecialHotColdDialog: function () { + var html = '
' + + '
' + + ' ' + + ' ' + + '
' + + '
' + + ' ' + + ' ' + + ' ' + + '
' + + '
' + + '
'; + + 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 += ''; + } + $('#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('
' + __('Loading') + '
'); + $.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('
' + (ret.msg || __('Query failed')) + '
'); + } + }, + error: function () { + $('#shc-result', layero).html('
' + __('Query failed') + '
'); + }, + 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 = '
'; + + // 主信息卡片 + html += '
'; + html += '
'; + html += '
' + + '
期号 ' + data.expect + ' 的特码
' + + '
' + + '' + data.specialNum + '' + + '
' + + '
'; + html += '
' + + '
' + cfg.label + '
' + + '
' + cfg.desc + '
' + + '
'; + html += '
'; + + // 统计数据 + html += '
'; + html += '
' + + '
' + data.count + '
' + + '
近' + data.lookback + '期出现次数
'; + html += '
' + + '
' + data.avgCount + '
' + + '
平均出现次数
'; + html += '
' + + '
第' + data.rank + '名
' + + '
频率排名 (共' + data.totalPeriods + '期)
'; + html += '
'; + + // 热号/冷号参考 + if (data.hotNums && data.hotNums.length > 0) { + html += '
🔥 热号 Top5
'; + for (var i = 0; i < data.hotNums.length; i++) { + var item = data.hotNums[i]; + html += '' + item.num + ''; + } + html += '
'; + } + if (data.coldNums && data.coldNums.length > 0) { + html += '
❄️ 冷号 Top5
'; + for (var i = 0; i < data.coldNums.length; i++) { + var item = data.coldNums[i]; + html += '' + item.num + ''; + } + html += '
'; + } + + html += '
'; + $('#shc-result', layero).html(html); + }, + bindevent: function () { Form.api.bindevent($("form[role=form]")); },