Files
2026-04-21 23:02:15 +08:00

22 KiB
Raw Permalink Blame History

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

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

// 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 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 表有 numcolor 字段,由 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

SKIPPEDworkflow.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
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.jsFast.api.ajax(), Fast.api.open(), Layer integration
  • [VERIFIED: Codebase] public/assets/js/backend/command.jsLayer.alert() with custom content pattern
  • [VERIFIED: Codebase] public/assets/js/backend/general/config.jsLayer.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.phpgetColorMap() 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)