1
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
---
|
||||
phase: 01
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- application/admin/controller/History.php
|
||||
- application/admin/model/History.php
|
||||
- application/admin/lang/zh-cn/history.php
|
||||
autonomous: true
|
||||
requirements: [OMIT-04, OMIT-05]
|
||||
must_haves:
|
||||
truths:
|
||||
- "后端接口能接收 periods 参数并返回遗漏号码列表"
|
||||
- "返回数据包含每个遗漏号码的 num、omit(遗漏期数)、color(波色)"
|
||||
- "结果按遗漏期数 omit 从大到小排序"
|
||||
artifacts:
|
||||
- path: "application/admin/controller/History.php"
|
||||
provides: "missingNum() 控制器方法,处理 AJAX 请求"
|
||||
exports: ["missingNum"]
|
||||
- path: "application/admin/model/History.php"
|
||||
provides: "getMissingNumbers($periods) 模型方法,执行遗漏计算"
|
||||
exports: ["getMissingNumbers", "calcOmitCount"]
|
||||
- path: "application/admin/lang/zh-cn/history.php"
|
||||
provides: "遗漏号码相关 i18n 文本"
|
||||
contains: "'Missing Number Analysis'"
|
||||
key_links:
|
||||
- from: "application/admin/controller/History.php"
|
||||
to: "application/admin/model/History.php"
|
||||
via: "$this->model->getMissingNumbers($periods)"
|
||||
pattern: "getMissingNumbers"
|
||||
- from: "application/admin/model/History.php"
|
||||
to: "fa_history"
|
||||
via: "Db::name('history') 查询 num1~num7"
|
||||
pattern: "Db::name\\('history'\\)"
|
||||
- from: "application/admin/model/History.php"
|
||||
to: "fa_num"
|
||||
via: "Db::name('num') 查询波色映射"
|
||||
pattern: "Db::name\\('num'\\)"
|
||||
---
|
||||
|
||||
<objective>
|
||||
实现遗漏号码查询的后端逻辑:History 控制器新增 missingNum() AJAX 接口,History 模型新增 getMissingNumbers() 计算方法,支持查询最近 X 期开奖数据并计算 1-49 中未出现的号码及其遗漏期数和波色,结果按遗漏期数降序返回。
|
||||
|
||||
Purpose: 为前端弹窗提供遗漏号码数据源(per D-02: 遗漏计算在后端完成)
|
||||
Output: 可用的 history/missingNum AJAX 端点 + i18n 文本
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@D:/code/php/amlhc/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@D:/code/php/amlhc/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/01-omitted-number-analysis/01-RESEARCH.md
|
||||
@D:/code/php/amlhc/application/admin/controller/History.php
|
||||
@D:/code/php/amlhc/application/admin/model/History.php
|
||||
@D:/code/php/amlhc/application/admin/lang/zh-cn/history.php
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
|
||||
|
||||
From application/admin/controller/History.php:
|
||||
```php
|
||||
namespace app\admin\controller;
|
||||
use app\common\controller\Backend;
|
||||
class History extends Backend {
|
||||
protected $model = null;
|
||||
public function _initialize(); // sets $this->model = new \app\admin\model\History
|
||||
}
|
||||
// Inherits: $this->success($msg, $data), $this->error($msg) from Backend trait
|
||||
// Inherits: $this->request->isAjax(), $this->request->get() from think\Controller
|
||||
```
|
||||
|
||||
From application/admin/model/History.php:
|
||||
```php
|
||||
namespace app\admin\model;
|
||||
use think\Model;
|
||||
class History extends Model {
|
||||
protected $name = 'history'; // maps to fa_history table
|
||||
protected $autoWriteTimestamp = false;
|
||||
// Fields: expect (int, PK), num1~num7 (int), openTime (datetime)
|
||||
}
|
||||
```
|
||||
|
||||
From application/admin/controller/Num.php::getColorMap() pattern:
|
||||
```php
|
||||
public function getColorMap() {
|
||||
$list = $this->model->field('num,color')->select();
|
||||
$map = [];
|
||||
foreach ($list as $item) { $map[$item['num']] = $item['color']; }
|
||||
$this->success($map);
|
||||
}
|
||||
// Returns: {code: 1, msg: {"1":"红波","2":"蓝波",...}, data: null}
|
||||
```
|
||||
|
||||
Expected response format from missingNum():
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"msg": "查询成功",
|
||||
"data": [
|
||||
{"num": 7, "omit": 50, "color": "绿波"},
|
||||
{"num": 13, "omit": 32, "color": "红波"}
|
||||
]
|
||||
}
|
||||
```
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="false">
|
||||
<name>Task 1: 在 History 模型中添加 getMissingNumbers() 和 calcOmitCount() 方法</name>
|
||||
<files>application/admin/model/History.php</files>
|
||||
<read_first>
|
||||
- application/admin/model/History.php(当前模型结构)
|
||||
- application/admin/model/Num.php(参考波色查询模式)
|
||||
- sql/amlhc.sql line 471-482(fa_history 表结构,确认字段名和类型)
|
||||
</read_first>
|
||||
<action>
|
||||
在 application/admin/model/History.php 中添加两个方法:
|
||||
|
||||
1. `getMissingNumbers($periods = 10)` — 公共方法,计算遗漏号码:
|
||||
- 使用 `Db::name('history')->field('expect,num1,num2,num3,num4,num5,num6,num7')->order('openTime', 'desc')->limit($periods)->select()` 获取最近 $periods 期数据
|
||||
- 遍历结果,将出现的号码收集到 `$appeared` 数组(key 为号码 int,value 为 true)
|
||||
- 遍历 1-49,找出未在 `$appeared` 中的号码,收集到 `$missing` 数组
|
||||
- 查询更多历史数据用于计算遗漏期数:`Db::name('history')->field('num1,num2,num3,num4,num5,num6,num7')->order('openTime', 'desc')->limit(500)->select()`
|
||||
- 查询波色映射:`Db::name('num')->column('color', 'num')` 获取 `$colorMap`
|
||||
- 对每个遗漏号码调用 `calcOmitCount($num, $allHistory)` 计算遗漏期数
|
||||
- 组装结果:`['num' => $num, 'omit' => $omitCount, 'color' => $colorMap[$num] ?? '—']`
|
||||
- 使用 `usort($result, function($a, $b) { return $b['omit'] - $a['omit']; })` 按 omit 降序排序
|
||||
- 返回 `$result` 数组
|
||||
|
||||
2. `calcOmitCount($num, $allHistory)` — 私有方法,计算某个号码的遗漏期数:
|
||||
- 遍历 `$allHistory`(已按 openTime DESC 排序),对每行检查 num1~num7 是否等于 $num
|
||||
- 一旦找到,返回当前索引 $idx(即该号码最后一次出现距今多少期)
|
||||
- 如果遍历完都没找到,返回 `count($allHistory)`(表示 500 期内都未出现)
|
||||
|
||||
关键实现细节:
|
||||
- 必须使用 `use think\facade\Db;` 或 `\think\Db::name()` 来执行数据库查询(检查当前文件命名空间,如果模型已继承 think\Model,可直接用 `self::field(...)->select()` 或 `Db::name()`)
|
||||
- 号码比较必须用 `(int)` 转换,因为数据库返回的 num1~num7 是 int 类型(schema 显示为 int(11)),但要确保一致
|
||||
- $periods 参数范围校验:1-100,但模型层不校验(由控制器层校验),模型只负责计算
|
||||
- 波色缺失时返回 `'—'` 字符串作为兜底
|
||||
|
||||
添加 `use think\facade\Db;` 到文件顶部(如果尚未存在)。
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- application/admin/model/History.php 包含 `public function getMissingNumbers($periods = 10)`
|
||||
- application/admin/model/History.php 包含 `private function calcOmitCount($num, $allHistory)`
|
||||
- getMissingNumbers 方法体包含 `Db::name('history')` 或 `self::` 查询
|
||||
- getMissingNumbers 方法体包含 `Db::name('num')` 查询波色映射
|
||||
- getMissingNumbers 方法体包含 `usort` 按 omit 降序排序
|
||||
- 方法返回数组结构为 `[['num' => int, 'omit' => int, 'color' => string], ...]`
|
||||
- 文件顶部有 `use think\facade\Db;` 或使用 `\think\Db::` 完整命名空间
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>grep -c "getMissingNumbers\|calcOmitCount" application/admin/model/History.php | grep "2"</automated>
|
||||
</verify>
|
||||
<done>History 模型有 getMissingNumbers($periods) 和 calcOmitCount($num, $allHistory) 两个方法,能查询 fa_history 表计算遗漏号码并返回按遗漏期数降序的 [{num, omit, color}] 数组</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="false">
|
||||
<name>Task 2: 在 History 控制器中添加 missingNum() AJAX 接口方法</name>
|
||||
<files>application/admin/controller/History.php</files>
|
||||
<read_first>
|
||||
- application/admin/controller/History.php(当前控制器结构)
|
||||
- application/admin/controller/Num.php(参考 getColorMap 方法的响应模式)
|
||||
- application/common/controller/Backend.php(确认 $noNeedRight 用法)
|
||||
</read_first>
|
||||
<action>
|
||||
在 application/admin/controller/History.php 中添加 missingNum() 方法:
|
||||
|
||||
1. 在类中添加权限控制属性(在 _initialize() 方法上方或下方):
|
||||
```php
|
||||
// 无需额外权限检查(但仍在 admin 模块内,需要 admin 登录)
|
||||
protected $noNeedRight = ['missingNum'];
|
||||
```
|
||||
|
||||
2. 添加 missingNum() 方法:
|
||||
```php
|
||||
/**
|
||||
* 查询遗漏号码
|
||||
* @return void
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
关键实现细节:
|
||||
- 使用 `$this->request->isAjax()` 判断是否为 AJAX 请求
|
||||
- 使用 `$this->request->get('periods', 10, 'intval')` 获取参数,默认值 10,强制转为 int
|
||||
- 参数校验:$periods < 1 或 > 100 时调用 `$this->error()` 返回错误
|
||||
- 调用 `$this->model->getMissingNumbers($periods)` 获取结果
|
||||
- 使用 `$this->success('查询成功', $result)` 返回标准 FastAdmin 响应格式 `{code: 1, msg: '查询成功', data: [...]}`
|
||||
- 方法无返回值(void),通过 $this->success/error 输出 JSON
|
||||
|
||||
不要使用 `protected $noNeedRight = ['*']`,只放开 missingNum 一个方法即可(最小权限原则)。
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- application/admin/controller/History.php 包含 `protected $noNeedRight = ['missingNum']`
|
||||
- application/admin/controller/History.php 包含 `public function missingNum()`
|
||||
- missingNum 方法体包含 `$this->request->isAjax()` 判断
|
||||
- missingNum 方法体包含 `$this->request->get('periods', 10, 'intval')`
|
||||
- missingNum 方法体包含 `$periods < 1 || $periods > 100` 范围校验
|
||||
- missingNum 方法体包含 `$this->model->getMissingNumbers($periods)` 调用
|
||||
- missingNum 方法体包含 `$this->success('查询成功', $result)` 响应
|
||||
- missingNum 方法体包含 `$this->error(...)` 错误响应
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>grep -c "missingNum\|noNeedRight" application/admin/controller/History.php | awk '$1 >= 2'</automated>
|
||||
</verify>
|
||||
<done>History 控制器有 missingNum() 方法,接受 AJAX GET 请求,校验 periods 参数 1-100,调用模型 getMissingNumbers() 返回标准 JSON 响应</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="false">
|
||||
<name>Task 3: 添加遗漏号码相关 i18n 文本到语言文件</name>
|
||||
<files>application/admin/lang/zh-cn/history.php</files>
|
||||
<read_first>
|
||||
- application/admin/lang/zh-cn/history.php(当前语言文件)
|
||||
- application/admin/lang/zh-cn/num.php(参考格式,如果存在)
|
||||
</read_first>
|
||||
<action>
|
||||
修改 application/admin/lang/zh-cn/history.php,在现有返回值数组中添加遗漏号码相关的语言键:
|
||||
|
||||
当前内容:
|
||||
```php
|
||||
return [
|
||||
'Expect' => '期号',
|
||||
'OpenTime' => '时间',
|
||||
'Num7' => '特码',
|
||||
];
|
||||
```
|
||||
|
||||
修改为:
|
||||
```php
|
||||
return [
|
||||
'Expect' => '期号',
|
||||
'OpenTime' => '时间',
|
||||
'Num7' => '特码',
|
||||
'Missing Number Analysis' => '遗漏号码分析',
|
||||
'Query Periods' => '查询期数',
|
||||
'Missing' => '遗漏',
|
||||
'periods' => '期',
|
||||
'No missing numbers found' => '最近所有号码均出现过',
|
||||
'Query failed' => '查询失败',
|
||||
'Loading' => '查询中...',
|
||||
];
|
||||
```
|
||||
|
||||
保持与现有格式一致:单引号包裹 key 和 value,逗号结尾,缩进 4 空格。
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- application/admin/lang/zh-cn/history.php 包含键 'Missing Number Analysis',值为 '遗漏号码分析'
|
||||
- application/admin/lang/zh-cn/history.php 包含键 'Query Periods',值为 '查询期数'
|
||||
- application/admin/lang/zh-cn/history.php 包含键 'Missing',值为 '遗漏'
|
||||
- application/admin/lang/zh-cn/history.php 包含键 'periods',值为 '期'
|
||||
- 文件格式保持 `return [ ... ];` 结构,使用单引号
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>grep -c "Missing Number Analysis\|Query Periods\|Missing" application/admin/lang/zh-cn/history.php | awk '$1 >= 3'</automated>
|
||||
</verify>
|
||||
<done>语言文件包含遗漏号码相关的所有中文翻译键值对</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Browser → Controller (AJAX GET) | 用户输入 periods 参数,可能注入非数值 |
|
||||
| Controller → Model | 内部调用,信任已建立 |
|
||||
| Model → Database (fa_history, fa_num) | ORM 查询,无原始 SQL 拼接 |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-01-01 | Tampering | History::missingNum() - periods 参数 | mitigate | 使用 `$this->request->get('periods', 10, 'intval')` 强制类型转换 + 范围校验 `$periods < 1 || $periods > 100` |
|
||||
| T-01-02 | Tampering | History::getMissingNumbers() - SQL 查询 | mitigate | 使用 ThinkPHP ORM `Db::name()` 参数化查询,无原始 SQL 字符串拼接 |
|
||||
| T-01-03 | Elevation of Privilege | History::missingNum() 权限绕过 | mitigate | 使用 `$noNeedRight = ['missingNum']`(非 `$noNeedLogin`),admin 登录仍然必需,仅跳过权限规则检查 |
|
||||
| T-01-04 | Denial of Service | 超大 periods 值导致查询慢 | mitigate | 参数上限 100,模型层 LIMIT 500 查询历史,确保 O(500*7) 时间复杂度可接受 |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- 通过浏览器访问 admin history 页面,在地址栏直接请求 `history/missingNum?periods=10`,返回 JSON 且 code=1,data 为非空数组
|
||||
- 请求 `history/missingNum?periods=0` 返回错误(code!=1)
|
||||
- 请求 `history/missingNum?periods=200` 返回错误(code!=1)
|
||||
- 返回数据中每个元素包含 num(int 1-49)、omit(int >=0)、color(string)
|
||||
- 返回数组按 omit 字段降序排列
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] History 模型有 getMissingNumbers() 和 calcOmitCount() 方法
|
||||
- [ ] History 控制器有 missingNum() AJAX 端点,带 periods 参数校验
|
||||
- [ ] 请求返回标准 FastAdmin 格式 {code: 1, msg: '查询成功', data: [{num, omit, color}, ...]}
|
||||
- [ ] 结果按遗漏期数 omit 从大到小排序
|
||||
- [ ] 波色映射从 fa_num 表获取,缺失时显示 '—'
|
||||
- [ ] 语言文件包含遗漏号码相关 i18n 键值对
|
||||
- [ ] missingNum 方法仅需 admin 登录即可访问($noNeedRight 非 $noNeedLogin)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-omitted-number-analysis/01-01-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
phase: 01
|
||||
plan: 01
|
||||
subsystem: admin
|
||||
tags: [lottery, missing-number, backend]
|
||||
dependency_graph:
|
||||
requires: []
|
||||
provides: [missingNum endpoint, getMissingNumbers model method, i18n keys]
|
||||
affects: [application/admin/model/History.php, application/admin/controller/History.php, application/admin/lang/zh-cn/history.php]
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [FastAdmin Backend controller, ThinkPHP 5.x Model, Db facade]
|
||||
key-files:
|
||||
created:
|
||||
- application/admin/model/History.php
|
||||
- application/admin/controller/History.php
|
||||
- application/admin/lang/zh-cn/history.php
|
||||
modified: []
|
||||
decisions:
|
||||
- Used $noNeedRight = ['missingNum'] instead of ['*'] for minimal permission bypass
|
||||
- Model queries up to 500 historical records for true omission count calculation
|
||||
- Color fallback uses '—' string when fa_num table has no mapping for a number
|
||||
metrics:
|
||||
duration: ~5min
|
||||
completed: "2026-04-21"
|
||||
---
|
||||
|
||||
# Phase 01 Plan 01: Backend Missing Number Logic Summary
|
||||
|
||||
Backend missing number calculation logic and AJAX endpoint — History model with `getMissingNumbers()` + `calcOmitCount()` methods, History controller with `missingNum()` endpoint, and i18n language keys for the missing number feature.
|
||||
|
||||
## Tasks Completed
|
||||
|
||||
| # | Task | Commit | Files |
|
||||
|---|------|--------|-------|
|
||||
| 1 | Add getMissingNumbers() and calcOmitCount() to History model | 6386a40 | application/admin/model/History.php |
|
||||
| 2 | Add missingNum() AJAX endpoint to History controller | 15bb870 | application/admin/controller/History.php |
|
||||
| 3 | Add i18n text keys to language file | 96d5e78 | application/admin/lang/zh-cn/history.php |
|
||||
|
||||
## One-liner
|
||||
|
||||
History model `getMissingNumbers()` computes missing lottery numbers (1-49) with true omission counts from up to 500 historical records, controller `missingNum()` validates periods (1-100) and returns JSON, i18n file provides Chinese translations.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
- **$noNeedRight minimal scope**: Only `missingNum` method is exempted from permission checks (not `['*']`), following least-privilege principle.
|
||||
- **500-record limit for omission calculation**: Queries up to 500 historical records to calculate true omission counts (last appearance distance), not just "appeared/not appeared" in the query window.
|
||||
- **Color fallback**: Returns `'—'` when fa_num table lacks a wave color mapping for a given number.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None - plan executed exactly as written.
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
None beyond what was identified in the plan's threat model (T-01-01 through T-01-04).
|
||||
|
||||
## Self-Check
|
||||
|
||||
- Model file exists with both methods: PASS
|
||||
- Controller file exists with missingNum endpoint: PASS
|
||||
- Lang file exists with all i18n keys: PASS
|
||||
- All 3 commits present in git log: PASS
|
||||
|
||||
## Self-Check: PASSED
|
||||
@@ -0,0 +1,355 @@
|
||||
---
|
||||
phase: 01
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- public/assets/js/backend/history.js
|
||||
- application/admin/view/history/index.html
|
||||
autonomous: true
|
||||
requirements: [OMIT-01, OMIT-02, OMIT-03]
|
||||
must_haves:
|
||||
truths:
|
||||
- "用户在 history 页面能看到'遗漏号码'按钮"
|
||||
- "点击按钮后弹出 Layer 模态窗口"
|
||||
- "弹窗内有期数输入框(默认值 10)和查询按钮"
|
||||
- "点击查询后显示结果区域,包含遗漏号码、遗漏期数、波色球"
|
||||
artifacts:
|
||||
- path: "application/admin/view/history/index.html"
|
||||
provides: "history 页面 toolbar 区域的'遗漏号码'按钮"
|
||||
contains: "btn-missingnum"
|
||||
- path: "public/assets/js/backend/history.js"
|
||||
provides: "按钮点击处理、Layer 弹窗、AJAX 请求、结果渲染"
|
||||
exports: ["showMissingNumDialog", "queryMissingNum", "renderMissingNum"]
|
||||
key_links:
|
||||
- from: "application/admin/view/history/index.html"
|
||||
to: "public/assets/js/backend/history.js"
|
||||
via: "toolbar 按钮 class .btn-missingnum 绑定 click 事件"
|
||||
pattern: "btn-missingnum"
|
||||
- from: "public/assets/js/backend/history.js"
|
||||
to: "history/missingNum"
|
||||
via: "$.ajax 请求后端接口"
|
||||
pattern: "history/missingNum"
|
||||
- from: "public/assets/js/backend/history.js"
|
||||
to: "Controller.api.getColorByNum()"
|
||||
via: "渲染波色球时复用已有颜色函数"
|
||||
pattern: "getColorByNum"
|
||||
---
|
||||
|
||||
<objective>
|
||||
在 history 页面添加"遗漏号码"按钮和 Layer 弹窗 UI,包含期数输入框、查询按钮、结果展示区域和波色球渲染,复用已有的 getColorByNum() 颜色函数。
|
||||
|
||||
Purpose: 提供用户交互界面(per D-01: 按钮+弹窗形式,不新增独立页面/菜单;per D-03: Layer 弹窗展示)
|
||||
Output: toolbar 按钮 + Layer 弹窗 HTML + 结果渲染逻辑
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@D:/code/php/amlhc/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@D:/code/php/amlhc/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/01-omitted-number-analysis/01-RESEARCH.md
|
||||
@D:/code/php/amlhc/public/assets/js/backend/history.js
|
||||
@D:/code/php/amlhc/application/admin/view/history/index.html
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
|
||||
|
||||
From public/assets/js/backend/history.js:
|
||||
```javascript
|
||||
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
|
||||
var Controller = {
|
||||
index: function () { ... },
|
||||
add: function () { ... },
|
||||
edit: function () { ... },
|
||||
api: {
|
||||
colorMap: {}, // 波色映射缓存 {num: colorText}
|
||||
colorMapLoaded: false, // 是否已加载
|
||||
loadColorMap: function (callback) { ... }, // 异步加载,完成后调用 callback
|
||||
getColorByNum: function (num) { ... }, // 返回 CSS 颜色值字符串
|
||||
formatter: {
|
||||
numBall: function (value, row, index) { ... } // 表格内号码球渲染
|
||||
},
|
||||
bindevent: function () { ... }
|
||||
}
|
||||
};
|
||||
return Controller;
|
||||
});
|
||||
```
|
||||
|
||||
From application/admin/view/history/index.html:
|
||||
- toolbar 区域 id="toolbar",已有刷新按钮
|
||||
- 使用 Bootstrap 3 样式类:btn btn-primary, form-control, text-center, text-danger 等
|
||||
- Layer 对象全局可用(通过 requirejs 加载 fastadmin-layer)
|
||||
|
||||
FastAdmin AJAX response format:
|
||||
```json
|
||||
{ "code": 1, "msg": "查询成功", "data": [...] } // success
|
||||
{ "code": 0, "msg": "错误信息" } // error
|
||||
```
|
||||
code === 1 表示成功。
|
||||
|
||||
Layer.open() API:
|
||||
```javascript
|
||||
Layer.open({
|
||||
type: 1, // 1 = page 类型,使用 content 中的 HTML
|
||||
title: '标题',
|
||||
area: ['650px', '550px'], // [width, height]
|
||||
content: html, // HTML 字符串
|
||||
shadeClose: true // 点击遮罩关闭
|
||||
});
|
||||
```
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="false">
|
||||
<name>Task 1: 在 history 页面 toolbar 添加"遗漏号码"按钮</name>
|
||||
<files>application/admin/view/history/index.html</files>
|
||||
<read_first>
|
||||
- application/admin/view/history/index.html(当前 toolbar 结构)
|
||||
</read_first>
|
||||
<action>
|
||||
在 application/admin/view/history/index.html 的 toolbar 区域(id="toolbar")中添加"遗漏号码"按钮。
|
||||
|
||||
当前 toolbar 内容(第 8-11 行):
|
||||
```html
|
||||
<div id="toolbar" class="toolbar">
|
||||
<a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
|
||||
<!-- <a href="javascript:;" class="btn btn-success btn-add ...
|
||||
</div>
|
||||
```
|
||||
|
||||
在刷新按钮之后、注释的添加按钮之前,插入遗漏号码按钮:
|
||||
```html
|
||||
<a href="javascript:;" class="btn btn-warning btn-missingnum" title="{:__('Missing Number Analysis')}"><i class="fa fa-search"></i> {:__('Missing Number Analysis')}</a>
|
||||
```
|
||||
|
||||
完整 toolbar 变为:
|
||||
```html
|
||||
<div id="toolbar" class="toolbar">
|
||||
<a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
|
||||
<a href="javascript:;" class="btn btn-warning btn-missingnum" title="{:__('Missing Number Analysis')}"><i class="fa fa-search"></i> {:__('Missing Number Analysis')}</a>
|
||||
<!-- <a href="javascript:;" class="btn btn-success btn-add {:$auth->check('history/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>-->
|
||||
</div>
|
||||
```
|
||||
|
||||
关键实现细节:
|
||||
- 使用 `btn-warning` 样式(黄色按钮,区别于刷新按钮的蓝色)
|
||||
- class 必须包含 `btn-missingnum`(JS 事件绑定选择器)
|
||||
- 图标使用 `fa fa-search`(搜索图标,符合查询语义)
|
||||
- 文本使用 `{:__('Missing Number Analysis')}` 通过 i18n 函数获取(对应 plan 01 task 3 添加的语言键)
|
||||
- title 属性同样使用 i18n
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- application/admin/view/history/index.html 的 toolbar 中包含 class="btn-missingnum" 的 <a> 元素
|
||||
- 按钮 class 包含 btn-warning
|
||||
- 按钮包含 {:__('Missing Number Analysis')} 文本调用
|
||||
- 按钮图标 class 为 fa fa-search
|
||||
- 按钮位于 btn-refresh 之后
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>grep "btn-missingnum" application/admin/view/history/index.html</automated>
|
||||
</verify>
|
||||
<done>history 页面 toolbar 出现黄色"遗漏号码"按钮,使用 i18n 文本和搜索图标</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="false">
|
||||
<name>Task 2: 在 history.js 中添加按钮事件绑定、Layer 弹窗和结果渲染逻辑</name>
|
||||
<files>public/assets/js/backend/history.js</files>
|
||||
<read_first>
|
||||
- public/assets/js/backend/history.js(当前 JS 结构,特别是 Controller.api 对象)
|
||||
- public/assets/js/backend/command.js(参考 Layer.alert / Layer.open 使用模式)
|
||||
- public/assets/js/backend/general/config.js(参考 Layer 弹窗内 HTML + 事件绑定模式)
|
||||
</read_first>
|
||||
<action>
|
||||
在 public/assets/js/backend/history.js 中添加遗漏号码弹窗相关代码。
|
||||
|
||||
**位置 1:在 Controller.index() 函数内,Table.api.bindevent(table) 调用之后,添加按钮事件绑定:**
|
||||
|
||||
在 `Table.api.bindevent(table);` 这一行之后添加:
|
||||
```javascript
|
||||
// 遗漏号码按钮事件
|
||||
$(document).off('click', '.btn-missingnum').on('click', '.btn-missingnum', function () {
|
||||
Controller.api.showMissingNumDialog();
|
||||
});
|
||||
```
|
||||
使用 `off().on()` 防止重复绑定(FastAdmin 在 tab 切换时可能重复初始化)。
|
||||
|
||||
**位置 2:在 Controller.api 对象中添加三个新方法(在 bindevent 方法之前):**
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* 显示遗漏号码分析弹窗
|
||||
*/
|
||||
showMissingNumDialog: function () {
|
||||
var html = '<div style="padding:20px;">' +
|
||||
'<div class="form-group">' +
|
||||
' <label>' + __('Query Periods') + ':</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> ' + __('Query') + '</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) {
|
||||
// 确保颜色映射已加载
|
||||
if (!Controller.api.colorMapLoaded) {
|
||||
Controller.api.loadColorMap(function () {
|
||||
Controller.api._doQueryMissingNum(periods, layero);
|
||||
});
|
||||
} else {
|
||||
Controller.api._doQueryMissingNum(periods, layero);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行遗漏号码查询(内部方法)
|
||||
*/
|
||||
_doQueryMissingNum: function (periods, layero) {
|
||||
$('#missing-result', layero).html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> ' + __('Loading') + '</div>');
|
||||
$.ajax({
|
||||
url: 'history/missingNum',
|
||||
type: 'GET',
|
||||
data: { periods: periods },
|
||||
dataType: 'json',
|
||||
success: function (ret) {
|
||||
if (ret.code == 1) {
|
||||
Controller.api.renderMissingNum(ret.data, periods, layero);
|
||||
} else {
|
||||
$('#missing-result', layero).html('<div class="alert alert-danger">' + (ret.msg || __('Query failed')) + '</div>');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
$('#missing-result', layero).html('<div class="alert alert-danger">' + __('Query failed') + '</div>');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 渲染遗漏号码结果
|
||||
*/
|
||||
renderMissingNum: function (data, periods, layero) {
|
||||
if (!data || data.length === 0) {
|
||||
$('#missing-result', layero).html('<div class="alert alert-info">' + __('No missing numbers found') + '</div>');
|
||||
return;
|
||||
}
|
||||
var html = '<div style="display:flex;flex-wrap:wrap;gap:12px;">';
|
||||
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;">' + __('Missing') + ' ' + data[i].omit + ' ' + __('periods') + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
$('#missing-result', layero).html(html);
|
||||
},
|
||||
```
|
||||
|
||||
关键实现细节:
|
||||
- 弹窗 HTML 使用 Bootstrap 3 form-group 和 form-control 样式
|
||||
- 输入框 type="number",默认值 10,min=1,max=100,宽度 120px 内联显示
|
||||
- 查询按钮在输入框右侧,margin-left:10px
|
||||
- 结果区域 id="missing-result",初始为空
|
||||
- Layer.open 使用 type:1(page 类型),area: ['650px', '550px'],shadeClose:true(点击遮罩关闭)
|
||||
- success 回调中绑定查询按钮事件,使用 layero 作为上下文选择器根(`$('#btn-missing-query', layero)`)
|
||||
- queryMissingNum 先检查 colorMapLoaded,未加载则先调用 loadColorMap
|
||||
- AJAX 使用标准 $.ajax,url 为 'history/missingNum',type 为 'GET'
|
||||
- 响应判断 `ret.code == 1`(FastAdmin 成功标志)
|
||||
- 加载状态显示 spinner + 文字
|
||||
- 渲染时使用 flex 布局(display:flex; flex-wrap:wrap; gap:12px)展示球网格
|
||||
- 每个球 48x48px,圆角 50%,白色文字,背景色来自 getColorByNum()
|
||||
- 球下方显示"遗漏 X 期"文字,12px 灰色字体
|
||||
- 所有文本使用 __('key') 获取 i18n
|
||||
- 空结果时显示 alert-info 提示
|
||||
|
||||
不要修改现有的 Controller.index、Controller.add、Controller.edit 方法签名。只在 index 函数内追加按钮事件绑定,在 api 对象内追加新方法。
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- public/assets/js/backend/history.js 的 Controller.index() 中包含 `$(document).off('click', '.btn-missingnum').on('click', '.btn-missingnum'`
|
||||
- Controller.api 对象包含 `showMissingNumDialog` 方法
|
||||
- Controller.api 对象包含 `queryMissingNum` 方法
|
||||
- Controller.api 对象包含 `_doQueryMissingNum` 方法
|
||||
- Controller.api 对象包含 `renderMissingNum` 方法
|
||||
- showMissingNumDialog 调用 Layer.open({type: 1, ...})
|
||||
- Layer.open 的 content 包含 input type="number" id="missing-periods"
|
||||
- queryMissingNum 检查 colorMapLoaded 状态
|
||||
- _doQueryMissingNum 使用 $.ajax 请求 url: 'history/missingNum'
|
||||
- renderMissingNum 使用 flex-wrap 布局渲染球网格
|
||||
- renderMissingNum 调用 Controller.api.getColorByNum() 获取颜色
|
||||
- 结果球显示数字 + 下方"遗漏 X 期"文字
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>grep -c "showMissingNumDialog\|queryMissingNum\|renderMissingNum\|btn-missingnum" public/assets/js/backend/history.js | awk '$1 >= 4'</automated>
|
||||
</verify>
|
||||
<done>history.js 有完整的遗漏号码按钮处理、Layer 弹窗(含期数输入和查询按钮)、AJAX 请求、结果渲染(波色球+遗漏期数)逻辑</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| User input (number field) → AJAX request | 用户可能在浏览器开发者工具中修改 periods 值 |
|
||||
| Layer dialog HTML injection | 弹窗 HTML 由 JS 拼接,无外部输入注入风险 |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-01-05 | Tampering | 前端 periods 输入值 | mitigate | 后端已有 range 校验 1-100;前端 HTML input min/max 为 UX 辅助,不依赖前端校验 |
|
||||
| T-01-06 | Information Disclosure | Layer 弹窗内容 | accept | 仅展示遗漏号码统计数据,无敏感信息 |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- 在浏览器中访问 admin history 页面,确认 toolbar 出现黄色"遗漏号码"按钮
|
||||
- 点击按钮,确认 Layer 弹窗打开,包含期数输入框(默认 10)和查询按钮
|
||||
- 输入 10 点击查询,确认出现加载状态 → 结果网格(带颜色的球 + 遗漏期数文字)
|
||||
- 输入 0 或 200 点击查询,确认后端返回错误提示
|
||||
- 确认结果球颜色与 history 表格中的波色球一致(复用 getColorByNum)
|
||||
- 确认结果按遗漏期数从大到小排列(视觉上最大 omit 的球在最左)
|
||||
- 点击弹窗遮罩,确认弹窗关闭
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] history 页面 toolbar 有"遗漏号码"按钮(btn-warning 样式)
|
||||
- [ ] 点击按钮弹出 Layer 弹窗,标题为"遗漏号码分析"
|
||||
- [ ] 弹窗内有期数输入框(默认值 10,范围 1-100)和查询按钮
|
||||
- [ ] 点击查询后显示加载状态,然后显示结果
|
||||
- [ ] 结果以 flex 网格展示,每个球显示号码、波色、遗漏期数
|
||||
- [ ] 波色球颜色与表格中一致(复用 getColorByNum)
|
||||
- [ ] 无遗漏号码时显示友好提示
|
||||
- [ ] 点击遮罩可关闭弹窗
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-omitted-number-analysis/01-02-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,126 @@
|
||||
---
|
||||
phase: 01-omitted-number-analysis
|
||||
plan: 02
|
||||
subsystem: ui
|
||||
tags: [layer, bootstrap, requirejs, jquery, fastadmin, thinkphp]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 01-omitted-number-analysis
|
||||
provides: 01-01 backend missingNum endpoint (parallel wave, to be verified)
|
||||
provides:
|
||||
- History page toolbar "Missing Number Analysis" button
|
||||
- Layer dialog with period input (default 10, range 1-100) and query button
|
||||
- AJAX integration to history/missingNum endpoint
|
||||
- Result rendering with colored num-balls and omission count display
|
||||
- Reuse of existing getColorByNum() for color consistency
|
||||
affects: [01-03 integration verification, future omission trend analysis]
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Layer.open({type:1}) with inline HTML for custom dialogs"
|
||||
- "jQuery $(document).off().on() for delegated event binding to prevent duplicates"
|
||||
- "Async color map loading guard before rendering colored elements"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- application/admin/view/history/index.html (added button to existing toolbar)
|
||||
modified:
|
||||
- public/assets/js/backend/history.js (added 4 new API methods + button handler)
|
||||
- application/admin/lang/zh-cn/history.php (added i18n keys for dialog text)
|
||||
|
||||
key-decisions:
|
||||
- "Used Layer.open type:1 with inline HTML instead of Layer.prompt or Fast.api.open (needs input + button + results area)"
|
||||
- "Delegated event binding on document with .off().on() to prevent duplicate handlers on tab re-initialization"
|
||||
- "queryMissingNum checks colorMapLoaded before AJAX to ensure balls render with correct colors"
|
||||
- "All text uses __() i18n function with keys in lang/zh-cn/history.php"
|
||||
|
||||
patterns-established:
|
||||
- "Dialog pattern: Layer.open with success callback for binding events within layero context"
|
||||
- "AJAX pattern: GET request to controller method, check ret.code==1 for success"
|
||||
- "Rendering pattern: flex-wrap grid with inline-styled num-ball components"
|
||||
|
||||
requirements-completed: [OMIT-01, OMIT-02, OMIT-03]
|
||||
|
||||
# Metrics
|
||||
duration: 5min
|
||||
completed: 2026-04-21
|
||||
---
|
||||
|
||||
# Phase 01 Plan 02: History Toolbar Button + Layer Dialog UI Summary
|
||||
|
||||
**Missing number analysis button + Layer dialog with period input, AJAX query, and colored ball result rendering on history admin page**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~5 min
|
||||
- **Started:** 2026-04-21T13:06:00Z
|
||||
- **Completed:** 2026-04-21T13:11:00Z
|
||||
- **Tasks:** 2
|
||||
- **Files modified:** 3 (1 toolbar, 1 JS, 1 lang)
|
||||
|
||||
## Accomplishments
|
||||
- Added "Missing Number Analysis" button (btn-warning, fa-search icon) to history page toolbar
|
||||
- Implemented Layer dialog with period number input (default 10, range 1-100) and query button
|
||||
- Built AJAX integration to `history/missingNum` endpoint with loading spinner and error handling
|
||||
- Implemented flex-wrap grid rendering of missing numbers as colored balls with omission count labels
|
||||
- Added all required i18n keys for dialog text in Chinese
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Task 1: Add "遗漏号码" button to history page toolbar** - `4104746` (feat)
|
||||
2. **Task 2: Button event binding, Layer dialog, and result rendering logic** - `538e414` (feat)
|
||||
3. **Deviation: Add missing i18n language keys** - `637b847` (i18n)
|
||||
|
||||
## Files Created/Modified
|
||||
- `D:/code/php/amlhc/.claude/worktrees/agent-a5a02f12/application/admin/view/history/index.html` - Added btn-missingnum button to toolbar
|
||||
- `D:/code/php/amlhc/.claude/worktrees/agent-a5a02f12/public/assets/js/backend/history.js` - Added showMissingNumDialog, queryMissingNum, _doQueryMissingNum, renderMissingNum methods
|
||||
- `D:/code/php/amlhc/.claude/worktrees/agent-a5a02f12/application/admin/lang/zh-cn/history.php` - Added 8 i18n keys for dialog text
|
||||
|
||||
## Decisions Made
|
||||
- Used `$(document).off('click', '.btn-missingnum').on('click', '.btn-missingnum', ...)` instead of `$('#toolbar').on('click', '.btn-missingnum', ...)` to prevent duplicate binding when FastAdmin reinitializes on tab switches
|
||||
- Checked `colorMapLoaded` in `queryMissingNum` before calling `_doQueryMissingNum` to ensure balls render with correct colors even if color map hasn't been loaded yet
|
||||
- All display text uses `__('key')` i18n function rather than hardcoded Chinese strings, matching project convention
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 2 - Missing Critical] Added i18n keys for dialog text**
|
||||
- **Found during:** Task 2 (Layer dialog implementation)
|
||||
- **Issue:** The lang/zh-cn/history.php file only had Expect, OpenTime, Num7 keys. The dialog uses __() calls for 8 additional keys (Missing Number Analysis, Query Periods, Query, Loading, Missing, periods, Query failed, No missing numbers found) which would display as raw English keys instead of Chinese text
|
||||
- **Fix:** Added all 8 i18n keys to application/admin/lang/zh-cn/history.php
|
||||
- **Files modified:** application/admin/lang/zh-cn/history.php
|
||||
- **Verification:** Verified lang file contains all keys referenced by __() calls in history.js
|
||||
- **Committed in:** `637b847` (separate commit)
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 1 auto-fixed (1 missing critical - i18n)
|
||||
**Impact on plan:** Essential for correct display of Chinese text in dialog. No scope creep.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
- Worktree missing base files: The worktree was created from commit `e1cb014` which only contains planning files. All existing application code (controller, model, view, lang, JS) was untracked in the main repo and had to be copied into the worktree before modifications could be committed. Resolved by copying from the main repo's working directory.
|
||||
|
||||
## Threat Surface Scan
|
||||
|
||||
| Flag | File | Description |
|
||||
|------|------|-------------|
|
||||
| threat_flag: XSS | public/assets/js/backend/history.js | Dialog HTML is JS-concatenated with no external input; safe. AJAX response data displayed directly — backend should sanitize (plan 01-01 responsibility) |
|
||||
|
||||
## Known Stubs
|
||||
None. This plan delivers UI only; data rendering depends on backend endpoint from plan 01-01.
|
||||
|
||||
## Next Phase Readiness
|
||||
- Frontend UI complete and ready for integration with backend `missingNum` endpoint (plan 01-01)
|
||||
- Requires plan 01-01 to provide `History::missingNum()` controller method with proper input validation (periods 1-100)
|
||||
- Plan 01-03 will verify end-to-end integration (button -> dialog -> AJAX -> backend -> result display)
|
||||
|
||||
---
|
||||
*Phase: 01-omitted-number-analysis*
|
||||
*Completed: 2026-04-21*
|
||||
@@ -0,0 +1,218 @@
|
||||
---
|
||||
phase: 01
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: [01-01, 01-02]
|
||||
files_modified:
|
||||
- public/assets/js/backend/history.js
|
||||
autonomous: false
|
||||
requirements: [OMIT-02, OMIT-03, OMIT-04]
|
||||
must_haves:
|
||||
truths:
|
||||
- "前后端联调通过:前端请求能正确到达后端接口并获取数据"
|
||||
- "结果按遗漏期数从大到小正确渲染"
|
||||
- "波色球着色与已有颜色映射一致"
|
||||
artifacts:
|
||||
- path: "public/assets/js/backend/history.js"
|
||||
provides: "联调验证逻辑和边界情况处理"
|
||||
contains: "history/missingNum"
|
||||
key_links:
|
||||
- from: "public/assets/js/backend/history.js"
|
||||
to: "application/admin/controller/History.php::missingNum()"
|
||||
via: "$.ajax GET history/missingNum?periods=X"
|
||||
pattern: "history/missingNum"
|
||||
- from: "public/assets/js/backend/history.js::renderMissingNum"
|
||||
to: "后端返回 data[].omit"
|
||||
via: "按 omit 降序渲染(后端已排序)"
|
||||
pattern: "data\\[i\\]\\.omit"
|
||||
---
|
||||
|
||||
<objective>
|
||||
前后端联调验证:确保 history.js 的 AJAX 请求正确调用 history/missingNum 接口,结果按遗漏期数降序渲染,波色球着色与已有映射一致,并处理边界情况(无数据、加载失败、颜色映射未就绪)。
|
||||
|
||||
Purpose: 验证完整功能链路(per D-03: $.ajax 请求遗漏接口)
|
||||
Output: 联调验证通过,边界情况处理完善
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@D:/code/php/amlhc/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@D:/code/php/amlhc/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/01-omitted-number-analysis/01-RESEARCH.md
|
||||
@D:/code/php/amlhc/public/assets/js/backend/history.js(plan 01-02 修改后的版本)
|
||||
@D:/code/php/amlhc/application/admin/controller/History.php(plan 01-01 修改后的版本)
|
||||
@D:/code/php/amlhc/application/admin/model/History.php(plan 01-01 修改后的版本)
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts from plans 01-01 and 01-02. -->
|
||||
|
||||
Backend endpoint (from plan 01-01):
|
||||
```
|
||||
GET /admin/history/missingNum?periods=X
|
||||
Response success: {code: 1, msg: "查询成功", data: [{num: int, omit: int, color: string}, ...]}
|
||||
Response error: {code: 0, msg: "期数范围必须在 1-100 之间"}
|
||||
```
|
||||
|
||||
Frontend API (from plan 01-02):
|
||||
```javascript
|
||||
Controller.api.showMissingNumDialog() // opens Layer dialog
|
||||
Controller.api.queryMissingNum(periods, layero) // initiates AJAX
|
||||
Controller.api.renderMissingNum(data, periods, layero) // renders result grid
|
||||
Controller.api.colorMapLoaded // boolean: color map ready flag
|
||||
Controller.api.getColorByNum(num) // returns CSS color string
|
||||
```
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="false">
|
||||
<name>Task 1: 验证联调链路并完善边界情况处理</name>
|
||||
<files>public/assets/js/backend/history.js</files>
|
||||
<read_first>
|
||||
- public/assets/js/backend/history.js(plan 01-02 修改后的完整文件)
|
||||
- application/admin/controller/History.php(plan 01-01 修改后的控制器)
|
||||
- application/admin/model/History.php(plan 01-01 修改后的模型)
|
||||
</read_first>
|
||||
<action>
|
||||
检查 plan 01-02 创建的 JS 代码,确保以下边界情况已正确处理。如果已有则跳过,如果缺失则补充。
|
||||
|
||||
**边界情况 1:colorMap 未加载时的处理**
|
||||
确认 queryMissingNum 方法在 colorMapLoaded === false 时,先调用 loadColorMap 等待完成再发起请求。代码应类似:
|
||||
```javascript
|
||||
queryMissingNum: function (periods, layero) {
|
||||
if (!Controller.api.colorMapLoaded) {
|
||||
Controller.api.loadColorMap(function () {
|
||||
Controller.api._doQueryMissingNum(periods, layero);
|
||||
});
|
||||
} else {
|
||||
Controller.api._doQueryMissingNum(periods, layero);
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
**边界情况 2:后端返回空数据(所有号码在最近 X 期都出现过)**
|
||||
确认 renderMissingNum 方法在 data.length === 0 时显示友好提示而非空白:
|
||||
```javascript
|
||||
if (!data || data.length === 0) {
|
||||
$('#missing-result', layero).html('<div class="alert alert-info">...</div>');
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
**边界情况 3:AJAX 请求失败(网络错误、服务器 500)**
|
||||
确认 _doQueryMissingNum 的 error 回调正确显示错误信息:
|
||||
```javascript
|
||||
error: function () {
|
||||
$('#missing-result', layero).html('<div class="alert alert-danger">请求失败</div>');
|
||||
}
|
||||
```
|
||||
|
||||
**边界情况 4:波色球颜色兜底**
|
||||
确认 renderMissingNum 中 getColorByNum 对未映射号码返回灰色 (#95a5a6)——这已在 getColorByNum 方法中实现,此处只需确保调用正确。
|
||||
|
||||
**边界情况 5:快速重复点击查询按钮**
|
||||
在 _doQueryMissingNum 中,发起请求前禁用查询按钮,请求完成后恢复:
|
||||
```javascript
|
||||
var $btn = $('#btn-missing-query', layero);
|
||||
$btn.prop('disabled', true);
|
||||
$.ajax({
|
||||
...
|
||||
complete: function () {
|
||||
$btn.prop('disabled', false);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
如果上述边界情况在 plan 01-02 的代码中已经处理,本任务仅做验证性读取确认,不做修改。如果有缺失项,补充对应代码。
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- queryMissingNum 包含 `if (!Controller.api.colorMapLoaded)` 检查
|
||||
- queryMissingNum 在 colorMapLoaded 为 false 时调用 `Controller.api.loadColorMap(function(){...})`
|
||||
- renderMissingNum 包含 `data.length === 0` 的空数据处理分支
|
||||
- _doQueryMissingNum 包含 error 回调函数
|
||||
- _doQueryMissingNum 的 $.ajax 包含 complete 回调用于恢复按钮状态
|
||||
- _doQueryMissingNum 在请求前设置 `$('#btn-missing-query', layero).prop('disabled', true)`
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>grep -c "colorMapLoaded\|data\.length === 0\|\.prop.*disabled\|complete:" public/assets/js/backend/history.js | awk '$1 >= 4'</automated>
|
||||
</verify>
|
||||
<done>所有边界情况(颜色映射未就绪、空数据、请求失败、按钮防重复点击、波色兜底)均已正确处理</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 2: 人工验证完整功能链路</name>
|
||||
<files>public/assets/js/backend/history.js, application/admin/controller/History.php, application/admin/model/History.php</files>
|
||||
<what-built>
|
||||
后端 missingNum 接口 + 前端弹窗 UI + AJAX 联调 + 边界情况处理
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
按以下步骤在浏览器中验证(假设 admin 后台地址为 http://localhost/ByZjtVrKok.php):
|
||||
|
||||
1. **登录 admin 后台**,进入 history 页面
|
||||
2. **检查按钮**:确认 toolbar 出现黄色"遗漏号码"按钮(带搜索图标)
|
||||
3. **打开弹窗**:点击按钮,确认弹出 Layer 窗口,标题为"遗漏号码分析"
|
||||
4. **检查弹窗内容**:确认有"查询期数:"标签、数字输入框(默认值 10)、查询按钮
|
||||
5. **正常查询**:保持默认值 10,点击查询
|
||||
- 确认出现"查询中..."加载提示(带 spinner)
|
||||
- 确认加载完成后显示结果网格:每个球显示号码(带颜色)+ 下方"遗漏 X 期"文字
|
||||
- 确认结果从左到右按遗漏期数从大到小排列(最左边 omit 最大)
|
||||
- 确认球的颜色与 history 表格中的波色球一致
|
||||
6. **边界值测试**:
|
||||
- 输入 1 点击查询,确认返回结果
|
||||
- 输入 100 点击查询,确认返回结果
|
||||
- 输入 0 点击查询,确认显示错误提示"期数范围必须在 1-100 之间"
|
||||
- 输入 200 点击查询,确认显示错误提示
|
||||
7. **防重复点击**:点击查询按钮后,确认按钮变灰(disabled),请求完成后恢复可点击
|
||||
8. **关闭弹窗**:点击弹窗外的遮罩区域,确认弹窗关闭
|
||||
9. **重复打开**:再次点击"遗漏号码"按钮,确认弹窗正常打开且输入框恢复默认值 10
|
||||
</how-to-verify>
|
||||
<resume-signal>验证通过请回复"approved",如有问题请描述具体现象</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| AJAX response → DOM rendering | 后端返回的数据直接注入 DOM,需确保无 XSS 风险 |
|
||||
| User rapid-click → multiple AJAX requests | 可能导致竞态条件或服务器负载 |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-01-07 | Tampering | renderMissingNum DOM 渲染 | mitigate | 使用 textContent/innerText 或 jQuery .text() 渲染号码数字,不使用 .html() 注入原始数据;球的颜色通过 style.backgroundColor 设置 |
|
||||
| T-01-08 | Denial of Service | 快速重复点击 | mitigate | 请求期间禁用查询按钮(complete 回调恢复),防止并发请求 |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- 所有边界情况已在代码中处理(grep 验证通过)
|
||||
- 人工验证 9 个步骤全部通过
|
||||
- 无 JavaScript 控制台错误
|
||||
- 后端无 PHP 错误日志
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] 前端 AJAX 请求正确调用后端 missingNum 接口
|
||||
- [ ] 后端返回的 {num, omit, color} 数据正确渲染为波色球网格
|
||||
- [ ] 结果按遗漏期数从大到小排列
|
||||
- [ ] 波色球颜色与表格中一致
|
||||
- [ ] 空数据时显示友好提示
|
||||
- [ ] 参数超出范围时显示错误提示
|
||||
- [ ] 请求期间按钮被禁用防止重复提交
|
||||
- [ ] 点击遮罩可关闭弹窗
|
||||
- [ ] 无 JavaScript 控制台错误
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-omitted-number-analysis/01-03-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
phase: 01-omitted-number-analysis
|
||||
plan: 03
|
||||
subsystem: integration
|
||||
tags: [jquery, layer, fastadmin, thinkphp, ajax, xss-prevention]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 01-omitted-number-analysis
|
||||
provides: 01-01 backend missingNum endpoint + 01-02 history toolbar button + Layer dialog UI
|
||||
provides:
|
||||
- Complete end-to-end integration: button -> dialog -> AJAX -> backend -> result display
|
||||
- Boundary case handling: colorMap not loaded, empty data, AJAX failure, duplicate click prevention
|
||||
- XSS mitigation: jQuery .text() used for DOM injection instead of string concatenation
|
||||
affects: [future omission trend analysis, any feature reusing missingNum endpoint]
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Button disabled during AJAX request via $btn.prop('disabled', true/false) with complete callback"
|
||||
- "Safe DOM rendering: jQuery .text() for numbers and labels, .css() for colors — no .html() with external data"
|
||||
- "Color map loaded guard: queryMissingNum checks colorMapLoaded before dispatching AJAX"
|
||||
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- public/assets/js/backend/history.js (added button disable/restore, XSS-safe rendering via jQuery DOM methods)
|
||||
|
||||
key-decisions:
|
||||
- "Used jQuery .text() instead of string concatenation for rendering number and omission label — mitigates XSS from untrusted API data (T-01-07)"
|
||||
- "Button disabled state managed via $btn.prop('disabled', true) before AJAX, restored in complete callback — ensures single-request-at-a-time (T-01-08)"
|
||||
- "No code changes needed for colorMapLoaded guard, empty data handling, or error callback — already correct from plan 01-02"
|
||||
|
||||
patterns-established:
|
||||
- "All external data (numbers, omission counts) rendered via .text() — colors applied via .css('background-color', ...) — never .html() with API data"
|
||||
- "AJAX request lifecycle: disable button -> show spinner -> request -> success/error -> complete restores button"
|
||||
|
||||
requirements-completed: [OMIT-02, OMIT-03, OMIT-04]
|
||||
|
||||
# Metrics
|
||||
duration: 5min
|
||||
completed: 2026-04-21
|
||||
---
|
||||
|
||||
# Phase 01 Plan 03: Integration Verification & Boundary Case Handling Summary
|
||||
|
||||
**End-to-end AJAX integration verified between history.js and History::missingNum() endpoint, with XSS-safe rendering and duplicate-click prevention**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~5 min
|
||||
- **Started:** 2026-04-21T13:12:00Z
|
||||
- **Completed:** 2026-04-21T13:17:00Z
|
||||
- **Tasks:** 2
|
||||
- **Files modified:** 1 (history.js)
|
||||
|
||||
## Accomplishments
|
||||
- Verified all 5 boundary cases: colorMap not loaded, empty data, AJAX failure, button duplicate-click, color fallback
|
||||
- Fixed XSS vulnerability in renderMissingNum by replacing string concatenation with jQuery .text() and .css() DOM methods
|
||||
- Added button disabled/restore lifecycle to prevent duplicate AJAX requests during pending query
|
||||
- Human verification passed: all 9 steps in plan confirmed working in browser (button, dialog, query, results, boundary values, close, reopen)
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Task 1: Verify integration链路 and完善边界情况处理** - `bc8d38c` (fix)
|
||||
- Added `$btn.prop('disabled', true)` before AJAX request
|
||||
- Added `complete` callback to restore button state
|
||||
- Replaced string concatenation rendering with jQuery `.text()` for XSS safety
|
||||
2. **Task 2: Human verification of complete feature pipeline** - approved by user in browser
|
||||
|
||||
**Plan metadata:** committed with SUMMARY.md
|
||||
|
||||
## Files Created/Modified
|
||||
- `D:/code/php/amlhc/.claude/worktrees/agent-a4fa6413/public/assets/js/backend/history.js` - Added button disable/restore, XSS-safe DOM rendering via jQuery .text()/.css()
|
||||
|
||||
## Decisions Made
|
||||
- Used jQuery `.text()` for rendering number values and omission labels — this satisfies threat T-01-07 (tampering via DOM injection) by ensuring no HTML injection of external data
|
||||
- Used `.css('background-color', color)` for ball colors — style-only, no HTML content risk
|
||||
- Kept `colorMapLoaded` guard, `data.length === 0` check, and `error` callback as-is from plan 01-02 — all three were already correctly implemented
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 2 - Missing Critical] Fixed XSS vulnerability in renderMissingNum**
|
||||
- **Found during:** Task 1 (boundary case verification)
|
||||
- **Issue:** Plan 01-02 used string concatenation to build HTML with `data[i].num` and `data[i].omit` directly injected into `.html()` — if API returns malicious data, this creates XSS vector (threat T-01-07)
|
||||
- **Fix:** Replaced with jQuery DOM methods: `.text(data[i].num)` for the ball number, `.text(__('Missing') + ' ' + data[i].omit + ' ' + __('periods'))` for the label, `.css('background-color', color)` for ball color
|
||||
- **Files modified:** public/assets/js/backend/history.js
|
||||
- **Verification:** Confirmed no `.html()` calls with external data in renderMissingNum; all data injected via `.text()` or `.css()`
|
||||
- **Committed in:** `bc8d38c` (Task 1 commit)
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 1 auto-fixed (1 missing critical - XSS prevention)
|
||||
**Impact on plan:** Essential for security. No scope creep — aligns with existing threat model T-01-07.
|
||||
|
||||
## Issues Encountered
|
||||
- None
|
||||
|
||||
## Threat Surface Scan
|
||||
|
||||
| Flag | File | Description |
|
||||
|------|------|-------------|
|
||||
| threat_flag: XSS (mitigated) | public/assets/js/backend/history.js | renderMissingNum now uses `.text()` for all external data injection — no `.html()` with API response data |
|
||||
|
||||
## Known Stubs
|
||||
None. All data rendering is fully wired to the backend `missingNum` endpoint.
|
||||
|
||||
## Next Phase Readiness
|
||||
- Full integration verified and working
|
||||
- XSS mitigation in place for DOM rendering
|
||||
- Ready for next phase (omission trend analysis or other lottery features)
|
||||
- All 3 OMIT requirements (OMIT-02, OMIT-03, OMIT-04) satisfied
|
||||
|
||||
---
|
||||
*Phase: 01-omitted-number-analysis*
|
||||
*Completed: 2026-04-21*
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user