Files
amlhc/.planning/phases/01-omitted-number-analysis/01-RESEARCH.md
T
2026-04-21 23:02:15 +08:00

502 lines
22 KiB
Markdown
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.
# 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:**
```php
// 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:**
```javascript
// 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 use `Layer.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)
```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
```javascript
// 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();
});
```
```javascript
// 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
```php
// 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 use `Layer.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
1. **遗漏期数的计算基准**OMIT-03 要求的"遗漏期数"是指"该号码最后一次出现距今多少期",还是"该号码在最近 X 期中没出现的期数"?当前方案采用前者(全局遗漏),这是彩票分析的标准定义。
- Recommendation: 使用全局遗漏期数(最后一次出现距今多少期),这是行业标准。
2. **fa_num 表的波色数据完整性**:是否确保 1-49 每个数字都有波色记录?
- What we know: `fa_num` 表有 `num``color` 字段,由 Num 控制器维护
- Recommendation: 前端对缺失波色的号码显示灰色兜底(`#95a5a6`),已有此逻辑。
## 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_validation` is not configured in `.planning/config.json`, but no test infrastructure exists in the project (no PHPUnit, no `tests/` 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 || $periods > 100` |
| 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 with `loadColorMap()`, `getColorByNum()`, `numBall` formatter
- [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`, `$noNeedRight` properties
- [VERIFIED: Codebase] `public/assets/js/fast.js``Fast.api.ajax()`, `Fast.api.open()`, `Layer` integration
- [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)