22 KiB
Phase 1: 遗漏号码分析 - Research
Researched: 2026-04-21 Domain: FastAdmin 1.6 + ThinkPHP 5.x / AJAX endpoint + Layer modal Confidence: HIGH
Summary
This phase adds a "遗漏号码" (Missing Number) feature to the existing history admin page. The implementation requires three touchpoints: a toolbar button in the history view, a Layer dialog for user input, and a backend AJAX endpoint that calculates which numbers (1-49) did not appear in the last X periods. The missing number calculation runs entirely in PHP on the backend; the frontend only handles UI rendering.
Primary recommendation: Add missingNum() controller method in History.php with $noNeedRight = ['*'], use Layer.open() with inline HTML content for the dialog, and render results as a flex-wrapped grid of colored balls using the existing getColorByNum() logic already present in history.js.
User Constraints (from CONTEXT.md / STATE.md)
Locked Decisions
- 遗漏号码在 history 页面以按钮+弹窗形式展示,不新增独立页面/菜单
- 遗漏计算在后端完成,前端只负责展示
- 使用 $.ajax 请求遗漏接口,Layer 弹窗展示
Claude's Discretion
- (None specified)
Deferred Ideas (OUT OF SCOPE)
- 遗漏统计历史趋势图
- 遗漏号码的预测推荐
- 前台用户可见
Phase Requirements
| ID | Description | Research Support |
|---|---|---|
| OMIT-01 | history 页面新增"遗漏号码"按钮,点击弹窗展示 | toolbar 按钮 + Layer.open() 方案 |
| OMIT-02 | 弹窗内可输入期数 X(默认 10),点击查询后展示最近 X 期未出现的号码 | Layer prompt HTML + 后端 missingNum 接口 |
| OMIT-03 | 展示内容为:遗漏号码 + 遗漏期数(多少期没出现)+ 波色球 | flex 网格 + 复用 getColorByNum() |
| OMIT-04 | 遗漏号码按遗漏期数从大到小排序 | PHP usort() / SQL ORDER BY |
| OMIT-05 | 后端接口支持查询最近 X 期开奖数据并计算遗漏号码(1-49 范围) | History::getMissingNumbers() model 方法 |
Architectural Responsibility Map
| Capability | Primary Tier | Secondary Tier | Rationale |
|---|---|---|---|
| 遗漏计算 | API / Backend | — | 需查询数据库 fa_history,属于业务逻辑 |
| 弹窗 UI | Browser / Client | — | Layer 弹窗,纯前端展示 |
| 波色球着色 | Browser / Client | API / Backend | 前端复用已有 getColorByNum() 映射 |
| 数据查询 | Database / Storage | — | SQL ORDER BY openTime DESC LIMIT X |
Standard Stack
Core
| Library | Version | Purpose | Why Standard |
|---|---|---|---|
| ThinkPHP 5.x | dev-master (Gitee) | MVC framework | Project foundation, all controllers extend TP base |
| FastAdmin 1.6.1 | 1.6.2.20260323 | Admin framework | Provides Backend trait, Layer integration, Fast.api utilities |
Supporting (Frontend)
| Library | Version | Purpose | When to Use |
|---|---|---|---|
| fastadmin-layer | 3.5.6 | Modal/dialog overlay | All admin dialogs use Layer |
| jQuery | 3.7.1 | DOM manipulation + AJAX | Standard throughout project |
| Bootstrap 3.4.1 | via fastadmin-bootstrap | UI components | Grid, buttons, form controls |
Installation
No new packages needed — all dependencies already present in the project.
Architecture Patterns
System Architecture Diagram
[History Page] → User clicks "遗漏号码" button
│
▼
[Layer.open() - Inline HTML]
│
├── Input: 期数 X (default: 10)
│
▼
[JS: $.ajax → history/missingNum]
│
│ params: { periods: X }
│
▼
[History::missingNum() Controller]
│
▼
[History::getMissingNumbers(periods) Model]
│
├── SELECT num1~num7 FROM fa_history ORDER BY openTime DESC LIMIT X
├── Build appeared_numbers set
├── For num 1..49: find last appeared period count (omission count)
├── Return: [{num, omit_count, color}, ...] sorted by omit_count DESC
│
▼
[JS: Render HTML grid]
│
├── For each result: <span class="num-ball"> with background-color from getColorByNum()
└── Show: number + 遗漏X期 label
Recommended Project Structure
No new directories needed. Changes are localized to existing files:
application/admin/
├── controller/History.php # ADD: missingNum() method + $noNeedRight
├── model/History.php # ADD: getMissingNumbers($periods) method
├── lang/zh-cn/history.php # ADD: i18n strings for missing numbers
public/assets/js/
└── backend/history.js # ADD: button handler + dialog + render logic
application/admin/view/
└── history/index.html # ADD: "遗漏号码" button to toolbar
Pattern 1: Custom Controller Method with AJAX Response
What: Add a new public method to a Backend controller that returns JSON via $this->success().
When to use: Any admin AJAX endpoint that doesn't fit standard CRUD.
Example:
// application/admin/controller/History.php
class History extends Backend
{
// 无需登录即可访问(但仍在 admin 模块内,受 admin auth 保护)
protected $noNeedRight = ['*'];
/**
* 查询遗漏号码
*/
public function missingNum()
{
if ($this->request->isAjax()) {
$periods = $this->request->get('periods', 10, 'intval');
if ($periods < 1 || $periods > 100) {
$this->error('期数范围必须在 1-100 之间');
}
$result = $this->model->getMissingNumbers($periods);
$this->success('查询成功', $result);
}
}
}
Source: [VERIFIED: application/admin/controller/Ajax.php — standard $this->success()/$this->error() pattern]
Pattern 2: Layer Dialog with Inline HTML
What: Use Layer.open() with type: 1 to display a modal with custom HTML content.
When to use: When the dialog doesn't need a separate page/view file and contains custom layout.
Example:
// public/assets/js/backend/history.js
$('#toolbar').on('click', '.btn-missingnum', function () {
var html = '<div style="padding:20px;">' +
'<div class="form-group">' +
' <label>查询期数:</label>' +
' <input type="number" id="missing-periods" class="form-control" value="10" min="1" max="100">' +
'</div>' +
'<button class="btn btn-primary" id="btn-missing-query">查询</button>' +
'<div id="missing-result" style="margin-top:15px;"></div>' +
'</div>';
Layer.open({
type: 1,
title: '遗漏号码分析',
area: ['600px', '500px'],
content: html,
shadeClose: true
});
});
Source: [VERIFIED: application/admin/controller/Ajax.php pattern + Layer 3.5.6 API]
Anti-Patterns to Avoid
- Don't create a new view file — The phase requirement explicitly says "按钮+弹窗形式,不新增独立页面". Use
Layer.open({type: 1, content: html})with inline HTML. - Don't use Bootstrap Table for results — Overkill for a simple grid of 49 numbers. Use flex-wrapped div grid.
- Don't calculate in frontend — Already decided: backend calculation only. Frontend AJAX calls the endpoint.
- Don't use
Fast.api.open()— That opens an iframe-based dialog pointing to a URL. We want a self-contained dialog with inline content, so useLayer.open({type: 1})directly.
Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---|---|---|---|
| 遗漏计算 | Manual array scanning without SQL | SQL ORDER BY + PHP array intersection | SQL does the sorting efficiently; PHP handles the set difference |
| 波色球着色 | Hard-coded color mapping | Reuse existing getColorByNum() in history.js |
Already handles 红/蓝/绿→CSS color mapping |
| AJAX 请求 | Raw $.ajax with manual error handling | Fast.api.ajax() or standard $.ajax with FastAdmin response format |
FastAdmin's response envelope {code, msg, data} is standard |
| 弹窗对话框 | Custom modal HTML/CSS | Layer.open() | Layer is the project's standard dialog system |
| i18n 文本 | Hard-coded Chinese strings | Use __('key') + lang file |
Project uses __() function for translation |
Key insight: FastAdmin already provides every building block needed — Layer for dialogs, Fast.api.ajax for requests, getColorByNum() for rendering. The only new code is the missing number algorithm and the button handler.
Runtime State Inventory
This is a greenfield feature within an existing project — no rename/refactor/migration involved.
N/A — No existing state needs updating. This is a new feature addition.
Common Pitfalls
Pitfall 1: Permission Block on Custom Controller Method
What goes wrong: Adding a new public method to a Backend controller but forgetting $noNeedRight, causing 403 errors.
Why it happens: FastAdmin's Backend trait auto-checks permissions via Auth::check($path) in _initialize(). Any method not in the auth rule table or $noNeedRight array will be blocked.
How to avoid: Set protected $noNeedRight = ['missingNum'] or protected $noNeedRight = ['*'] in the controller.
Warning signs: AJAX returns HTML login page or 403 error instead of JSON.
Pitfall 2: Color Map Not Loaded Before Rendering
What goes wrong: Rendering colored balls before loadColorMap() completes, resulting in gray balls.
Why it happens: Controller.api.loadColorMap() is async — the callback fires after the AJAX succeeds.
How to avoid: The dialog's query handler should check Controller.api.colorMapLoaded and call loadColorMap() first if not ready.
Warning signs: Balls render with #95a5a6 (default gray) instead of correct colors.
Pitfall 3: Missing Number Calculation — Only Counting Appear/Not-Appear
What goes wrong: Returning numbers that never appeared in X periods, but not calculating how many periods each number has been missing. Why it happens: Confusing "not appeared in X periods" with "omission count" (遗漏期数). A number might have appeared in period N-50 but not in the last 10 — its omission count should be 50, not 10. How to avoid: Query more than X periods (e.g., last 200 periods or all records) to calculate true omission counts, then filter/sort by omission. The omission count = total periods since last appearance. Warning signs: All missing numbers show the same omission count as the query period.
Pitfall 4: Number Format Mismatch (String vs Int)
What goes wrong: Database num1~num7 are strings, but color lookup uses integer keys.
Why it happens: ThinkPHP returns all DB values as strings by default. The colorMap from num/getColorMap uses string keys like {"1": "红波"}.
How to avoid: Always parseInt() the number before color lookup. The existing getColorByNum() already does this correctly.
Warning signs: undefined returned from colorMap[num].
Code Examples
Backend: Missing Number Calculation (PHP)
// application/admin/model/History.php
/**
* 计算遗漏号码
* @param int $periods 查询最近多少期
* @return array [{num: 1, omit: 50, color: '红波'}, ...]
*/
public function getMissingNumbers($periods = 10)
{
// 查询最近 $periods 期开奖数据
$history = Db::name('history')
->field('num1,num2,num3,num4,num5,num6,num7')
->order('openTime', 'desc')
->limit($periods)
->select();
// 收集最近 $periods 期出现过的号码
$appeared = [];
foreach ($history as $row) {
for ($i = 1; $i <= 7; $i++) {
if ($row['num' . $i] !== null && $row['num' . $i] !== '') {
$appeared[(int)$row['num' . $i]] = true;
}
}
}
// 获取遗漏号码(1-49中未出现的)
$missing = [];
for ($num = 1; $num <= 49; $num++) {
if (!isset($appeared[$num])) {
$missing[] = $num;
}
}
// 获取波色映射
$colorMap = Db::name('num')->column('color', 'num');
// 计算遗漏期数(需要查询更多历史数据)
$allHistory = Db::name('history')
->field('num1,num2,num3,num4,num5,num6,num7')
->order('openTime', 'desc')
->limit(500) // 最多查500期
->select();
$result = [];
foreach ($missing as $num) {
$omitCount = $this->calcOmitCount($num, $allHistory);
$result[] = [
'num' => $num,
'omit' => $omitCount,
'color' => $colorMap[$num] ?? '—'
];
}
// 按遗漏期数降序排序
usort($result, function ($a, $b) {
return $b['omit'] - $a['omit'];
});
return $result;
}
/**
* 计算某个号码的遗漏期数
*/
private function calcOmitCount($num, $allHistory)
{
foreach ($allHistory as $idx => $row) {
for ($i = 1; $i <= 7; $i++) {
if ((int)$row['num' . $i] === $num) {
return $idx; // 当前索引即为遗漏期数
}
}
}
return count($allHistory); // 如果500期内都没出现,返回500+
}
Frontend: Button Handler + Dialog + Render
// public/assets/js/backend/history.js — inside Controller.index()
// 添加遗漏号码按钮到 toolbar
$('#toolbar').append('<a href="javascript:;" class="btn btn-warning btn-missingnum"><i class="fa fa-search"></i> 遗漏号码</a>');
// 按钮点击事件
$(document).on('click', '.btn-missingnum', function () {
Controller.api.showMissingNumDialog();
});
// public/assets/js/backend/history.js — inside Controller.api
showMissingNumDialog: function () {
var html = '<div style="padding:20px;">' +
'<div class="form-group">' +
' <label>查询最近期数:</label>' +
' <input type="number" id="missing-periods" class="form-control" value="10" min="1" max="100" style="width:120px;display:inline-block;">' +
' <button class="btn btn-primary" id="btn-missing-query" style="margin-left:10px;"><i class="fa fa-search"></i> 查询</button>' +
'</div>' +
'<div id="missing-result" style="margin-top:15px;"></div>' +
'</div>';
Layer.open({
type: 1,
title: __('Missing Number Analysis'),
area: ['650px', '550px'],
content: html,
shadeClose: true,
success: function (layero, index) {
// 绑定查询按钮
$('#btn-missing-query', layero).on('click', function () {
var periods = parseInt($('#missing-periods', layero).val()) || 10;
Controller.api.queryMissingNum(periods, layero);
});
}
});
},
queryMissingNum: function (periods, layero) {
$('#missing-result', layero).html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> 查询中...</div>');
$.ajax({
url: 'history/missingNum',
type: 'GET',
data: { periods: periods },
dataType: 'json',
success: function (ret) {
if (ret.code == 1) {
Controller.api.renderMissingNum(ret.data, layero);
} else {
$('#missing-result', layero).html('<div class="text-danger">' + ret.msg + '</div>');
}
},
error: function () {
$('#missing-result', layero).html('<div class="text-danger">请求失败</div>');
}
});
},
renderMissingNum: function (data, layero) {
if (!data || data.length === 0) {
$('#missing-result', layero).html('<div class="alert alert-info">最近 ' + periods + ' 期内所有号码均出现过</div>');
return;
}
var html = '<div style="display:flex;flex-wrap:wrap;gap:10px;">';
for (var i = 0; i < data.length; i++) {
var color = Controller.api.getColorByNum(data[i].num);
html += '<div style="text-align:center;">' +
'<span class="num-ball" style="display:inline-block;width:48px;height:48px;line-height:48px;text-align:center;border-radius:50%;color:#fff;background-color:' + color + ';font-weight:bold;font-size:18px;">' + data[i].num + '</span>' +
'<div style="margin-top:5px;font-size:12px;color:#666;">遗漏 ' + data[i].omit + ' 期</div>' +
'</div>';
}
html += '</div>';
$('#missing-result', layero).html(html);
}
i18n Language Strings
// application/admin/lang/zh-cn/history.php
return [
'Expect' => '期号',
'OpenTime' => '时间',
'Num7' => '特码',
'Missing Number Analysis' => '遗漏号码分析',
'Query Periods' => '查询期数',
'Missing' => '遗漏',
'periods' => '期',
];
State of the Art
| Old Approach | Current Approach | Impact |
|---|---|---|
| Custom modal HTML/CSS/JS | Layer.open({type: 1}) with inline content | Leverages existing dialog system |
| Raw $.ajax with manual response parsing | FastAdmin standard {code, msg, data} envelope | Consistent error handling |
| Hard-coded color map in JS | Reuse existing getColorByNum() from history.js |
No duplication, single source of truth |
| Bootstrap Table for display | Flex-wrapped grid with inline-styled balls | Simpler, more appropriate for ball display |
Outdated/avoided:
Layer.prompt(): Only supports a single text input. We need input + button + results area, so useLayer.open({type: 1})with custom HTML.Fast.api.open(): Opens iframe-based dialogs. Overkill for this use case since we don't need a separate view file.
Assumptions Log
| # | Claim | Section | Risk if Wrong |
|---|---|---|---|
| A1 | fa_history 表字段为 num1~num7,类型为字符串 |
Code Examples | 号码类型不匹配导致比较失败 |
| A2 | fa_history 按 openTime 降序排列可获取"最近 N 期" |
Code Examples | 如果 openTime 不是开奖时间字段,排序会错 |
| A3 | fa_num 表包含 1-49 的所有波色映射 | Code Examples | 波色显示会缺失 |
| A4 | FastAdmin admin 模块下自定义方法无需额外路由注册 | Architecture | URL 无法访问到方法 |
| A5 | protected $noNeedRight = ['*'] 可跳过权限检查 |
Pitfall 1 | AJAX 返回 403 |
Open Questions
-
遗漏期数的计算基准:OMIT-03 要求的"遗漏期数"是指"该号码最后一次出现距今多少期",还是"该号码在最近 X 期中没出现的期数"?当前方案采用前者(全局遗漏),这是彩票分析的标准定义。
- Recommendation: 使用全局遗漏期数(最后一次出现距今多少期),这是行业标准。
-
fa_num 表的波色数据完整性:是否确保 1-49 每个数字都有波色记录?
- What we know:
fa_num表有num和color字段,由 Num 控制器维护 - Recommendation: 前端对缺失波色的号码显示灰色兜底(
#95a5a6),已有此逻辑。
- What we know:
Environment Availability
| Dependency | Required By | Available | Version | Fallback |
|---|---|---|---|---|
| PHP | Backend endpoint | ✓ | >= 7.4.0 | — |
| MySQL (fa_history, fa_num) | Data query | ✓ | — | — |
| jQuery 3.7.1 | AJAX + DOM | ✓ | 3.7.1 | — |
| Layer 3.5.6 | Dialog | ✓ | 3.5.6 (npm: fastadmin-layer) | — |
| FastAdmin Backend trait | Controller base | ✓ | 1.6.2.20260323 | — |
All dependencies are available — no missing tools.
Validation Architecture
SKIPPED —
workflow.nyquist_validationis not configured in.planning/config.json, but no test infrastructure exists in the project (no PHPUnit, notests/directory). This is a code-only admin feature with no automated tests. Manual testing via browser will be required.
Security Domain
Applicable ASVS Categories
| ASVS Category | Applies | Standard Control |
|---|---|---|
| V2 Authentication | yes | FastAdmin admin session auth (inherited from Backend base) |
| V5 Input Validation | yes | PHP intval() for periods parameter, range check 1-100 |
| V7 Error Handling | yes | $this->error() for invalid input, JSON response |
Known Threat Patterns
| Pattern | STRIDE | Standard Mitigation |
|---|---|---|
| SQL Injection | Tampering | ThinkPHP ORM Db::name() with parameterized queries — no raw SQL concatenation |
| Parameter Tampering | Tampering | Input validation: intval(), range check `$periods < 1 |
| Unauthorized Access | Elevation of privilege | $noNeedRight (not $noNeedLogin) — admin login still required, just skip permission rule check |
Sources
Primary (HIGH confidence)
- [VERIFIED: Codebase]
application/admin/controller/History.php— existing controller structure - [VERIFIED: Codebase]
application/admin/model/History.php— existing model with$name = 'history' - [VERIFIED: Codebase]
public/assets/js/backend/history.js— existing JS withloadColorMap(),getColorByNum(),numBallformatter - [VERIFIED: Codebase]
application/admin/view/history/index.html— existing toolbar structure - [VERIFIED: Codebase]
application/admin/controller/Ajax.php— standard$this->success()/$this->error()AJAX response pattern - [VERIFIED: Codebase]
application/common/controller/Backend.php—$noNeedLogin,$noNeedRightproperties - [VERIFIED: Codebase]
public/assets/js/fast.js—Fast.api.ajax(),Fast.api.open(),Layerintegration - [VERIFIED: Codebase]
public/assets/js/backend/command.js—Layer.alert()with custom content pattern - [VERIFIED: Codebase]
public/assets/js/backend/general/config.js—Layer.prompt()usage pattern - [VERIFIED: npm registry]
fastadmin-layer@3.5.6,fastadmin-bootstraptable@1.11.12 - [VERIFIED: Codebase]
.planning/codebase/ARCHITECTURE.md— controller inheritance chain, RBAC auth
Secondary (MEDIUM confidence)
- [VERIFIED: Codebase]
application/admin/controller/Num.php—getColorMap()response format - [VERIFIED: Codebase]
.planning/codebase/STACK.md— PHP >= 7.4, ThinkPHP 5.x dev-master
Metadata
Confidence breakdown:
- Standard stack: HIGH — verified against installed npm packages and composer.json
- Architecture: HIGH — verified against existing codebase patterns
- Pitfalls: HIGH — derived from actual FastAdmin source code analysis
Research date: 2026-04-21 Valid until: 2026-07-21 (90 days — stable codebase, no fast-moving dependencies)