Files
amlhc/.planning/phases/01-omitted-number-analysis/01-01-PLAN.md
T
2026-04-21 23:02:15 +08:00

321 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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-482fa_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 为号码 intvalue 为 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=1data 为非空数组
- 请求 `history/missingNum?periods=0` 返回错误(code!=1
- 请求 `history/missingNum?periods=200` 返回错误(code!=1
- 返回数据中每个元素包含 numint 1-49)、omitint >=0)、colorstring
- 返回数组按 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>