This commit is contained in:
2026-04-21 23:01:55 +08:00
commit 08e56caa72
597 changed files with 159445 additions and 0 deletions
@@ -0,0 +1,501 @@
# 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)