# 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: 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 = '
' + '
' + ' ' + ' ' + '
' + '' + '
' + '
'; 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(' 遗漏号码'); // 按钮点击事件 $(document).on('click', '.btn-missingnum', function () { Controller.api.showMissingNumDialog(); }); ``` ```javascript // public/assets/js/backend/history.js — inside Controller.api showMissingNumDialog: function () { var html = '
' + '
' + ' ' + ' ' + ' ' + '
' + '
' + '
'; 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('
查询中...
'); $.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('
' + ret.msg + '
'); } }, error: function () { $('#missing-result', layero).html('
请求失败
'); } }); }, renderMissingNum: function (data, layero) { if (!data || data.length === 0) { $('#missing-result', layero).html('
最近 ' + periods + ' 期内所有号码均出现过
'); return; } var html = '
'; for (var i = 0; i < data.length; i++) { var color = Controller.api.getColorByNum(data[i].num); html += '
' + '' + data[i].num + '' + '
遗漏 ' + data[i].omit + ' 期
' + '
'; } html += '
'; $('#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)