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