# 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)
