This commit is contained in:
2026-04-21 23:01:55 +08:00
commit 08e56caa72
597 changed files with 159445 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
# amlhc — 澳门六合彩后台遗漏号码分析
## What This Is
基于 FastAdmin 1.6 + ThinkPHP 5.x 的澳门六合彩数据管理后台。已有功能包括:开奖历史查看、号码波色映射(fa_num 表)、数据抓取。本次新增**遗漏号码分析**功能——在后台 history 页面添加按钮,弹窗展示最近 X 期未出现的冷门号码。
## Core Value
快速识别冷门号码,辅助投注决策——遗漏期数越长的号码,出现概率的感知越强。
## Requirements
### Validated
- ✓ 开奖历史列表展示(含波色球渲染) — 已有
- ✓ 号码波色映射(fa_num 表,从数据库读取) — 已有
- ✓ 历史数据录入(admin 添加) — 已有
### Active
- [ ] **OMIT-01**: history 页面新增"遗漏号码"按钮,点击弹窗展示
- [ ] **OMIT-02**: 弹窗内可输入期数 X(默认 10),点击查询后展示最近 X 期未出现的号码
- [ ] **OMIT-03**: 展示内容为:遗漏号码 + 遗漏期数(多少期没出现)+ 波色球
- [ ] **OMIT-04**: 遗漏号码按遗漏期数从大到小排序
- [ ] **OMIT-05**: 后端接口支持查询最近 X 期开奖数据并计算遗漏号码(1-49 范围)
### Out of Scope
- 遗漏统计历史趋势图 — 首期不涉及,后续可扩展
- 遗漏号码的预测推荐 — 仅做数据展示,不做分析推荐
- 前台用户可见 — 仅后台 admin 功能
## Context
- 数据库:MySQL,表前缀 `fa_`,主要表 `fa_history`(开奖历史)、`fa_num`(号码波色)
- 前端:RequireJS + Bootstrap Table + Layer 弹窗
- 后端:FastAdmin Backend 基类,ThinkPHP 5.x
- 号码范围:1-49(标准六合彩)
- 波色球颜色已在 `fa_num` 表中定义(红/蓝/绿)
- history.js 已有 `getColorByNum()` 方法从后端加载颜色映射
## Constraints
- **[Tech stack]**: 必须沿用 FastAdmin + ThinkPHP 5.x 架构,不引入新框架
- **[UI style]**: 使用 FastAdmin 现有的 Layer 弹窗和 Bootstrap 组件风格
- **[Code convention]**: 保持与原代码风格统一,函数级中文注释
## Key Decisions
| Decision | Rationale | Outcome |
|----------|-----------|---------|
| 遗漏号码在 history 页面以按钮+弹窗形式展示 | 不新增独立页面/菜单,最小化改动 | ✓ Good |
| 遗漏计算在后端完成,前端只负责展示 | 避免前端大量计算,后端可复用模型 | ✓ Good |
| 使用 `$.ajax` 请求遗漏接口,Layer 弹窗展示 | FastAdmin 标准交互模式 | ✓ Good |
---
*Last updated: 2026-04-21 after initialization*
## Evolution
This document evolves at phase transitions and milestone boundaries.
**After each phase transition** (via `/gsd-transition`):
1. Requirements invalidated? → Move to Out of Scope with reason
2. Requirements validated? → Move to Validated with phase reference
3. New requirements emerged? → Add to Active
4. Decisions to log? → Add to Key Decisions
5. "What This Is" still accurate? → Update if drifted
**After each milestone** (via `/gsd-complete-milestone`):
1. Full review of all sections
2. Core Value check — still the right priority?
3. Audit Out of Scope — reasons still valid?
4. Update Context with current state
+45
View File
@@ -0,0 +1,45 @@
# Requirements: amlhc 遗漏号码分析
**Defined:** 2026-04-21
**Core Value:** 快速识别冷门号码,辅助投注决策
## v1 Requirements
### 遗漏号码分析
- [ ] **OMIT-01**: history 页面新增"遗漏号码"按钮,点击弹窗展示
- [ ] **OMIT-02**: 弹窗内可输入期数 X(默认 10),点击查询后展示最近 X 期未出现的号码
- [ ] **OMIT-03**: 展示内容为:遗漏号码 + 遗漏期数(多少期没出现)+ 波色球
- [ ] **OMIT-04**: 遗漏号码按遗漏期数从大到小排序
- [ ] **OMIT-05**: 后端接口支持查询最近 X 期开奖数据并计算遗漏号码(1-49 范围)
## v2 Requirements
(None yet)
## Out of Scope
| Feature | Reason |
|---------|--------|
| 遗漏统计历史趋势图 | 首期不涉及,后续可扩展 |
| 遗漏号码的预测推荐 | 仅做数据展示,不做分析推荐 |
| 前台用户可见 | 仅后台 admin 功能 |
## Traceability
| Requirement | Phase | Status |
|-------------|-------|--------|
| OMIT-01 | Phase 1 | Pending |
| OMIT-02 | Phase 1 | Pending |
| OMIT-03 | Phase 1 | Pending |
| OMIT-04 | Phase 1 | Pending |
| OMIT-05 | Phase 1 | Pending |
**Coverage:**
- v1 requirements: 5 total
- Mapped to phases: 5
- Unmapped: 0 ✓
---
*Requirements defined: 2026-04-21*
*Last updated: 2026-04-21 after initial definition*
+37
View File
@@ -0,0 +1,37 @@
# Roadmap: amlhc 遗漏号码分析
## Overview
为 FastAdmin 后台 history 页面新增遗漏号码分析功能——点击按钮弹窗展示最近 X 期未出现的冷门号码,附带波色球和遗漏期数,辅助投注决策。
## Phases
- [ ] **Phase 1: 遗漏号码分析** - 在 history 页面添加"遗漏号码"按钮,弹窗支持输入期数查询并展示遗漏号码、遗漏期数及波色球
## Phase Details
### Phase 1: 遗漏号码分析
**Goal**: 用户可在 history 页面通过弹窗查询并查看遗漏号码及其波色和遗漏期数
**Depends on**: Nothing (first phase)
**Requirements**: OMIT-01, OMIT-02, OMIT-03, OMIT-04, OMIT-05
**Success Criteria** (what must be TRUE):
1. 用户在 history 页面能看到"遗漏号码"按钮,点击后弹出模态窗口
2. 用户可在弹窗内输入期数 X(默认 10),点击查询后看到结果
3. 结果展示包含遗漏号码、遗漏期数和对应颜色的波色球,按遗漏期数从大到小排序
**Plans**: 3 plans
Plans:
- [x] 01-01-PLAN.md — 后端遗漏号码查询接口(History::missingNum() + History::getMissingNumbers(),查询最近 X 期并计算 1-49 遗漏号码、遗漏期数及波色,按 omit 降序返回)
- [x] 01-02-PLAN.md — history 页面"遗漏号码"按钮及 Layer 弹窗 UItoolbar 按钮、期数输入框、查询按钮、flex 网格结果渲染、复用 getColorByNum() 波色球着色)
- [x] 01-03-PLAN.md — 前后端联调验证(AJAX 链路测试、边界情况处理:颜色映射未就绪/空数据/请求失败/按钮防重复、人工验证完整功能)
**UI hint**: yes
## Progress
**Execution Order:**
Phases execute in numeric order: 1
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
| 1. 遗漏号码分析 | 0/3 | Not started | - |
+82
View File
@@ -0,0 +1,82 @@
---
gsd_state_version: 1.0
milestone: v1.0
milestone_name: milestone
status: executing
stopped_at: ROADMAP.md created, Phase 1 ready to plan
last_updated: "2026-04-21T13:05:05.724Z"
last_activity: 2026-04-21 -- Phase 01 execution started
progress:
total_phases: 1
completed_phases: 0
total_plans: 3
completed_plans: 0
percent: 0
---
# Project State
## Project Reference
See: .planning/PROJECT.md (updated 2026-04-21)
**Core value:** 快速识别冷门号码,辅助投注决策
**Current focus:** Phase 01 — omitted-number-analysis
## Current Position
Phase: 01 (omitted-number-analysis) — EXECUTING
Plan: 1 of 3
Status: Executing Phase 01
Last activity: 2026-04-21 -- Phase 01 execution started
Progress: [░░░░░░░░░░] 0%
## Performance Metrics
**Velocity:**
- Total plans completed: 0
- Average duration: N/A
- Total execution time: N/A
**By Phase:**
| Phase | Plans | Total | Avg/Plan |
|-------|-------|-------|----------|
| - | - | - | - |
**Recent Trend:**
- N/A (no completed plans yet)
## Accumulated Context
### Decisions
Decisions are logged in PROJECT.md Key Decisions table.
Recent decisions affecting current work:
- [Phase 1]: 遗漏号码在 history 页面以按钮+弹窗形式展示,不新增独立页面/菜单
- [Phase 1]: 遗漏计算在后端完成,前端只负责展示
- [Phase 1]: 使用 $.ajax 请求遗漏接口,Layer 弹窗展示
### Pending Todos
None yet.
### Blockers/Concerns
None yet.
## Deferred Items
| Category | Item | Status | Deferred At |
|----------|------|--------|-------------|
| *(none)* | | | |
## Session Continuity
Last session: 2026-04-21
Stopped at: ROADMAP.md created, Phase 1 ready to plan
Resume file: None
+380
View File
@@ -0,0 +1,380 @@
# Architecture
**Analysis Date:** 2026-04-21
## Pattern Overview
**Overall:** MVC (Model-View-Controller) with module-based multi-application architecture, built on ThinkPHP 5.x framework and FastAdmin 1.6.1 admin framework.
**Key Characteristics:**
- **Multi-module structure**: `admin` (backend management), `index` (frontend user portal), `api` (RESTful API), `common` (shared code)
- **Trait-based CRUD inheritance**: Backend controllers inherit `index/add/edit/del/multi/recyclebin/destroy/restore` from `app\admin\library\traits\Backend`
- **RBAC (Role-Based Access Control)**: Admin permissions via admin → auth_group → auth_group_access → auth_rule chain; user permissions via user → user_group → user_rule chain
- **Addon/plugin architecture**: Extensible system via `addons/` directory with lifecycle hooks
- **Dual Auth systems**: `app\admin\library\Auth` (session-based admin auth) and `app\common\library\Auth` (token-based user auth)
## Modules
The application is organized into 4 ThinkPHP modules under `application/`:
### Admin Module (`application/admin/`)
- Purpose: Backend management panel for administrators
- Contains: Admin CRUD controllers, RBAC management, system config, user management, lottery data management (History, Num), command execution interface
- Entry: `public/index.php` → module `admin`
- Controllers: 18 controllers across 4 subdirectories
### Index Module (`application/index/`)
- Purpose: Frontend user-facing website
- Contains: Public homepage, user login/register/profile, ajax endpoints, lottery data scraping
- Controllers: `Index.php` (lottery scraping + homepage), `User.php` (member center), `Ajax.php` (frontend async operations)
### API Module (`application/api/`)
- Purpose: RESTful API endpoints for mobile/third-party clients
- Contains: User auth, registration, profile, token management, SMS/EMS verification
- Controllers: `Index.php`, `User.php`, `Common.php`, `Demo.php`, `Ems.php`, `Sms.php`, `Token.php`, `Validate.php`
### Common Module (`application/common/`)
- Purpose: Shared code across all modules
- Contains: Base controllers, shared models, libraries (Auth, Upload, Email, Token drivers), exceptions
## Controller Inheritance Chain
### Backend (Admin Controllers)
```
think\Controller (ThinkPHP base)
└── app\common\controller\Backend (base backend controller)
└── app\admin\library\traits\Backend (trait: CRUD methods)
└── app\admin\controller\* (all admin controllers)
```
**`app\common\controller\Backend`** (`application/common/controller/Backend.php`):
- Properties: `$noNeedLogin`, `$noNeedRight`, `$layout`, `$auth`, `$model`, `$searchFields`, `$relationSearch`, `$dataLimit`, `$dataLimitField`, `$dataLimitFieldAutoFill`, `$modelValidate`, `$modelSceneValidate`, `$multiFields`, `$selectpageFields`, `$excludeFields`, `$importHeadType`
- `_initialize()`: IP check → Auth instance → login verification → permission check → breadcrumb setup → layout → language loading → view config assignment
- `buildparams()`: Constructs WHERE conditions from GET params (search, filter, op, sort, order, offset, limit) with support for LIKE, IN, BETWEEN, RANGE, FIND_IN_SET, NULL operators
- `selectpage()`: Universal select/dropdown search with tree support
- `loadlang()`: Loads language file for current controller
- `assignconfig()`: Merges config into view
- `getDataLimitAdminIds()`: Returns admin IDs for data scoping based on `$dataLimit` setting
**`app\admin\library\traits\Backend`** (`application/admin/library/traits/Backend.php`):
- `index()`: List with pagination, JSON response for AJAX
- `add()`: Create with validation, transaction support
- `edit()`: Update with validation, data limit check
- `del()`: Soft delete (batch supported)
- `recyclebin()`: View soft-deleted records
- `destroy()`: Permanent delete from recycle bin
- `restore()`: Restore from recycle bin
- `multi()`: Batch update specified fields
- `import()`: Excel/CSV import with PhpSpreadsheet
### Frontend (Index Controllers)
```
think\Controller (ThinkPHP base)
└── app\common\controller\Frontend (base frontend controller)
└── app\index\controller\* (all frontend controllers)
```
**`app\common\controller\Frontend`** (`application/common/controller/Frontend.php`):
- `_initialize()`: Input filtering → IP check → layout → Auth init → token-based login check → user assignment → site config → language loading
- Auth uses `app\common\library\Auth` (token-based)
- `loadlang()`: Loads language file for current controller
- `assignconfig()`: Merges config into view
### API Controllers
```
(no ThinkPHP Controller base)
└── app\common\controller\Api (base API controller, not extending think\Controller)
└── app\api\controller\* (all API controllers)
```
**`app\common\controller\Api`** (`application/common/controller/Api.php`):
- Does NOT extend `think\Controller` — manual constructor pattern
- Properties: `$noNeedLogin`, `$noNeedRight`, `$auth`, `$responseType`, `$failException`, `$batchValidate`
- `_initialize()`: CORS check → IP check → input filtering → token auth → permission check → language loading
- `success()` / `error()`: Standard API response format `{code, msg, time, data}`
- `result()`: Unified response with HTTP status code mapping
- `validate()`: Data validation with fail-exception mode
- `beforeAction()`: Before-action hook support
## RBAC Auth System
### Admin RBAC
```
fa_admin (管理员)
└── fa_auth_group_access (管理员-角色关联, N:N)
└── fa_auth_group (角色组)
└── rules (权限规则ID列表, 逗号分隔)
└── fa_auth_rule (权限规则)
```
**Key classes:**
- `app\admin\library\Auth` extends `fast\Auth` — admin authentication and authorization
- `app\admin\model\Admin` — admin user model
- `app\admin\model\AuthGroup` — role group model
- `app\admin\model\AuthGroupAccess` — admin-role pivot model
- `app\admin\model\AuthRule` — permission rule model
- `app\admin\model\AdminLog` — admin operation log model
**Auth flow:**
1. Login: `Admin::login()` → verifies password (MD5 double-hash with salt: `md5(md5(password) . salt)`) → sets Session + token + keeplogin cookie
2. Permission check: `Auth::check($path)` → reads user's group rules → checks if controller/action path is in allowed rules
3. Super admin: `Auth::isSuperAdmin()` → returns true if rule list contains `*`
4. Data scoping: `Backend::$dataLimit` supports `auth` (group-scoped) and `personal` (user-scoped) data filtering via `getDataLimitAdminIds()`
5. Session management: `safecode` validation ensures re-login on credential changes; supports single-session mode (`fastadmin.login_unique`) and IP check mode (`fastadmin.loginip_check`)
6. Auto-login: `Auth::autologin()` checks `keeplogin` cookie with time-limited key validation
**Key Auth methods:**
- `login($username, $password, $keeptime)` — admin login
- `logout()` — clear session and token
- `check($name, $uid, $relation, $mode)` — permission check
- `match($arr)` — check if current action matches whitelist
- `isLogin()` — session + safecode validation
- `isSuperAdmin()` — check for `*` in rules
- `getSidebar($params, $fixedPage)` — generate left/top navigation menu
- `getBreadCrumb($path)` — breadcrumb navigation
- `getChildrenAdminIds($withself)` — scoped admin IDs
- `getChildrenGroupIds($withself)` — scoped group IDs
### User RBAC (Frontend)
```
fa_user (会员)
└── group_id → fa_user_group (会员组)
└── rules → fa_user_rule (会员权限规则)
```
**Key classes:**
- `app\common\library\Auth` — user authentication (token-based, singleton pattern)
- `app\common\model\User` — user model with money/score log hooks
- `app\common\model\UserGroup` — user group model
- `app\common\model\UserRule` — user rule model
- Token storage: `app\common\library\Token` with MySQL or Redis drivers
**Auth flow:**
1. Token init: `Auth::init($token)` → Token::get() → load user → set `_logined` flag
2. Login: `Auth::login($account, $password)` → finds user by email/mobile/username → verifies password → `direct($user_id)`
3. Token management: UUID token stored in Token model (or Redis) with configurable TTL (`keeptime = 2592000` = 30 days)
4. Password encryption: `md5(md5(password) . salt)` — same algorithm as admin
**Key Auth methods:**
- `instance($options)` — singleton getter
- `init($token)` — initialize from token
- `login($account, $password)` — user login
- `register($username, $password, $email, $mobile, $extend)` — user registration
- `logout()` — delete token
- `changepwd($newpassword, $oldpassword, $ignoreoldpassword)` — change password
- `direct($user_id)` — direct login (bypass password check)
- `check($path, $module)` — permission check
- `delete($user_id)` — delete user and clear tokens
## MVC Patterns
### Model Pattern
- All models extend `think\Model`
- Timestamps: `autoWriteTimestamp = 'int'` with custom field names (`createtime`, `updatetime`)
- Attribute mutators: `getXxxAttr()` and `setXxxAttr()` for data transformation
- Model events: `self::init()` with `self::beforeWrite`, `self::afterInsert` hooks
- Relations: `belongsTo`, `hasMany` using ThinkPHP ORM
- Appended attributes: `$append = ['field_text']` for computed/display fields
- Enum lists: `getStatusList()`, `getGenderList()` for select/radio options
- Hidden fields: `$hidden = ['password', 'salt']` for sensitive data
### View Pattern
- Template engine: ThinkPHP template with `{}` syntax
- Layout: `application/admin/view/layout/default.html` includes `common/meta`, `common/script`, `common/header`, `common/menu`
- Admin views: `build_heading()` generates toolbar/buttons, `{:__()}` for i18n
- View config injection via `$this->view->assign()` and `$this->assignconfig()`
- Layout template set via `$this->layout` property in controllers
- Frontend layout: `application/index/view/layout/default.html`
### Validation Pattern
- Validators extend `think\Validate`
- Rules defined in `$rule` array (require, unique, regex, email, length, etc.)
- Scenes: `$scene = ['add' => [...], 'edit' => [...]]` for context-specific validation
- Constructor customization for i18n field labels
- Model-level validation: `Backend::$modelValidate` and `$modelSceneValidate` toggle automatic validation on add/edit
## Addon Architecture
**Directory:** `addons/`
**Plugin lifecycle:**
- Install: `Service::install($name)` → downloads, extracts, runs SQL
- Uninstall: `Service::uninstall($name)` → removes files, optionally drops tables
- Enable/Disable: `Service::enable($name)` / `Service::disable($name)`
- Upgrade: `Service::upgrade($name)` → downloads new version, runs migration
- Configure: `get_addon_config($name)` / `set_addon_fullconfig($name, $config)`
**Addon structure:**
```
addons/{name}/
├── info.ini # Plugin metadata
├── {Name}.php # Main class extends \think\addons\Addon
├── config.php # Plugin configuration schema
├── config.html # Config view (optional, custom config UI)
├── controller/ # Plugin controllers
├── model/ # Plugin models
├── view/ # Plugin views
└── install.sql # Installation SQL
```
**Admin addon controller** (`application/admin/controller/Addon.php`):
- Manages plugin list, install/uninstall, enable/disable, config, upgrade
- Restricted to super admin for destructive operations (install, uninstall, local, upgrade, authorization, testdata)
- Communicates with FastAdmin marketplace API (`fastadmin.api_url`)
- Methods: `index()`, `config($name)`, `install()`, `uninstall()`, `state()`, `local()`, `upgrade()`, `testdata()`, `downloaded()`, `isbuy()`, `authorization()`, `get_table_list()`
**Admin commands for addons:**
- `application/admin/command/Crud.php` — one-click CRUD generation from table
- `application/admin/command/Menu.php` — menu generation from controllers
- `application/admin/command/Min.php` — JS/CSS minification
- `application/admin/command/Api.php` — API documentation generation
- `application/admin/command/Install.php` — installation wizard
- `application/admin/command/Addon.php` — addon-related stubs and operations
**Hook system:** ThinkPHP Hook integration
- `admin_login_after`, `admin_logout_after`, `admin_nologin`, `admin_nopermission`
- `user_login_successed`, `user_register_successed`, `user_logout_successed`, `user_delete_successed`
- `upload_config_init`, `config_init`, `wipecache_after`
- `upload_delete`, `admin_sidebar_begin`
- `admin_login_init`
## Request Flow
### Admin Request
```
public/index.php
→ define APP_PATH → check install.lock → require thinkphp/start.php
→ ThinkPHP dispatch (module/controller/action)
→ app\admin\controller\{Controller}
→ parent::_initialize() (app\common\controller\Backend)
→ check_ip_allowed()
→ Auth::instance() → login check → permission check
→ loadlang() → view config assignment
→ action method (index/add/edit/del from trait or custom)
→ model operations
→ $this->view->fetch() or json()
```
### Frontend Request
```
public/index.php
→ ThinkPHP dispatch
→ app\index\controller\{Controller}
→ parent::_initialize() (app\common\controller\Frontend)
→ input filter → Auth init (token-based)
→ loadlang() → view config
→ action method → view->fetch()
```
### API Request
```
public/index.php
→ ThinkPHP dispatch
→ app\api\controller\{Controller}
→ __construct() → _initialize() (app\common\controller\Api)
→ CORS → IP check → token init → permission check
→ action method → $this->success() / $this->error()
→ result() → JSON response {code, msg, time, data}
```
## Data Flow: Lottery Feature (History, Num)
### History Model
- Table: `fa_history` (`application/admin/model/History.php`)
- Fields: `id`, `expect` (期号), `openTime` (开奖时间), `num1`~`num7` (7个开奖号码)
- No timestamps: `autoWriteTimestamp = false`, `createTime = false`, `updateTime = false`, `deleteTime = false`
- No validation rules defined in `application/admin/validate/History.php`
### Num Model
- Table: `fa_num` (`application/admin/model/Num.php`)
- Fields: `num`, `color` (波色映射 — wave color mapping for lottery numbers)
- No timestamps: `autoWriteTimestamp = false`
### Data Scraping Flow (`application/index/controller/Index.php::get_history()`)
1. User visits `index/index/get_history` (no auth required: `$noNeedLogin = '*'`)
2. Controller creates `\GuzzleHttp\Client` instance
3. GET request to `https://history.macaumarksix.com/history/macaujc2/y/2026` (Macau lottery history API)
4. Parses JSON response, extracts `data` array
5. For each item: splits `openCode` by comma into `num1`~`num7`
6. Checks if `expect` already exists in `fa_history` via `Db::name('history')->where('expect', $item['expect'])->find()`
7. If new: `Db::name('history')->insert($insert_data)`; if exists: `Db::name('history')->where('expect', $item['expect'])->update($insert_data)`
8. Returns success/failure JSON via `$this->success()` / `$this->error()`
### Admin History Management (`application/admin/controller/History.php`)
- Extends `Backend`, uses standard CRUD from trait
- Model: `app\admin\model\History`
- View: `application/admin/view/history/index.html` — list only, add/edit buttons hidden in template
- No custom methods — relies entirely on inherited CRUD
### Admin Num Query (`application/admin/controller/Num.php`)
- Extends `Backend`
- Model: `app\admin\model\Num`
- Custom method: `getColorMap()` — returns num→color mapping for frontend display as JSON
## Key Abstractions
### Auth (Dual System)
- `app\admin\library\Auth` — session-based admin auth extending `fast\Auth` (base RBAC class from `fast` namespace)
- `app\common\library\Auth` — token-based user auth (singleton), no ThinkPHP Controller dependency
### Tree
- `fast\Tree` — hierarchical data structure for menus, categories, rules
- Methods: `init()`, `getTreeList()`, `getTreeArray()`, `getChildrenIds()`, `getParentsIds()`, `getTreeMenu()`, `getChildren()`
### Token Drivers
- `app\common\library\Token` — token storage abstraction
- `app\common\library\token\driver\Mysql` — MySQL-backed token storage
- `app\common\library\token\driver\Redis` — Redis-backed token storage
### Upload
- `app\common\library\Upload` — file upload with chunked upload support
- Integrates with `app\common\model\Attachment` for metadata tracking
- Exception: `app\common\exception\UploadException`
### Date Utility
- `fast\Date` — date manipulation utility (e.g., `Date::unixtime('day', -6)`)
### Random Utility
- `fast\Random` — random string/UUID generation (used for salt, tokens)
## Entry Points
**`public/index.php`** — Main entry point for all modules
- Defines `APP_PATH`
- Checks `application/admin/command/Install/install.lock` for installation status
- Redirects to `install.php` if not installed
- Loads `thinkphp/start.php` for framework bootstrap
**`think`** — CLI entry point for ThinkPHP commands
- Used for CRUD generation, menu generation, API docs, minification, addon management
## Error Handling
**Strategy:** Exception-based with user-friendly error pages
**Patterns:**
- `$this->error($msg)` / `$this->success($msg)` — controller-level response helpers
- `try/catch` with `Db::startTrans()` / `Db::commit()` / `Db::rollback()` for transaction safety
- `ValidateException` — model validation failures
- `PDOException` — database errors
- `UploadException` — upload failures
- `AddonException` — plugin operation failures with structured error data
- `HttpResponseException` — API response termination
## Cross-Cutting Concerns
**Logging:** `AdminLog::record()` auto-logs admin operations (title, content, URL, IP, user agent); filtered by `ignoreRegex` to skip selectpage/index actions; `AdminLog::setTitle()` for custom titles; behavior hook via `app\admin\behavior\AdminLog`
**Validation:** ThinkPHP Validate with rule-based validation, scene support, custom messages; model-level and controller-level validation modes
**Authentication:** Dual auth system — session-based for admin, token-based for users/API; IP allowlist check via `check_ip_allowed()`; CSRF tokens for forms
**Internationalization:** Language files per module under `lang/zh-cn/` and `lang/en/`; `__()` function for translation; controller auto-loads matching lang file in `_initialize()`; admin loads `zh-cn` as default
**IP Filtering:** `check_ip_allowed()` called in all base controllers — reads `fastadmin.ip_blacklist` / `fastadmin.ip_whitelist` config
**Caching:** File-based cache (ThinkPHP default); menu cache via `cache("__menu__")`; template caching enabled; addon list cache via `Cache::get("onlineaddons")`
---
*Architecture analysis: 2026-04-21*
+630
View File
@@ -0,0 +1,630 @@
# Codebase Concerns
**Analysis Date:** 2026-04-21
---
## Security
### S1. Password Hashing Uses Weak Double-MD5 (HIGH)
**Files:**
- `application/common/library/Auth.php` line 490-492
- `application/admin/library/Auth.php` line 146-148
**Issue:** Both frontend and admin password hashing use double-MD5 with salt:
```php
// application/common/library/Auth.php:490-492
public function getEncryptPassword($password, $salt = '')
{
return md5(md5($password) . $salt);
}
```
MD5 is cryptographically broken. The inner hash is unsalted, making rainbow table attacks viable. MD5 can be brute-forced at billions of hashes per second on commodity hardware. The salt is only 6 characters (`Random::alnum()` typically produces short strings).
**Impact:** If the database is compromised, all user passwords can be cracked rapidly.
**Fix approach:** Migrate to `password_hash()` with `PASSWORD_BCRYPT` or `PASSWORD_ARGON2ID`. Add a migration script to re-hash passwords on next successful login.
---
### S2. SQL Injection via Raw Queries in Admin Controllers (HIGH)
**Files:**
- `application/admin/command/Crud.php` lines 440, 444, 462, 466
- `application/admin/controller/general/Config.php` line 293
- `application/admin/controller/Dashboard.php` line 47
- `application/admin/controller/Command.php` line 39
- `extend/fast/Auth.php` line 160
**Issue:** Multiple raw SQL queries interpolate variables without parameterization:
```php
// application/admin/command/Crud.php:440
$modelTableInfo = $dbconnect->query("SHOW TABLE STATUS LIKE '{$modelTableName}'", [], true);
// application/admin/controller/general/Config.php:293
$tableList = \think\Db::query("SELECT `TABLE_NAME` AS `name`,`TABLE_COMMENT` AS `title` FROM `information_schema`.`TABLES` where `TABLE_SCHEMA` = '{$dbname}';");
// extend/fast/Auth.php:160
->where("aga.uid='{$uid}' and ag.status='normal'")
```
The `Crud.php` command receives table names via CLI arguments (`--table=`) which could be manipulated. The `Auth.php` interpolates `$uid` directly into the WHERE clause.
**Impact:** An attacker with admin access could inject malicious SQL through crafted table names in CRUD generation. The Auth SQL injection is more concerning if `$uid` can ever be user-controlled.
**Fix approach:** Use parameterized queries. Validate table names against `/^[a-z0-9_]+$/i` before embedding in SQL. Replace string interpolation in Auth.php with `->where('aga.uid', $uid)->where('ag.status', 'normal')`.
---
### S3. HTTP Method Spoofing Enabled (MEDIUM)
**Files:**
- `application/config.php` line 109
**Issue:** The config `'var_method' => '_method'` allows clients to spoof HTTP methods via POST parameter. An attacker can send `_method=DELETE` in a POST request to bypass CSRF protections that only check POST requests.
**Impact:** CSRF bypass via method override on state-changing operations.
**Fix approach:** Set `'var_method' => ''` to disable method spoofing, or ensure all state-changing operations require explicit CSRF token validation regardless of HTTP method.
---
### S4. Cookie Security Flags Not Set (MEDIUM)
**Files:**
- `application/config.php` lines 216-231
**Issue:** Session cookies lack security flags:
```php
'cookie' => [
'secure' => false, // Not enforced
'httponly' => '', // Empty string = disabled
],
```
**Impact:** Session cookies vulnerable to interception on HTTP connections and theft via XSS attacks.
**Fix approach:** Set `'secure' => true` (when HTTPS is available) and `'httponly' => true`.
---
### S5. Token Key Hardcoded in Config (MEDIUM)
**Files:**
- `application/config.php` line 264
**Issue:** The token encryption key is hardcoded directly in the config file:
```php
'token' => [
'key' => '3byNV4KupeZAvl60sdr2COjDYUmqwPJW', // line 264
],
```
This value is committed to git and visible to anyone with repository access. If leaked, an attacker can forge valid tokens.
**Impact:** Token forgery if the key is exposed.
**Fix approach:** Move to environment variable via `Env::get('token.key')` and regenerate the key.
---
### S6. External API Scraping Without Data Validation (HIGH)
**Files:**
- `application/index/controller/Index.php` lines 20-58
**Issue:** The `get_history()` method fetches lottery data from `https://history.macaumarksix.com` and writes directly to the database with zero validation:
```php
public function get_history()
{
$client = new \GuzzleHttp\Client();
$res = $client->request('GET', 'https://history.macaumarksix.com/history/macaujc2/y/2026');
// ...
foreach ($data as $item) {
$insert_data['expect'] = $item['expect'];
$insert_data['num1'] = $openCode[0];
// ... direct DB insert without any validation
Db::name('history')->insert($insert_data);
}
}
```
No validation of: response data structure, value ranges (lottery numbers should be 1-49), data types, or expected format of `openCode`. The year `2026` is hardcoded.
**Impact:** If the external API is compromised or returns malformed data, the database gets corrupted with invalid lottery records. The hardcoded year requires annual manual updates.
**Fix approach:** Add data validation (type checks, range validation, schema verification), make the year parameter configurable, and add error handling for API structure changes.
---
### S7. External Scraping Has No Reliability Safeguards (MEDIUM)
**Files:**
- `application/index/controller/Index.php`
**Issue:** No retry logic, timeout configuration, or circuit breaker for the external API call. No rate limiting on the `get_history` endpoint - any user can trigger the scrape repeatedly. No Guzzle timeout is configured.
**Impact:** External API downtime causes unhandled exceptions. Repeated calls could trigger rate limiting or IP bans on the source API.
**Fix approach:** Add Guzzle timeout config, implement retry logic with exponential backoff, add rate limiting to the endpoint, and cache results.
---
### S8. Login CAPTCHA Disabled by Default (MEDIUM)
**Files:**
- `application/config.php` line 277
**Issue:** `'login_captcha' => false` disables login CAPTCHA.
**Impact:** Without login CAPTCHA, the application is vulnerable to brute-force password attacks and credential stuffing. The 10-attempts-per-day lockout is the only defense.
**Fix approach:** Set `'login_captcha' => true` in production.
---
### S9. Upload File Type Check Relies on Client-Supplied MIME (LOW-MEDIUM)
**Files:**
- `application/common/library/Upload.php` lines 87-98, 106-120
**Issue:** The upload check uses `$this->fileInfo['type']` which comes from the client's `Content-Type` header. PHP's `$_FILES['type']` is set by the browser and can be easily spoofed.
**Impact:** An attacker could potentially upload files with disguised MIME types if the suffix check has gaps.
**Fix approach:** Use `finfo_open()` / `finfo_file()` to detect actual file type from content bytes.
---
### S10. Suspicious PHP File in Public Directory (HIGH)
**Files:**
- `public/ByZjtVrKok.php` (1250 bytes, untracked in git)
**Issue:** A PHP file with a random-looking name exists in the public web root. Any PHP file in `public/` is directly executable via HTTP request.
**Impact:** This could be an unauthorized backdoor, test file, or admin script. If it contains administrative functionality, anyone who discovers the URL could execute it.
**Fix approach:** Audit the file contents immediately. If not needed, delete it. If it serves a purpose, move it outside the public directory.
---
### S11. No CSRF Protection on API Endpoints (MEDIUM)
**Files:**
- `application/api/controller/User.php`
- `application/api/controller/Sms.php`
- `application/api/controller/Ems.php`
**Issue:** API controller methods do not use CSRF token validation. While APIs typically use token-based auth, browser-initiated API calls from authenticated sessions are vulnerable to CSRF.
**Impact:** An authenticated user visiting a malicious page could have API calls triggered on their behalf (e.g., sending SMS codes, changing account settings).
**Fix approach:** For browser-initiated API calls that modify state, require CSRF token validation. For pure API usage, ensure token-based auth is mandatory.
---
### S12. 0777 File Permissions (MEDIUM)
**Files:**
- `application/common.php` line 121
- `application/admin/command/Addon.php` line 271
**Issue:** `@chmod($file, 0777)` sets world-writable permissions on created files.
**Impact:** On shared hosting, any user/process can modify these files, potentially injecting malicious code.
**Fix approach:** Use `0755` for directories and `0644` for files.
---
### S13. Deprecated mcrypt Fallback (LOW)
**Files:**
- `application/common/library/Security.php` lines 438-439
**Issue:** References `mcrypt_create_iv()` and `MCRYPT_DEV_URANDOM` — the mcrypt extension was removed in PHP 7.2. The project requires PHP >= 7.4.
**Impact:** Dead code that will never execute but creates confusion and maintenance debt.
**Fix approach:** Remove the mcrypt fallback. `random_bytes()` is sufficient for PHP 7.4+.
---
### S14. exec() Calls with User-Influenced Input (MEDIUM)
**Files:**
- `application/admin/command/Crud.php` lines 580, 1042, 1232
**Issue:** Shell commands are constructed with interpolated variables:
```php
exec("php think menu -c {$controllerUrl} -d 1 -f 1");
exec("php think crud -t {$relation['relationTableName']} ...");
```
**Impact:** If input variables can be controlled by non-admin users, command injection is possible.
**Fix approach:** Use `escapeshellarg()` on all variables interpolated into shell commands.
---
## Maintainability
### M1. Massive CRUD Command File (1795 lines)
**Files:**
- `application/admin/command/Crud.php` (1795 lines)
**Issue:** This single file handles table analysis, code generation for models, controllers, views, validation, language packs, menu generation, and relation handling. It violates the Single Responsibility Principle.
**Impact:** Difficult to maintain, test, and extend.
**Fix approach:** Split into separate classes: TableAnalyzer, ModelGenerator, ControllerGenerator, ViewGenerator, MenuGenerator.
---
### M2. Duplicated Code Across Base Controllers
**Files:**
- `application/common/controller/Api.php` lines 318-329, 153-160
- `application/common/controller/Backend.php` lines 600-613, 237-244
- `application/common/controller/Frontend.php` lines 149-161, 128-135
**Issue:** The `token()` and `loadlang()` methods are identically duplicated across all three base controllers.
**Impact:** Changes must be applied in three places.
**Fix approach:** Move to a shared trait.
---
### M3. Tightly Coupled Backend Traits
**Files:**
- `application/admin/library/traits/Backend.php` (481 lines)
- `application/common/controller/Backend.php` (614 lines)
**Issue:** The Backend trait injects massive functionality (index, add, edit, del, recyclebin, restore, destroy, multi, import, selectpage) into every admin controller. The trait and base controller are deeply intertwined.
**Impact:** All admin controllers carry full CRUD weight. Customizing one method requires overriding the entire trait method.
**Fix approach:** Split into smaller, focused traits (CrudTrait, ImportTrait, RecycleBinTrait).
---
### M4. No Service Layer
**Files:** All controllers
**Issue:** Business logic is embedded directly in controllers. `application/index/controller/Index.php` handles external API fetching, data parsing, and database insertion all in one method.
**Impact:** Controllers are difficult to test. Logic cannot be reused across entry points.
**Fix approach:** Introduce a service layer (e.g., `LotteryDataService`, `UserAuthService`).
---
### M5. composer.lock Ignored in Version Control
**Files:**
- `.gitignore` line 11
**Issue:** `composer.lock` is gitignored, meaning dependency versions are not pinned.
**Impact:** Different environments install different dependency versions, leading to inconsistent behavior and potential security vulnerabilities.
**Fix approach:** Remove `composer.lock` from `.gitignore` and commit it.
---
## Performance
### P1. N+1 Query Pattern in Data Scraping (HIGH)
**Files:**
- `application/index/controller/Index.php` lines 28-45
**Issue:** For each item from the external API, a separate SELECT + INSERT/UPDATE is executed:
```php
foreach ($data as $item) {
$exist = Db::name('history')->where('expect', $item['expect'])->find();
if (!$exist) {
Db::name('history')->insert($insert_data);
} else {
Db::name('history')->where('expect', $item['expect'])->update($insert_data);
}
}
```
This generates 2N queries for N records.
**Impact:** For large datasets, significant database load and slow execution.
**Fix approach:** Use `INSERT ... ON DUPLICATE KEY UPDATE` for single-query upsert, or batch operations.
---
### P2. File-Based Cache Only
**Files:**
- `application/config.php` lines 187-197
**Issue:** Only file-based caching configured. Token storage defaults to MySQL.
**Impact:** File I/O is slower than memory-based caching, especially under concurrent access. Token validation on every request adds database load.
**Fix approach:** Configure Redis for caching and token storage in production.
---
### P3. Dashboard Runs Heavy Queries Without Caching
**Files:**
- `application/admin/controller/Dashboard.php` lines 24-82
**Issue:** The dashboard executes ~10+ aggregate queries on every page load with no caching:
```php
$totaluser = User::count();
$todayusersignup = User::whereTime('jointime', 'today')->count();
$sevendau = User::whereTime('jointime|logintime|prevtime', '-7 days')->count();
$dbTableList = Db::query("SHOW TABLE STATUS");
```
**Impact:** Dashboard becomes slower as user base grows.
**Fix approach:** Cache dashboard statistics with a short TTL (e.g., 5 minutes).
---
### P4. No Frontend Asset Build Pipeline
**Files:**
- `Gruntfile.js`
- `public/assets/js/backend/command.js`
- `public/assets/js/backend/history.js`
**Issue:** While a Gruntfile.js exists, there is no evidence of automated build pipeline. JavaScript files are loaded individually per controller.
**Impact:** Slow page load times due to multiple HTTP requests.
**Fix approach:** Implement asset bundling and minification in the deployment pipeline.
---
## Reliability
### R1. Silent Exception Swallowing in Financial Operations (HIGH)
**Files:**
- `application/common/model/User.php` lines 93-111, 119-137
**Issue:** The `money()` and `score()` methods catch exceptions but only roll back without logging or returning error:
```php
public static function money($money, $user_id, $memo)
{
Db::startTrans();
try {
// ... money operation
Db::commit();
} catch (\Exception $e) {
Db::rollback();
// No logging, no return value - silent failure
}
}
```
**Impact:** Failed money/score operations silently fail. Financial data integrity is at risk.
**Fix approach:** Log the exception, throw or return error indicator. Add audit trail for financial operations.
---
### R2. Empty Catch Block in Dashboard
**Files:**
- `application/admin/controller/Dashboard.php` lines 26-30
**Issue:** Exception caught and completely ignored:
```php
try {
\think\Db::execute("SET @@sql_mode='';");
} catch (\Exception $e) {
// Empty
}
```
**Fix approach:** Log the exception at minimum.
---
### R3. No Structured Logging
**Files:**
- `application/config.php` lines 168-177
**Issue:** Logging configured minimally with empty level array. No log rotation, no error tracking service.
**Impact:** Difficult to debug production issues. Log files can grow unbounded.
**Fix approach:** Configure log levels, add log rotation, integrate with error tracking (e.g., Sentry).
---
### R4. No Validation on History Model
**Files:**
- `application/admin/controller/History.php`
- `application/admin/model/History.php`
- `application/admin/validate/History.php` (exists but not referenced)
**Issue:** The History controller has no custom validation. The model has no validation rules. A validate file exists (`application/admin/validate/History.php`) but is not referenced in the controller.
**Impact:** Malformed lottery data can be stored through the admin panel.
**Fix approach:** Enable model validation in the controller and define rules for lottery data fields.
---
## Architecture
### A1. ThinkPHP 5.x is Outdated
**Files:**
- `composer.json` line 19
- `thinkphp/` directory
**Issue:** Uses `topthink/framework: dev-master` (ThinkPHP 5.x fork). ThinkPHP 5 is EOL; current stable is 8.x. The dev-master branch from Gitee is a maintained fork but diverges from the official framework.
**Impact:** No official security patches. Dependency compatibility issues.
**Fix approach:** Monitor the FastAdmin fork for security updates. Plan migration to ThinkPHP 6+ when feasible.
---
### A2. Dependency Version Risks
**Files:**
- `composer.json`
**Issue:** Unstable and outdated dependencies:
```json
"topthink/framework": "dev-master", // Unstable branch
"topthink/think-queue": "1.1.6", // Very old
"topthink/think-captcha": "^1.0.9", // TP5-era
```
**Impact:** `dev-master` can introduce breaking changes. Old versions may contain known vulnerabilities.
**Fix approach:** Pin stable versions. Run `composer audit` regularly.
---
### A3. Framework Code Committed to Repository
**Files:**
- `thinkphp/` directory
- `.gitignore` line 2
**Issue:** The entire ThinkPHP framework source is committed to the repository instead of being managed purely through Composer.
**Impact:** Repository bloat. Difficulty tracking framework upgrades.
**Fix approach:** Remove `thinkphp/` from the repository and manage solely through Composer.
---
### A4. No API Versioning
**Files:**
- `application/api/controller/` (all files)
**Issue:** API endpoints have no version prefix. Any breaking change affects all clients immediately.
**Impact:** Inability to make breaking API changes without disrupting existing clients.
**Fix approach:** Add API versioning (e.g., `/api/v1/`) using ThinkPHP routes.
---
## Domain-Specific
### D1. Lottery Data Accuracy Not Guaranteed (HIGH)
**Files:**
- `application/index/controller/Index.php` lines 20-58
**Issue:** Lottery data sourced from a single third-party API with no verification, backup source, or data integrity checks. Raw `openCode` string is split by comma and mapped directly without range validation.
**Impact:** If the external source provides incorrect data, the system propagates errors silently.
**Fix approach:** Add range validation (numbers 1-49), implement backup data sources, and maintain an audit log.
---
### D2. Data Validation Gaps for Lottery Records
**Files:**
- `sql/macaujc_history.sql`
- `application/admin/model/History.php`
- `application/admin/validate/History.php`
**Issue:** The History model has no validation rules. The SQL schema has loose VARCHAR constraints without format validation.
**Impact:** Invalid lottery records can be stored (negative numbers, out-of-range values, malformed dates).
**Fix approach:** Add validation rules to the History model. Implement data normalization accessors.
---
### D3. Hardcoded Year in Scraping Endpoint
**Files:**
- `application/index/controller/Index.php` line 23
**Issue:** The API URL hardcodes the year `2026`:
```php
$res = $client->request('GET', 'https://history.macaumarksix.com/history/macaujc2/y/2026');
```
**Impact:** Requires annual manual update. After January 1st of a new year, new data won't be fetched.
**Fix approach:** Make the year dynamic with `date('Y')` or accept it as a parameter.
---
## Test Coverage Gaps
### T1. Zero Automated Tests
**Files:** Entire application codebase
**Issue:** No test files exist anywhere in the application directory. No `phpunit.xml` configuration.
**Risk:** High
**Impact:** No automated regression testing. Code changes carry high risk of introducing bugs. Cannot safely refactor.
**Fix approach:** Set up PHPUnit. Start with unit tests for Auth library, then API endpoints, then lottery data handling.
---
## Additional Concerns
### AC1. No .env.example Template
**Files:**
- `.gitignore` line 15
- `.env` (present)
**Issue:** `.env` is gitignored but there is no `.env.example` template.
**Impact:** New developers cannot set up the environment without guessing variable names.
**Fix approach:** Create `.env.example` with all required variables documented.
---
### AC2. Debug Mode Default Risk
**Files:**
- `application/config.php` line 21
**Issue:** `'app_debug' => Env::get('app.debug', false)` defaults to `false`, but if `.env` is misconfigured in production with `debug = true`, full stack traces are exposed.
**Impact:** Detailed error messages with file paths and SQL queries exposed to end users.
**Fix approach:** Explicitly set `app_debug=false` in production config. Never rely on defaults.
---
### AC3. Install Script Redirect Logic Still Present
**Files:**
- `public/index.php` lines 16-19
**Issue:** The entry point checks for `install.lock` and redirects to `./install.php` if missing. Although `public/install.php` has been deleted, the redirect logic remains.
**Impact:** If someone restores `install.php`, the installation wizard becomes publicly accessible.
**Fix approach:** Remove the redirect logic from `public/index.php` after installation is confirmed complete.
---
*Concerns audit: 2026-04-21*
+599
View File
@@ -0,0 +1,599 @@
# Coding Conventions
**Analysis Date:** 2026-04-21
## Naming Patterns
**Files:**
- Controllers: PascalCase, directory structure mirrors URL path. Example: `application/admin/controller/user/User.php` -> `/admin/user/user`
- Models: PascalCase in parallel directory structure. Example: `application/admin/model/User.php`
- Validates: PascalCase. Example: `application/admin/validate/User.php`
- Libraries: PascalCase. Example: `application/admin/library/Auth.php`, `application/common/library/Upload.php`
- Traits: PascalCase. Example: `application/admin/library/traits/Backend.php`
- Behaviors: PascalCase. Example: `application/admin/behavior/AdminLog.php`
- Language files: lowercase `zh-cn.php`, mirroring controller directory structure
- View templates: lowercase `.html`, matching action names. Example: `application/admin/view/user/user/index.html`
- JS files: lowercase, mirroring controller path. Example: `public/assets/js/backend/user/user.js`
- CSS: lowercase `.css` with `.min.css` variants
- Less: lowercase `.less` (source files in `public/assets/less/`)
- Common helper functions: snake_case. Example: `build_select()`, `cdnurl()`, `datetime()`, `letter_avatar()`
**Functions/Methods:**
- Controller action methods: lowercase with underscores. Examples: `index()`, `add()`, `edit()`, `del()`, `recyclebin()`, `get_field_list()`, `get_controller_list()`
- Controller protected methods: camelCase. Examples: `buildparams()`, `selectpage()`, `getDataLimitAdminIds()`, `loadlang()`, `assignconfig()`
- Model accessors/mutators: ThinkPHP convention `get{FieldName}Attr()` / `set{FieldName}Attr()`. Example: `getPrevtimeTextAttr()`, `setJointimeAttr()`, `getAvatarAttr()`, `setBirthdayAttr()`
- Model list methods: PascalCase `getStatusList()`, `getGenderList()`
- Model static methods: camelCase. Example: `User::money()`, `User::score()`, `User::nextlevel()`
- Library methods: camelCase. Example: `Auth::login()`, `Auth::getUserinfo()`, `Upload::init()`
- Global functions: snake_case, wrapped in `if (!function_exists('...'))`. Example: `__()`, `format_bytes()`, `check_cors_request()`, `xss_clean()`
- Addon methods: camelCase following ThinkPHP convention
**Variables:**
- Controller properties: camelCase. Examples: `$noNeedLogin`, `$model`, `$searchFields`, `$relationSearch`, `$dataLimit`
- Library private properties: underscore prefix + camelCase. Examples: `$_error`, `$_logined`, `$_user`, `$_token` (in `application/common/library/Auth.php`)
- Library protected properties: camelCase, no prefix. Examples: `$keeptime`, `$requestUri`, `$allowFields`
- Local variables: camelCase. Examples: `$tableList`, `$fieldlist`, `$insert_data`, `$changedata`
- Database fields: lowercase with underscores. Examples: `createtime`, `updatetime`, `group_id`, `loginip`
- Request input: via `$this->request->post('row/a')` (array), `$this->request->request('keyField')` (single)
**Types/Namespaces:**
- Namespace convention: lowercase `app\admin\controller`, `app\common\model`, `app\api\library`
- Class names: PascalCase. Example: `class User extends Backend`
- Addon namespace: `addons\{name}\` mapped via PSR-4 in `composer.json`
- Fast tools namespace: `fast\` maps to `extend/fast/` directory
## Code Style
**Formatting:**
- 4-space indentation throughout (no tabs)
- Opening brace on same line for classes/functions: `class User extends Backend\n{`
- Opening brace on next line for control structures in some files, same line in others (inconsistent)
- Closing PHP tag `?>` omitted at end of files
- Files start with `<?php` followed by a blank line
- Blank line after namespace declaration
- Blank line after last `use` statement before class declaration
**PHP Version:** PHP >= 7.4.0 (per `composer.json`)
- Union types in catch blocks: `catch (ValidateException|PDOException|Exception $e)`
- Null coalescing: `$value ?? ''`
- Match expressions: Not used
- Arrow functions: Not used
- Typed properties: Not used
**Linting/Formatting Tools:**
- No project-level `.php-cs-fixer.php`, `phpcs.xml`, `.editorconfig`, or `biome.json`
- No ESLint or stylelint configured
- PhpStorm `.idea/` directory present with PHP 7.4 language level configured
- Static analysis tools (PHPStan, PHPCS, MessDetector) configured in IDE but not activated (`transferred=true`)
## Import Organization
**Order in PHP files:**
1. `namespace app\admin\controller;`
2. `use` statements for app classes: `use app\common\controller\Backend;`
3. `use` statements for ThinkPHP classes: `use think\Db;`, `use think\Config;`, `use think\Exception;`
4. `use` statements for vendor/third-party classes: `use fast\Tree;`
5. No grouping separators between use statement categories
**Example from `application/admin/controller/user/User.php`:**
```php
namespace app\admin\controller\user;
use app\common\controller\Backend;
use app\common\library\Auth;
```
**Example from `application/common/controller/Backend.php`:**
```php
namespace app\common\controller;
use app\admin\library\Auth;
use think\Config;
use think\Controller;
use think\Hook;
use think\Lang;
use think\Loader;
use think\Model;
use think\Session;
use fast\Tree;
use think\Validate;
```
**Path Aliases:**
- No PSR-4 path aliases configured beyond `addons\\` -> `addons/` in `composer.json`
- ThinkPHP autoload handles `app\` namespace mapping automatically
- `fast\` namespace handled by ThinkPHP custom autoloader -> `extend/fast/`
## ThinkPHP Conventions
**Model Table Naming:**
- `$name` property defines the table name without prefix. Example: `protected $name = 'user';` maps to `{prefix}user`
- Table prefix configured in `application/config.php` database section
- All models extend `think\Model`
**Timestamp Convention:**
- Auto timestamp type: `protected $autoWriteTimestamp = 'int';` (Unix integer timestamps)
- Create field name: `protected $createTime = 'createtime';`
- Update field name: `protected $updateTime = 'updatetime';`
**Controller Initialization:**
- `_initialize()` method called by ThinkPHP before each action
- Always calls `parent::_initialize()` first
- Sets `$this->model` instance in `_initialize()`
- Assigns view data: `$this->view->assign("statusList", ...)`
**Magic Model Methods:**
- Dynamic finders via `@method` PHPDoc annotations
- `getBy{Field}()` auto-generated by ThinkPHP for any column
- Example: `@method static mixed getByUsername($str)` in `application/common/model/User.php`
- Usage: `\app\common\model\User::getByMobile($mobile)`
**AJAX Detection Pattern:**
```php
if ($this->request->isAjax()) {
// Return JSON response
return json(['total' => $list->total(), 'rows' => $list->items()]);
}
return $this->view->fetch();
```
## FastAdmin Patterns
**Backend Base Class (`app\common\controller\Backend`):**
- All admin controllers extend this class
- Provides via trait `app\admin\library\traits\Backend`:
- `index()` - List with pagination and filtering
- `add()` - Create record
- `edit($ids)` - Update record
- `del($ids)` - Soft delete
- `destroy($ids)` - Hard delete (from recycle bin)
- `restore($ids)` - Restore from recycle bin
- `multi($ids)` - Batch update
- `recyclebin()` - Recycle bin list
- `import()` - Excel/CSV import
- `selectpage()` - SelectPage dropdown data
- Key configurable properties:
- `$noNeedLogin = []` - Methods skipping authentication entirely
- `$noNeedRight = []` - Methods skipping permission check (still need login)
- `$model = null` - Associated model instance
- `$searchFields = 'id'` - Fields for quick search
- `$relationSearch = false` - Whether to use table alias in queries
- `$dataLimit = false` - Data scope: `auth`/`personal`/`false`
- `$dataLimitField = 'admin_id'` - Field for data restriction
- `$dataLimitFieldAutoFill = true` - Auto-fill restriction field
- `$modelValidate = false` - Enable model-level validation
- `$modelSceneValidate = false` - Enable scene-based validation
- `$multiFields = 'status'` - Fields allowed in batch operations
- `$selectpageFields = '*'` - Fields shown in SelectPage
- `$excludeFields = ""` - Fields to exclude from form submission
- `$importHeadType = 'comment'` - Import header type: `comment`/`name`
- `$layout = 'default'` - Template layout name
**Standard CRUD Controller Pattern:**
```php
/**
* 会员管理
*
* @icon fa fa-user
*/
class User extends Backend
{
protected $relationSearch = true;
protected $searchFields = 'id,username,nickname';
/**
* @var \app\admin\model\User
*/
protected $model = null;
public function _initialize()
{
parent::_initialize();
$this->model = new \app\admin\model\User;
}
}
```
**CRUD Generation:**
- Via `php think crud` command
- Flags: `--table`, `--controller`, `--model`, `--fields`, `--force`, `--delete`, `--menu`
- Extended flags: `--setcheckboxsuffix`, `--enumradiosuffix`, `--imagefield`, `--filefield`, etc.
**Auth Patterns:**
- Admin auth: `app\admin\library\Auth` - backend admin user authentication
- User auth: `app\common\library\Auth` - frontend/member user authentication
- Both use singleton: `Auth::instance()`
- Permission check: `$this->auth->check($path)` where `$path = 'controller/action'`
- Login check: `$this->auth->isLogin()`
- Super admin check: `$this->auth->isSuperAdmin()`
**API Controller Pattern (`app\common\controller\Api`):**
- API controllers extend this (NOT `think\Controller`)
- Does NOT extend `think\Controller` - standalone class with `__construct()`
- Response format: `{'code': 1|0, 'msg': '', 'time': <timestamp>, 'data': ...}`
- Success: `$this->success($msg, $data)` (code=1, HTTP 200)
- Error: `$this->error($msg, $data, $httpCode)` (code=0)
- HTTP status codes: 401 for unauth, 403 for forbidden
- API annotations for doc generation: `@ApiMethod(POST)`, `@ApiParams(name="...", type="string", required=true, description="...")`
- Default request filter: `trim,strip_tags,htmlspecialchars`
**Frontend Controller Pattern (`app\common\controller\Frontend`):**
- Index module controllers extend this
- Similar to Backend but for public-facing pages
- Uses `app\common\library\Auth` for member auth
- Layout can be empty string for no layout: `protected $layout = '';`
## Language File Structure
**Directory Layout:**
```
application/{module}/lang/zh-cn.php # Module-level translations
application/{module}/lang/zh-cn/controller.php # Controller translations
application/{module}/lang/zh-cn/sub/controller.php # Nested controller translations
application/{module}/lang/{locale}/controller.php # Other locales
```
**Observed locales:**
- `zh-cn` (Simplified Chinese) - primary language for all modules
- `en` - only exists for `application/index/lang/en/index.php`
**File Format:**
```php
return [
'Id' => 'ID',
'Group_id' => '组别ID',
'Username' => '用户名',
'Leave password blank if dont want to change' => '不修改密码请留空',
];
```
**Usage Pattern:**
- `__('Key')` - Simple translation
- `__('Key %s', $value)` - Parameterized translation
- Keys use PascalCase for field names (matching DB column names)
- Keys use sentence case for messages
- Language auto-loaded by controller in `loadlang()` method
- Language detection: `$this->request->langset()` with regex validation, defaults to `zh-cn`
## Template Syntax
**Template Engine:** ThinkPHP built-in template engine
**File Extension:** `.html`
**Template Tags Used:**
| Tag | Example |
|-----|---------|
| Output | `{$variable}`, `{$var\|htmlentities}`, `{$var\|default='default'}` |
| Function | `{:__('Dashboard')}`, `{:build_heading()}`, `{:build_toolbar('refresh,edit,del')}` |
| Condition | `{if condition="$auth->check('dashboard')"}`, `{if !IS_DIALOG}`, `{/if}` |
| If-else shorthand | `{:defined('IS_DIALOG') && IS_DIALOG ? 'is-dialog' : ''}` |
| Loop | `{foreach $breadcrumb as $vo}`, `{/foreach}` |
| Include | `{include file="common/meta" /}` |
| Layout placeholder | `{__CONTENT__}` |
| ThinkPHP config | `{$Think.config.fastadmin.breadcrumb}` |
| Inline PHP | `{:$auth->check('user/user/multi')?'':'hide'}` |
**Layout Structure:**
```
application/{module}/view/layout/default.html # Layout wrapper with {__CONTENT__}
application/{module}/view/{controller}/index.html # Page content
application/{module}/view/common/meta.html # Shared <head> section
application/{module}/view/common/script.html # Shared JS loading
application/{module}/view/common/header.html # Header fragment
application/{module}/view/common/menu.html # Sidebar menu
application/{module}/view/common/control.html # Control bar fragment
```
**CSS Framework:** Bootstrap 3 with AdminLTE theme
**Common Classes:**
- Layout: `.panel`, `.panel-default`, `.panel-intro`, `.panel-heading`, `.panel-body`
- Table: `.table`, `.table-striped`, `.table-bordered`, `.table-hover`, `.table-nowrap`
- Buttons: `.btn`, `.btn-primary`, `.btn-success`, `.btn-danger`, `.btn-info`, `.btn-xs`
- Form: `.form-control`, `.selectpicker`, `.form-group`, `.control-label`
- FastAdmin custom: `.btn-dialog`, `.btn-addtabs`, `.btn-ajax`, `.btn-click`, `.searchit`
## JS AMD Module Pattern (RequireJS)
**Module Definition Pattern:**
```javascript
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
var Controller = {
index: function () {
Table.api.init({
extend: {
index_url: 'user/user/index',
add_url: 'user/user/add',
edit_url: 'user/user/edit',
del_url: 'user/user/del',
multi_url: 'user/user/multi',
table: 'user',
}
});
var table = $("#table");
table.bootstrapTable({
url: $.fn.bootstrapTable.defaults.extend.index_url,
pk: 'id',
sortName: 'user.id',
columns: [[
{checkbox: true},
{field: 'id', title: __('Id'), sortable: true},
{field: 'operate', title: __('Operate'), table: table,
events: Table.api.events.operate,
formatter: Table.api.formatter.operate}
]]
});
Table.api.bindevent(table);
},
add: function () {
Controller.api.bindevent();
},
edit: function () {
Controller.api.bindevent();
},
api: {
bindevent: function () {
Form.api.bindevent($("form[role=form]"));
}
}
};
return Controller;
});
```
**RequireJS Configuration Files:**
- `public/assets/js/require-backend.js` - Backend module config (minified as `require-backend.min.js`)
- `public/assets/js/require-frontend.js` - Frontend module config (minified)
- `public/assets/js/require-table.js` - Bootstrap-table wrapper module
- `public/assets/js/require-form.js` - Form handling module
- `public/assets/js/require-upload.js` - Upload module
**Core JS Modules:**
- `Fast` / `Fast.api` - Core API: `ajax()`, `open()`, `cdnurl()`, `fixurl()`, `selectedids()`
- `Backend` - Admin: sidebar badges, tab management, dialog/_ajax handlers
- `Frontend` - Frontend: captcha sending, touch swipe for sidebar
- `Table` - Bootstrap-table wrapper: `api.init()`, `api.bindevent()`, formatters, events
- `Form` - Form validation and submission: `api.bindevent()`
- `Template` - art-template JS template engine
- `Moment` - Date/time formatting (with `moment/locale/zh-cn`)
**Global Objects (set on `window`):**
- `window.Backend` - Backend namespace
- `window.Frontend` - Frontend namespace
- `window.Config` - Server-rendered config object
- `window.Toastr` - Toastr notification library
- `window.Layer` - Layer popup library (layui-based)
- `window.Table` - (via require) Table module
- `window.Form` - (via require) Form module
- `window.Template` - Template engine
- `window.Moment` - Moment.js
**Controller JS File Convention:**
- Location: `public/assets/js/backend/{controller_path}.js`
- Standard methods: `Controller.index()`, `Controller.add()`, `Controller.edit()`
- Sub-controller: `public/assets/js/backend/auth/admin.js`
- Each controller JS is loaded dynamically based on `Config.jsname`
**Button/Action Patterns in JS:**
- `.btn-dialog` / `.dialogit` - Opens URL in Layer dialog
- `.btn-addtabs` / `.addtabsit` - Opens URL in new tab
- `.btn-ajax` / `.ajaxit` - Sends AJAX request
- `.btn-click` / `.clickit` - Custom click handler
- `.searchit` - Triggers table search with field/value
**Table Formatter Patterns:**
- `Table.api.formatter.image` / `.images` - Image display
- `Table.api.formatter.datetime` - Date formatting
- `Table.api.formatter.status` - Status badge
- `Table.api.formatter.normal` - Generic label with color
- `Table.api.formatter.search` - Clickable search link
- `Table.api.formatter.operate` - Action buttons (edit/del)
- `Table.api.formatter.toggle` - Toggle switch
- `Table.api.formatter.flag` / `.label` - Multi-value flags
## CSS Class Naming
**Convention:** Bootstrap 3 + AdminLTE + FastAdmin extensions
**CSS Source:** Less files compiled to CSS
- `public/assets/less/backend.less` -> `public/assets/css/backend.css`
- `public/assets/less/frontend.less` -> `public/assets/css/frontend.css`
- `public/assets/less/bootstrap.less` -> `public/assets/css/bootstrap.css`
- `public/assets/css/fastadmin.css` - FastAdmin base overrides
- `public/assets/css/index.css` - Index page styles
- `public/assets/css/user.css` - User center styles
**Common Panel Pattern:**
```html
<div class="panel panel-default panel-intro">
{:build_heading()}
<div class="panel-body">
<div id="toolbar" class="toolbar">
{:build_toolbar('refresh,edit,del')}
</div>
<table id="table" class="table table-striped table-bordered table-hover table-nowrap"
data-operate-edit="{:$auth->check('user/user/edit')}"
data-operate-del="{:$auth->check('user/user/del')}"
width="100%">
</table>
</div>
</div>
```
## Error Handling
**Controller Layer:**
- `$this->error($message)` - Returns error, terminates execution
- `$this->success($message, $data)` - Returns success response
- Backend: throws `HttpResponseException` internally
- API: sets HTTP status codes (401/403) via header
**Transaction Pattern:**
```php
Db::startTrans();
try {
// database operations
Db::commit();
} catch (ValidateException|PDOException|Exception $e) {
Db::rollback();
$this->error($e->getMessage());
}
if ($result === false) {
$this->error(__('No rows were inserted'));
}
$this->success();
```
**Model Event Hooks:**
```php
protected static function init()
{
self::beforeWrite(function ($row) {
$changed = $row->getChangedData();
if (isset($changed['password'])) {
$salt = \fast\Random::alnum();
$row->password = \app\common\library\Auth::instance()->getEncryptPassword($changed['password'], $salt);
$row->salt = $salt;
}
});
}
```
## Logging
**Framework:** ThinkPHP built-in logging
- Configurable driver (file, etc.) in `application/config.php`
- Custom log library: `application/common/library/Log.php`
- Admin log behavior: `application/admin/behavior/AdminLog.php` - auto-logs admin actions to database
## Comments
**JSDoc/TSDoc Pattern:**
- All public/protected methods have PHPDoc with Chinese descriptions
- Controller methods: brief action description
- API methods: `@ApiMethod` + `@ApiParams` annotations for API doc generation
**Controller Class Doc:**
```php
/**
* 会员管理
*
* @icon fa fa-user
*/
class User extends Backend
```
**Method Doc:**
```php
/**
* 会员登录
*
* @ApiMethod (POST)
* @ApiParams (name="account", type="string", required=true, description="账号")
* @ApiParams (name="password", type="string", required=true, description="密码")
*/
public function login()
```
**Model Doc:**
```php
/**
* 会员模型
* @method static mixed getByUsername($str) 通过用户名查询用户
* @method static mixed getByNickname($str) 通过昵称查询用户
*/
class User extends Model
```
**Function Doc:**
```php
/**
* 将字节转换为可读文本
* @param int $size 大小
* @param string $delimiter 分隔符
* @param int $precision 小数位数
* @return string
*/
function format_bytes($size, $delimiter = '', $precision = 2)
```
## Function Design
**Size:**
- Controller action methods: 10-30 lines (when extending Backend, most logic is in trait)
- Backend trait methods: 30-80 lines (`index`, `add`, `edit`, `del`)
- Import method: ~130 lines (`application/admin/library/traits/Backend.php`)
- Library methods: 10-50 lines
- Global functions: 5-30 lines
**Parameters:**
- Primary key as method parameter: `edit($ids = null)`, `del($ids = "")`
- POST form data: `$this->request->post('row/a')` (returns associative array)
- Query params: `$this->request->request('key')`, `$this->request->get('filter')`
**Return Values:**
- AJAX: `json(['total' => N, 'rows' => [...]])`
- Non-AJAX: `$this->view->fetch()`
- API: Always JSON via `$this->success()` / `$this->error()`
## Module Design
**PHP Exports:**
- No explicit exports; PSR-4 autoloading handles class loading
- Language files: `return [...]` array
- Config files: `return [...]` array
**JS Exports:**
- AMD `define()` with `return Controller;` pattern
- No ES modules, no CommonJS
**No Barrel Files:** Direct imports throughout, no index/re-export files
**Addon Structure:**
```
addons/{addon_name}/
├── config.php # Addon configuration (returns array)
├── Command.php # Main addon class (extends \fast\Addons)
├── controller/Index.php # Addon controller
├── library/Output.php # Addon library
└── info.ini # Addon metadata
```
## Where to Add New Code
**New Admin Feature (CRUD):**
- Controller: `application/admin/controller/{module}/{Name}.php`
- Model: `application/admin/model/{Name}.php`
- Validate: `application/admin/validate/{Name}.php`
- Language: `application/admin/lang/zh-cn/{module}/{name}.php`
- View: `application/admin/view/{module}/{name}/index.html`, `add.html`, `edit.html`
- JS: `public/assets/js/backend/{module}/{name}.js`
- Or auto-generate: `php think crud --table={table} --controller={name}`
**New API Endpoint:**
- Controller: `application/api/controller/{Name}.php` (extends `app\common\controller\Api`)
- Response via `$this->success()` / `$this->error()`
- Add `@ApiMethod` and `@ApiParams` annotations for doc generation
**New Common Library:**
- Location: `application/common/library/{Name}.php`
- Use singleton `instance()` pattern if needed
**New Global Function:**
- Location: `application/common.php`
- Wrap in `if (!function_exists('name')) { ... }`
**New Module-Level Function:**
- Location: `application/{module}/common.php`
**New Model:**
- Shared model: `application/common/model/{Name}.php`
- Admin-only model: `application/admin/model/{Name}.php`
- Extend `think\Model`, set `$name`, `$autoWriteTimestamp`, `$createTime`, `$updateTime`
**New Validate:**
- Location: `application/admin/validate/{Name}.php`
- Extend `think\Validate`, define `$rule`, `$field`, `$scene`
---
*Convention analysis: 2026-04-21*
+252
View File
@@ -0,0 +1,252 @@
# External Integrations
**Analysis Date:** 2026-04-21
## APIs & External Services
**Lottery Data Scraping:**
- URL: `https://history.macaumarksix.com/history/macaujc2/y/{year}` (e.g., `2026`)
- Purpose: Fetching Macau Mark Six lottery historical results
- Client: `guzzlehttp/guzzle` ^7.10
- Integration point: `D:\code\php\amlhc\application\index\controller\Index.php` method `get_history()` (lines 20-58)
- Data flow: Scraped JSON response contains `expect` (period number), `openTime`, `openCode` (comma-separated numbers) -> parsed and upserted into `fa_history` table
**FastAdmin Official API:**
- URL: `https://api.fastadmin.net` (`application/config.php` `fastadmin.api_url`)
- Purpose: Plugin marketplace, version checks, addon updates
**WeChat (EasyWeChat SDK):**
- Package: `overtrue/wechat` ^4.6
- Purpose: WeChat OAuth login, messaging
- Integration point: Addon-level, managed via addon configuration
## Data Storage
**Databases:**
- **MySQL** - Primary database
- Connection via env vars: `database.hostname`, `database.database`, `database.username`, `database.password`, `database.hostport` (`D:\code\php\amlhc\application\database.php`)
- Charset: `utf8mb4` (configurable via `database.charset`)
- Table prefix: `fa_` (configurable via `database.prefix`)
- PDO driver required (`ext-pdo`)
- Single server mode by default (`deploy: 0`), supports master-slave replication
- Key tables: `fa_admin`, `fa_auth_group`, `fa_auth_rule`, `fa_user`, `fa_attachment`, `fa_history`, `fa_num`, `fa_command`
**Caching:**
- **File-based cache** - Default cache driver (`application/config.php` `cache.type => File`, path: `CACHE_PATH`)
- **Redis** - Used for queue system (`D:\code\php\amlhc\application\extra\queue.php`)
- Host: `127.0.0.1`, Port: `6379`
- Password: empty by default
- Database: `0` (select)
- Persistent connection: disabled
- Expire: `0` (no expiration on tasks)
- **Token storage** - MySQL-backed (`application/config.php` `token.type => Mysql`)
- **Menu cache** - Uses ThinkPHP cache with key `"__menu__"` (`D:\code\php\amlhc\application\admin\library\Auth.php` line 461)
- Session supports Redis/memcache drivers but defaults to file-based
**File Storage:**
- **Local filesystem** - Default upload storage
- Upload URL: `ajax/upload` (`D:\code\php\amlhc\application\extra\upload.php`)
- Upload path pattern: `/uploads/{year}{mon}{day}/{filemd5}{.suffix}`
- Max upload size: 10MB
- Allowed types: `jpg,png,bmp,jpeg,gif,webp,zip,rar,wav,mp4,mp3,webm`
- CDN support available via `cdnurl` config (empty by default)
- Chunked upload support available (disabled by default, chunk size: 2MB)
- Upload handled by: `D:\code\php\amlhc\application\api\controller\Common.php` `upload()` method with `app\common\library\Upload` class
## Authentication & Identity
**Backend Admin Auth:**
- Class: `D:\code\php\amlhc\application\admin\library\Auth.php` (extends `fast\Auth`)
- Password hashing: `md5(md5(password) . salt)` (double MD5 with salt)
- Session-based: Stores admin data in `Session::get('admin')`
- Role-based access control (RBAC): Admin -> AuthGroup -> AuthRule hierarchy
- Features:
- Login retry limit: 10 attempts, 1-day cooldown (`fastadmin.login_failure_retry`)
- IP change detection enabled (`fastadmin.loginip_check: true`)
- Unique login option available (`fastadmin.login_unique: false` by default)
- Safe code validation: MD5-based checksum of username + partial password + token key
- Auto-login via `keeplogin` cookie with time-limited key
- Tables: `fa_admin`, `fa_auth_group`, `fa_auth_group_access`, `fa_auth_rule`
**Frontend User Auth:**
- Class: `D:\code\php\amlhc\application\common\library\Auth.php`
- Token-based: UUID tokens stored in MySQL token table
- Token default lifetime: 2,592,000 seconds (30 days)
- Password hashing: Same double MD5 + salt as admin
- Features:
- Login by username, email, or mobile
- User groups and rules (`fa_user_group`, `fa_user_rule`)
- Score and money log tracking (`fa_money_log`, `fa_score_log`)
- Hook events: `user_init_successed`, `user_register_successed`, `user_login_successed`, `user_logout_successed`, `user_changepwd_successed`, `user_delete_successed`
- Tables: `fa_user`, `fa_user_group`, `fa_user_rule`
**API Auth:**
- Token passed via `HTTP_TOKEN` header, `token` POST param, or Cookie
- Controller base: `D:\code\php\amlhc\application\common\controller\Api.php`
- HTTP 401 for unauthorized, 403 for forbidden
- CORS handling via `check_cors_request()`
**Captcha:**
- ThinkPHP captcha (`topthink/think-captcha` ^1.0.9) - Image-based, 4 characters, size 130x40
- Text captcha - For user registration (`fastadmin.user_register_captcha: text`)
- Login captcha: disabled by default (`fastadmin.login_captcha: false`)
- Generated via: `D:\code\php\amlhc\application\api\controller\Common.php` `captcha()` method (large format: 350x150)
## Queue System
**Think-Queue (Redis-backed):**
- Package: `topthink/think-queue` 1.1.6
- Connector: Redis (`D:\code\php\amlhc\application\extra\queue.php`)
- Default queue: `default`
- Config: `application/extra/queue.php`
- Redis host: `127.0.0.1:6379`
- No password by default
- Persistent connection: disabled
- Task expire: `0` (no expiration)
- CLI: `php think queue:work` / `php think queue:listen` for processing
## Addon/Plugin System
**FastAdmin Addons:**
- Package: `fastadminnet/fastadmin-addons` ~1.4.0
- Location: `addons/` directory
- Config: `D:\code\php\amlhc\application\extra\addons.php`
- Autoload: `false` (manual loading)
- Hooks: empty by default (configured per addon)
- Routes: empty by default (configured per addon)
- PSR-4 autoload: `addons\` -> `addons/` (`composer.json`)
- Addon lifecycle: `install()`, `uninstall()`, `enable()`, `disable()` methods
- Example addon: `D:\code\php\amlhc\addons\command\Command.php`
- Installs menu entries via `Menu::create()`
- Deletes menu on uninstall via `Menu::delete()`
- Enable/disable toggles menu visibility
- Pure mode: removes `application/`, `public/`, `assets/` from addon packages when enabled (`fastadmin.addon_pure_mode: true`)
- Unknown source addons: blocked by default (`fastadmin.unknownsources: false`)
- Backup global files on addon enable/disable: enabled (`fastadmin.backup_global_files: true`)
- CLI: `php think addon` for addon management
- Admin controller: `D:\code\php\amlhc\application\admin\controller\Addon.php`
## ThinkPHP Hooks & Behaviors
**Hook Integration Points:**
- `upload_config_init` - Called when upload config is initialized (`Backend.php`, `Frontend.php`, `Api.php`)
- `config_init` - Called after config assembly (`Backend.php`, `Frontend.php`)
- `admin_nologin` - Fired when admin access is denied due to no login (`Backend.php` line 145)
- `admin_nopermission` - Fired when admin access is denied due to no permission (`Backend.php` line 158)
- `admin_sidebar_begin` - Fired before sidebar rendering (`Auth.php` line 429)
- `user_init_successed` - Fired on successful frontend user init (`common/library/Auth.php` line 115)
- `user_register_successed` - Fired on user registration (`common/library/Auth.php` line 194)
- `user_login_successed` - Fired on user login (`common/library/Auth.php` line 334)
- `user_logout_successed` - Fired on user logout (`common/library/Auth.php` line 256)
- `user_changepwd_successed` - Fired on password change (`common/library/Auth.php` line 283)
- `user_delete_successed` - Fired on user deletion (`common/library/Auth.php` line 474)
**Tags/Behaviors:** Configured in `application/tags.php` with `addon_begin` behavior hook
## Email
**Mailer:**
- Package: `fastadminnet/fastadmin-mailer` ^2.0.0
- SMTP Configuration (`D:\code\php\amlhc\application\extra\site.php`):
- Type: `1` (SMTP)
- Host: `smtp.qq.com`
- Port: `465` (SSL)
- Verification type: `2` (SSL/TLS)
- Username/password: configured via admin panel (empty by default)
- Mail from address: configured via admin panel
- Used for: email verification, password reset, notifications
- Config groups: `basic`, `email`, `dictionary`, `user`, `example`
## Monitoring & Observability
**Error Tracking:**
- None configured
**Logs:**
- File-based logging (`application/config.php` `log.type => File`, path: `LOG_PATH` typically `runtime/log/`)
- Level: empty array (logs all levels by default)
- Auto-record admin logs enabled (`fastadmin.auto_record_log: true`)
**Debug/Trace:**
- App debug mode: configurable via `app.debug` env var (default: `false`)
- App trace: configurable via `app.trace` env var (default: `false`)
- SQL explain: disabled by default
## CI/CD & Deployment
**Hosting:**
- Self-hosted PHP deployment
- Web server entry: `D:\code\php\amlhc\public\index.php`
- Router compatibility: `D:\code\php\amlhc\public\router.php` for PHP built-in server
- Admin entry: formerly `public/admin.php` (deleted per git status)
- Install script: formerly `public/install.php` (deleted per git status)
**CI Pipeline:**
- Not detected
## Environment Configuration
**Required env vars** (via `think\Env` in config files):
```
[app]
debug = false
trace = false
[database]
hostname = 127.0.0.1
database = fastadmin
username = root
password = (configured)
hostport = (configured)
prefix = fa_
charset = utf8mb4
debug = false
```
**Secrets location:**
- `.env` file (present, not committed)
- Database credentials in env vars
- SMTP credentials in admin-configurable site settings (`application/extra/site.php`)
- WeChat app credentials managed via WeChat addon
- Token key: hardcoded in `application/config.php` `token.key`
## Webhooks & Callbacks
**Incoming:**
- Not detected in base configuration
- Addons may register their own webhook endpoints
**Outgoing:**
- FastAdmin API calls to `https://api.fastadmin.net` for addon marketplace
- Lottery data scraping to `https://history.macaumarksix.com` (Guzzle HTTP GET)
- Email sending via SMTP (qq.com)
## Internationalization
**Supported Languages:**
- `zh-cn` (Simplified Chinese) - Default
- `en` (English) (`application/config.php` `allow_lang_list`)
- Multi-language: disabled by default (`lang_switch_on: false`)
- Language files in `application/*/lang/zh-cn/`
- Language loading per controller in base classes (`loadlang()` method)
- Recent additions: `D:\code\php\amlhc\application\admin\lang\zh-cn\command.php`, `D:\code\php\amlhc\application\admin\lang\zh-cn\history.php`
## CORS
**Allowed Origins:**
- `localhost`, `127.0.0.1` (`application/config.php` `fastadmin.cors_request_domain`)
- Configurable via `fastadmin.cors_request_domain`
- API module sets CORS headers in `D:\code\php\amlhc\application\api\controller\Common.php` `_initialize()` (line 26-28): exposes `__token__` header for cross-origin token retrieval
## Upload Integration
**Upload Flow:**
1. Client uploads to `ajax/upload` (index module) or `api/common/upload` (API module)
2. `app\common\library\Upload` class handles validation and storage
3. Files stored in `public/uploads/{year}{mon}{day}/{filemd5}{.suffix}`
4. Attachment record created in `fa_attachment` table via `app\common\model\Attachment`
5. CDN URL returned if `cdnurl` is configured
---
*Integration audit: 2026-04-21*
+178
View File
@@ -0,0 +1,178 @@
# Technology Stack
**Analysis Date:** 2026-04-21
## Languages
**Primary:**
- PHP >= 7.4 - Server-side application code (all `application/` and `addons/`)
- JavaScript (ES5) - Frontend client code (`public/assets/js/`)
**Secondary:**
- HTML/Think Template - View templates (`application/*/view/`)
## Runtime
**Environment:**
- PHP >= 7.4.0 (required by `composer.json`)
- Required extensions: `ext-json`, `ext-curl`, `ext-pdo`, `ext-bcmath`
**Package Manager:**
- Composer - PHP dependency management; lockfile `composer.lock` present
- npm - Frontend dependency management; `node_modules/` present
- Lockfiles: `composer.lock` (present), `package-lock.json` (not detected)
## Frameworks
**Core:**
- ThinkPHP 5.x (dev-master from `https://gitee.com/fastadminnet/framework.git`) - PHP MVC framework, the foundation of the entire application
- FastAdmin 1.6.1 - Admin backend framework built on ThinkPHP + Bootstrap; actual internal version `1.6.2.20260323` (from `application/config.php` `fastadmin.version`)
**Frontend:**
- RequireJS 2.x - AMD module loader for JavaScript (`public/assets/js/require-backend.js`, `require-frontend.js`)
- Bootstrap 3.4.1 (via `fastadmin-bootstrap`) - UI component framework
- jQuery 3.7.1 - DOM manipulation and AJAX
- AdminLTE - Admin dashboard theme (referenced in `require-backend.js` paths)
**Testing:**
- Not detected - No test framework configured (no PHPUnit, no `tests/` directory)
**Build/Dev:**
- Grunt 1.5.3 - Task runner for frontend asset build
- requirejs optimizer (r.js) - JS/CSS minification via custom `application/admin/command/Min/r`
- uglify - JavaScript minification
- parse-config-file + jsonminify - RequireJS config parsing during build
## Key Dependencies
**Critical:**
- `topthink/framework` dev-master - ThinkPHP core framework (Gitee mirror)
- `topthink/think-captcha` ^1.0.9 - CAPTCHA image generation
- `topthink/think-queue` 1.1.6 - Redis-backed job queue system
- `topthink/think-helper` ^1.0.7 - ThinkPHP utility helpers
- `fastadminnet/fastadmin-addons` ~1.4.0 - Plugin/addon system
- `fastadminnet/fastadmin-mailer` ^2.0.0 - Email sending
**Infrastructure:**
- `guzzlehttp/guzzle` ^7.10 - HTTP client for external API requests (used in `application/index/controller/Index.php` to scrape `macaumarksix.com`)
- `overtrue/pinyin` ^3.0 - Chinese Pinyin conversion
- `overtrue/wechat` ^4.6 - WeChat SDK integration
- `phpoffice/phpspreadsheet` ^1.29.1 - Excel/CSV import-export (used in `application/admin/library/traits/Backend.php` `import()` method)
**Frontend Libraries (via npm):**
- `fastadmin-bootstraptable` ^1.11.12 - Data table with search/sort/pagination
- `fastadmin-layer` ^3.5.6 - Modal/overlay dialogs
- `fastadmin-selectpage` ^1.1.1 - Select with autocomplete
- `fastadmin-nicevalidator` ^1.1.6 - Form validation
- `eonasdan-bootstrap-datetimepicker` ^4.17.49 - Date/time picker
- `bootstrap-daterangepicker` ~2.1.25 - Date range picker
- `bootstrap-select` ^1.13.18 - Enhanced select dropdown
- `jstree` ~3.3.2 - Tree view component
- `font-awesome` ^4.6.1 - Icon font
- `moment` ^2.10 - Date manipulation
- `art-template` (via fastadmin-arttemplate) ^3.1.4 - Template engine
- `toastr` ~2.1.3 - Notification toasts
- `jquery-slimscroll` ~1.3.8 - Custom scrollbar
- `jquery.cookie` ~1.4.1 - Cookie utility
- `sortablejs` ^1.12.0 - Drag and drop sorting
- `fastadmin-dragsort` ^1.0.5 - Drag sort plugin
- `fastadmin-addtabs` ^1.0.8 - Multi-tab navigation
- `fastadmin-citypicker` ^1.3.6 - City selector
- `fastadmin-cxselect` ^1.4.0 - Cascading select
- `bootstrap-slider` ^11.0.2 - Range slider
- `tableexport.jquery.plugin` ^1.20 - Table export to Excel/CSV/PDF
- `require-css` ~0.1.8 - CSS loading via RequireJS
## Configuration
**Environment:**
- Configuration via PHP arrays in `application/config.php`, `application/database.php`, `application/extra/*.php`
- Environment variable override via `think\Env` class (e.g., `Env::get('database.hostname', '127.0.0.1')`)
- `.env` file present for environment-specific overrides (secrets not inspected)
- Key configs: `site.php` (site settings, email SMTP), `upload.php` (file upload rules), `queue.php` (Redis connection), `addons.php` (plugin hooks/routes)
**Build:**
- `Gruntfile.js` - Build orchestration
- Build tasks: `deploy` (copy libs from node_modules to `public/assets/libs/`), `frontend:js`, `backend:js`, `frontend:css`, `backend:css` (RequireJS r.js optimization)
- Default task: `grunt` = `['deploy', 'frontend:js', 'backend:js', 'frontend:css', 'backend:css']`
- Output: `public/assets/js/require-backend.min.js`, `require-frontend.min.js`, etc.
## Platform Requirements
**Development:**
- PHP >= 7.4 with extensions: json, curl, pdo, bcmath
- MySQL database (utf8mb4 charset)
- Redis (for queue system)
- Node.js + npm (for frontend dependencies and Grunt build)
- Composer (for PHP dependencies)
- `.env` file configured with database credentials
**Production:**
- Self-hosted PHP deployment (no Docker detected)
- Apache or Nginx web server (with URL rewriting for ThinkPHP PATH_INFO)
- MySQL database with `fa_` table prefix
- Redis for queue processing (`think-queue`)
- `public/` directory as web root
- `application/admin/command/Install/install.lock` present - indicates installation completed
## Module Structure
The application uses ThinkPHP multi-module architecture:
| Module | Purpose | Location |
|--------|---------|----------|
| `admin` | Backend admin panel | `application/admin/` |
| `index` | Frontend website | `application/index/` |
| `api` | REST API endpoints | `application/api/` |
| `common` | Shared code | `application/common/` |
## Recently Added Components
**Num Controller/Model** - New "数字波色" (number color/wave) feature:
- Controller: `D:\code\php\amlhc\application\admin\controller\Num.php` - Returns number-to-color mapping via `getColorMap()` API endpoint
- Model: `D:\code\php\amlhc\application\admin\model\Num.php` - Simple model for `fa_num` table, no timestamp fields
- Used by `history.js` frontend to render colored number balls in lottery result tables
**Command Controller/Model** - Online CLI command management:
- Controller: `D:\code\php\amlhc\application\admin\controller\Command.php` - CRUD for CLI commands (crud/menu/min/api generation and execution)
- Model: `D:\code\php\amlhc\application\admin\model\Command.php` - Tracks command execution history with integer timestamps, status tracking
- Validate: `D:\code\php\amlhc\application\admin\validate\Command.php` - Empty validation rules
- Addon: `D:\code\php\amlhc\addons\command\` - Plugin wrapper with menu installation via `addons\command\Command.php`
- Output library: `D:\code\php\amlhc\addons\command\library\Output.php` - Extends `\think\console\Output` to capture command output
- Frontend: `D:\code\php\amlhc\public\assets\js\backend\command.js` - Complex UI with dynamic form, table field selection, relation config
**History Controller/Model** - Lottery history records:
- Controller: `D:\code\php\amlhc\application\admin\controller\History.php` - Standard CRUD (inherits Backend trait)
- Model: `D:\code\php\amlhc\application\admin\model\History.php` - Simple model for `fa_history` table, no timestamp fields
- Validate: `D:\code\php\amlhc\application\admin\validate\History.php` - Empty validation rules
- Frontend: `D:\code\php\amlhc\public\assets\js\backend\history.js` - Custom colored ball rendering, loads color map from Num API at `num/getColorMap`
- Index controller: `D:\code\php\amlhc\application\index\controller\Index.php` - `get_history()` scrapes `https://history.macaumarksix.com/history/macaujc2/y/2026` using Guzzle
**SQL Schema:** `D:\code\php\amlhc\sql\macaujc_history.sql` - Defines `macaujc_history` table with full lottery record fields (expect, open_code, wave, zodiac, odd_even, big_small, etc.)
## CLI Commands
Registered in `application/command.php`:
- `Crud` - Code generator for CRUD operations (`app\admin\command\Crud`)
- `Menu` - Menu generator (`app\admin\command\Menu`)
- `Install` - Installation wizard (`app\admin\command\Install`)
- `Min` - Asset minification (`app\admin\command\Min`)
- `Addon` - Addon management (`app\admin\command\Addon`)
- `Api` - API documentation generator (`app\admin\command\Api`)
## Custom Extensions (extend/fast/)
Located in `extend/fast/`:
- `Auth.php` - Authentication and permission library
- `Date.php` - Date/time utilities
- `Form.php` - Form builder/generator (largest utility)
- `Http.php` - HTTP request utilities
- `Pinyin.php` - Chinese pinyin wrapper
- `Random.php` - Random string generation
- `Rsa.php` - RSA encryption utilities
- `Tree.php` - Tree data structure utilities
- `Version.php` - Version comparison utilities
---
*Stack analysis: 2026-04-21*
+625
View File
@@ -0,0 +1,625 @@
# Codebase Structure
**Analysis Date:** 2026-04-21
## Directory Layout
```
D:\code\php\amlhc\
├── application/ # ThinkPHP application code
│ ├── admin/ # Backend admin module
│ │ ├── behavior/ # Admin behavior hooks
│ │ ├── command/ # CLI commands
│ │ │ ├── Addon.php # Addon command
│ │ │ ├── Api.php # API doc generator
│ │ │ │ ├── lang/zh-cn.php
│ │ │ │ └── library/ # Builder.php, Extractor.php
│ │ │ ├── Crud.php # CRUD code generator
│ │ │ │ └── stubs/ # Template stubs
│ │ │ ├── Install/ # Installation wizard
│ │ │ │ ├── install.lock
│ │ │ │ └── zh-cn.php
│ │ │ ├── Menu.php # Menu generator
│ │ │ └── Min.php # JS/CSS minifier
│ │ ├── controller/ # Admin controllers (18 total)
│ │ │ ├── Addon.php # Plugin management
│ │ │ ├── Ajax.php # Shared AJAX endpoints
│ │ │ ├── Category.php # Category management
│ │ │ ├── Command.php # Online command execution
│ │ │ ├── Dashboard.php # Dashboard statistics
│ │ │ ├── History.php # Lottery history management
│ │ │ ├── Index.php # Admin login/home/logout
│ │ │ ├── Num.php # Lottery number/color mapping
│ │ │ ├── auth/
│ │ │ │ ├── Admin.php # Admin user management
│ │ │ │ ├── Adminlog.php # Admin operation log
│ │ │ │ ├── Group.php # Role group management
│ │ │ │ └── Rule.php # Permission rule management
│ │ │ ├── general/
│ │ │ │ ├── Attachment.php # File attachment management
│ │ │ │ ├── Config.php # System configuration
│ │ │ │ └── Profile.php # Admin profile management
│ │ │ └── user/
│ │ │ ├── Group.php # User group management
│ │ │ ├── Rule.php # User rule management
│ │ │ └── User.php # Member management
│ │ ├── lang/zh-cn/ # Admin language (17 files)
│ │ │ ├── addon.php # Addon language
│ │ │ ├── ajax.php # Ajax language
│ │ │ ├── category.php # Category language
│ │ │ ├── command.php # Command language
│ │ │ ├── config.php # Config language
│ │ │ ├── dashboard.php # Dashboard language
│ │ │ ├── history.php # History language
│ │ │ ├── index.php # Login/home language
│ │ │ ├── auth/
│ │ │ │ ├── admin.php # Admin management language
│ │ │ │ ├── group.php # Group management language
│ │ │ │ └── rule.php # Rule management language
│ │ │ ├── general/
│ │ │ │ ├── attachment.php # Attachment language
│ │ │ │ ├── config.php # Config language
│ │ │ │ └── profile.php # Profile language
│ │ │ └── user/
│ │ │ ├── group.php # User group language
│ │ │ ├── rule.php # User rule language
│ │ │ └── user.php # User management language
│ │ ├── library/ # Admin-specific libraries
│ │ │ └── traits/
│ │ │ └── Backend.php # CRUD trait (index/add/edit/del/etc.)
│ │ │ └── Auth.php # Admin auth (extends fast\Auth)
│ │ ├── model/ # Admin models (11 files)
│ │ │ ├── Admin.php # Admin user model
│ │ │ ├── AdminLog.php # Admin log model
│ │ │ ├── AuthGroup.php # Auth group model
│ │ │ ├── AuthGroupAccess.php # Admin-group pivot model
│ │ │ ├── AuthRule.php # Auth rule model
│ │ │ ├── Command.php # Command execution log model
│ │ │ ├── History.php # Lottery history model
│ │ │ ├── Num.php # Lottery number model
│ │ │ ├── User.php # Admin-side user model (with hooks)
│ │ │ ├── UserGroup.php # User group model
│ │ │ └── UserRule.php # User rule model
│ │ ├── validate/ # Admin validators (8 files)
│ │ │ ├── Admin.php # Admin user validation
│ │ │ ├── AuthRule.php # Auth rule validation
│ │ │ ├── Category.php # Category validation
│ │ │ ├── Command.php # Command validation
│ │ │ ├── History.php # History validation (empty rules)
│ │ │ ├── User.php # User validation
│ │ │ ├── UserGroup.php # User group validation
│ │ │ └── UserRule.php # User rule validation
│ │ └── view/ # Admin view templates (48 HTML files)
│ │ ├── addon/ # Addon views
│ │ │ ├── add.html
│ │ │ ├── config.html
│ │ │ └── index.html
│ │ ├── auth/
│ │ │ ├── admin/
│ │ │ │ ├── add.html
│ │ │ │ ├── edit.html
│ │ │ │ └── index.html
│ │ │ ├── adminlog/
│ │ │ │ ├── detail.html
│ │ │ │ └── index.html
│ │ │ ├── group/
│ │ │ │ ├── add.html
│ │ │ │ ├── edit.html
│ │ │ │ └── index.html
│ │ │ └── rule/
│ │ │ ├── add.html
│ │ │ ├── edit.html
│ │ │ ├── index.html
│ │ │ └── tpl.html
│ │ ├── category/
│ │ │ ├── add.html
│ │ │ ├── edit.html
│ │ │ └── index.html
│ │ ├── command/
│ │ │ ├── add.html
│ │ │ ├── detail.html
│ │ │ └── index.html
│ │ ├── common/ # Shared admin partials
│ │ │ ├── control.html
│ │ │ ├── header.html
│ │ │ ├── menu.html
│ │ │ ├── meta.html
│ │ │ └── script.html
│ │ ├── dashboard/
│ │ │ └── index.html
│ │ ├── general/
│ │ │ ├── attachment/
│ │ │ │ ├── add.html
│ │ │ │ ├── edit.html
│ │ │ │ ├── index.html
│ │ │ │ └── select.html
│ │ │ └── config/
│ │ │ └── index.html
│ │ ├── history/
│ │ │ ├── add.html
│ │ │ ├── edit.html
│ │ │ └── index.html
│ │ ├── index/
│ │ │ ├── index.html # Admin home page
│ │ │ └── login.html # Admin login page
│ │ ├── layout/
│ │ │ └── default.html # Main admin layout
│ │ └── user/
│ │ ├── group/
│ │ │ ├── add.html
│ │ │ ├── edit.html
│ │ │ └── index.html
│ │ ├── rule/
│ │ │ ├── add.html
│ │ │ ├── edit.html
│ │ │ └── index.html
│ │ └── user/
│ │ ├── edit.html
│ │ └── index.html
│ ├── api/ # REST API module
│ │ ├── controller/ # API controllers (8 files)
│ │ │ ├── Common.php # Common API (upload endpoint)
│ │ │ ├── Demo.php # Demo API endpoints
│ │ │ ├── Ems.php # Email verification API
│ │ │ ├── Index.php # API homepage
│ │ │ ├── Sms.php # SMS verification API
│ │ │ ├── Token.php # Token management API
│ │ │ ├── User.php # User auth/profile API
│ │ │ └── Validate.php # Validation testing API
│ │ ├── lang/zh-cn/ # API language packs
│ │ └── library/ # API-specific libraries
│ ├── common/ # Shared code across modules
│ │ ├── behavior/ # Shared behavior hooks
│ │ │ └── Common.php
│ │ ├── controller/ # Base controllers (3 files)
│ │ │ ├── Backend.php # Admin base controller
│ │ │ ├── Frontend.php # Frontend base controller
│ │ │ └── Api.php # API base controller
│ │ ├── exception/ # Custom exceptions
│ │ │ └── UploadException.php # Upload failure exception
│ │ ├── lang/zh-cn/ # Shared language packs
│ │ ├── library/ # Shared libraries (10 files)
│ │ │ ├── Auth.php # User authentication (token-based)
│ │ │ ├── Email.php # Email sending (PHPMailer)
│ │ │ ├── Ems.php # Email verification code
│ │ │ ├── Log.php # Logging utility
│ │ │ ├── Menu.php # Menu generation
│ │ │ ├── Security.php # Security utilities
│ │ │ ├── Sms.php # SMS verification code
│ │ │ ├── Token.php # Token storage manager
│ │ │ ├── Upload.php # File upload handler
│ │ │ └── token/
│ │ │ ├── Driver.php # Token driver interface
│ │ │ └── driver/
│ │ │ ├── Mysql.php # MySQL token driver
│ │ │ └── Redis.php # Redis token driver
│ │ ├── model/ # Shared models (12 files)
│ │ │ ├── Area.php # Province/city/area data
│ │ │ ├── Attachment.php # File attachment model
│ │ │ ├── Category.php # Category model
│ │ │ ├── Config.php # System config model
│ │ │ ├── Em s.php # Email verification log
│ │ │ ├── MoneyLog.php # User money change log
│ │ │ ├── ScoreLog.php # User score change log
│ │ │ ├── Sms.php # SMS verification log
│ │ │ ├── User.php # User model
│ │ │ ├── UserGroup.php # User group model
│ │ │ ├── UserRule.php # User rule model
│ │ │ └── Version.php # Version info model
│ │ └── view/tpl/ # Shared templates
│ │ ├── dispatch_jump.tpl # Redirect template
│ │ └── think_exception.tpl # Exception page template
│ ├── index/ # Frontend (user-facing) module
│ │ ├── controller/ # Frontend controllers (3 files)
│ │ │ ├── Ajax.php # Frontend AJAX (lang, icon, upload)
│ │ │ ├── Index.php # Homepage + lottery scraping
│ │ │ └── User.php # Member center (login/register/profile)
│ │ ├── lang/ # Frontend language packs
│ │ │ ├── en/
│ │ │ └── zh-cn/
│ │ └── view/
│ │ ├── common/
│ │ ├── index/
│ │ │ └── index.html # Homepage
│ │ ├── layout/
│ │ │ └── default.html # Frontend layout
│ │ └── user/
│ ├── extra/ # Extra config files
│ │ ├── addons.php # Addon hooks and routes
│ │ ├── queue.php # Queue configuration
│ │ ├── site.php # Site configuration
│ │ └── upload.php # Upload configuration
│ ├── common.php # Global helper functions
│ ├── config.php # Main ThinkPHP config
│ ├── database.php # Database connection config
│ ├── command.php # CLI command registry
│ └── route.php # Route definitions
├── extend/ # Custom extension classes
│ └── fast/ # FastAdmin helper classes
│ ├── Auth.php # RBAC permission checker
│ ├── Date.php # Date formatting utilities
│ ├── Form.php # Form builder
│ ├── Http.php # HTTP client utility
│ ├── Pinyin.php # Chinese pinyin conversion
│ ├── Random.php # Random string generation
│ ├── Rsa.php # RSA encryption
│ ├── Tree.php # Tree data structure
│ └── Version.php # Version comparison
├── addons/ # Plugin/addon directory
├── public/ # Web root
│ ├── index.php # Web entry point
│ └── assets/ # Static assets (JS, CSS, images)
├── runtime/ # Runtime files (cache, logs, temp)
├── think # CLI entry point
├── thinkphp/ # ThinkPHP framework core
├── vendor/ # Composer dependencies
└── sql/ # SQL migration files
```
## Directory Purposes
### `application/admin/` — Backend Administration
- Purpose: Complete backend management system with RBAC
- Contains: 18 controllers, 11 models, 8 validators, 48 view templates, 17 language files
- Subdirectories:
- `controller/auth/` — Admin RBAC (admin users, groups, rules, logs)
- `controller/general/` — System utilities (attachments, config, profile)
- `controller/user/` — Frontend user management from admin panel
- `command/` — CLI code generators (CRUD, menu, min, api, addon)
- `library/` — Auth class + Backend CRUD trait
- `view/layout/default.html` — Main admin layout template
### `application/index/` — Frontend User Portal
- Purpose: Public-facing website and member center
- Contains: 3 controllers (Index, User, Ajax)
- Custom domain files:
- `controller/Index.php::get_history()` — Lottery data scraping from macaumarksix.com
- Uses `\GuzzleHttp\Client` to fetch lottery results
- Writes to `fa_history` table via raw `Db` queries (not ORM)
### `application/api/` — REST API
- Purpose: API for mobile/third-party clients
- Contains: 8 controllers, all extend `app\common\controller\Api`
- Standard response format: `{code, msg, time, data}`
### `application/common/` — Shared Code
- Purpose: Base controllers, shared models, utility libraries
- Contains: 3 base controllers, 12 shared models, 10 libraries, 1 exception
- Token driver pattern: pluggable MySQL or Redis storage
### `extend/fast/` — Framework Utilities
- Purpose: FastAdmin core utility classes (not in composer autoload)
- Contains: Tree (hierarchical data), Auth (RBAC base), Date, Random, Http, Form, Rsa, Pinyin, Version
## Admin Controller → Model → View → Validate → Lang Mapping
### auth/admin — 管理员管理
| Layer | File |
|-------|------|
| Controller | `application/admin/controller/auth/Admin.php` |
| Model | `application/admin/model/Admin.php` |
| Model (pivot) | `application/admin/model/AuthGroupAccess.php` |
| Model (group) | `application/admin/model/AuthGroup.php` |
| View (list) | `application/admin/view/auth/admin/index.html` |
| View (add) | `application/admin/view/auth/admin/add.html` |
| View (edit) | `application/admin/view/auth/admin/edit.html` |
| Validate | `application/admin/validate/Admin.php` |
| Lang | `application/admin/lang/zh-cn/auth/admin.php` |
### auth/adminlog — 管理员日志
| Layer | File |
|-------|------|
| Controller | `application/admin/controller/auth/Adminlog.php` |
| Model | `application/admin/model/AdminLog.php` |
| View (list) | `application/admin/view/auth/adminlog/index.html` |
| View (detail) | `application/admin/view/auth/adminlog/detail.html` |
| Validate | *(none — read-only)* |
| Lang | *(uses auth/admin.php)* |
### auth/group — 角色组管理
| Layer | File |
|-------|------|
| Controller | `application/admin/controller/auth/Group.php` |
| Model | `application/admin/model/AuthGroup.php` |
| View (list) | `application/admin/view/auth/group/index.html` |
| View (add) | `application/admin/view/auth/group/add.html` |
| View (edit) | `application/admin/view/auth/group/edit.html` |
| Validate | *(none — uses inline validation)* |
| Lang | `application/admin/lang/zh-cn/auth/group.php` |
### auth/rule — 权限规则管理
| Layer | File |
|-------|------|
| Controller | `application/admin/controller/auth/Rule.php` |
| Model | `application/admin/model/AuthRule.php` |
| View (list) | `application/admin/view/auth/rule/index.html` |
| View (add) | `application/admin/view/auth/rule/add.html` |
| View (edit) | `application/admin/view/auth/rule/edit.html` |
| View (template) | `application/admin/view/auth/rule/tpl.html` |
| Validate | `application/admin/validate/AuthRule.php` |
| Lang | `application/admin/lang/zh-cn/auth/rule.php` |
### general/attachment — 附件管理
| Layer | File |
|-------|------|
| Controller | `application/admin/controller/general/Attachment.php` |
| Model | `application/common/model/Attachment.php` |
| View (list) | `application/admin/view/general/attachment/index.html` |
| View (add) | `application/admin/view/general/attachment/add.html` |
| View (edit) | `application/admin/view/general/attachment/edit.html` |
| View (select) | `application/admin/view/general/attachment/select.html` |
| Validate | *(none — uses model validation)* |
| Lang | `application/admin/lang/zh-cn/general/attachment.php` |
### general/config — 系统配置
| Layer | File |
|-------|------|
| Controller | `application/admin/controller/general/Config.php` |
| Model | `application/common/model/Config.php` |
| View (list) | `application/admin/view/general/config/index.html` |
| Validate | *(none — inline validation)* |
| Lang | `application/admin/lang/zh-cn/general/config.php` |
### general/profile — 个人资料
| Layer | File |
|-------|------|
| Controller | `application/admin/controller/general/Profile.php` |
| Model | `application/admin/model/Admin.php` + `application/admin/model/AdminLog.php` |
| View (list) | `application/admin/view/general/profile/index.html` |
| Validate | *(none)* |
| Lang | `application/admin/lang/zh-cn/general/profile.php` |
### user/user — 会员管理
| Layer | File |
|-------|------|
| Controller | `application/admin/controller/user/User.php` |
| Model | `application/admin/model/User.php` |
| Model (group) | `application/admin/model/UserGroup.php` |
| View (list) | `application/admin/view/user/user/index.html` |
| View (edit) | `application/admin/view/user/user/edit.html` |
| Validate | `application/admin/validate/User.php` |
| Lang | `application/admin/lang/zh-cn/user/user.php` |
### user/group — 会员组管理
| Layer | File |
|-------|------|
| Controller | `application/admin/controller/user/Group.php` |
| Model | `application/admin/model/UserGroup.php` |
| View (list) | `application/admin/view/user/group/index.html` |
| View (add) | `application/admin/view/user/group/add.html` |
| View (edit) | `application/admin/view/user/group/edit.html` |
| Validate | `application/admin/validate/UserGroup.php` |
| Lang | `application/admin/lang/zh-cn/user/group.php` |
### user/rule — 会员规则管理
| Layer | File |
|-------|------|
| Controller | `application/admin/controller/user/Rule.php` |
| Model | `application/admin/model/UserRule.php` |
| View (list) | `application/admin/view/user/rule/index.html` |
| View (add) | `application/admin/view/user/rule/add.html` |
| View (edit) | `application/admin/view/user/rule/edit.html` |
| Validate | `application/admin/validate/UserRule.php` |
| Lang | `application/admin/lang/zh-cn/user/rule.php` |
### history — 彩票历史记录
| Layer | File |
|-------|------|
| Controller | `application/admin/controller/History.php` |
| Model | `application/admin/model/History.php` |
| View (list) | `application/admin/view/history/index.html` |
| View (add) | `application/admin/view/history/add.html` |
| View (edit) | `application/admin/view/history/edit.html` |
| Validate | `application/admin/validate/History.php` (empty rules) |
| Lang | `application/admin/lang/zh-cn/history.php` |
### num — 数字波色
| Layer | File |
|-------|------|
| Controller | `application/admin/controller/Num.php` |
| Model | `application/admin/model/Num.php` |
| View | *(no dedicated views — uses trait defaults)* |
| Validate | *(none)* |
| Lang | *(none — uses generic)* |
### category — 分类管理
| Layer | File |
|-------|------|
| Controller | `application/admin/controller/Category.php` |
| Model | `application/common/model/Category.php` |
| View (list) | `application/admin/view/category/index.html` |
| View (add) | `application/admin/view/category/add.html` |
| View (edit) | `application/admin/view/category/edit.html` |
| Validate | `application/admin/validate/Category.php` (empty rules) |
| Lang | `application/admin/lang/zh-cn/category.php` |
### command — 在线命令
| Layer | File |
|-------|------|
| Controller | `application/admin/controller/Command.php` |
| Model | `application/admin/model/Command.php` |
| View (list) | `application/admin/view/command/index.html` |
| View (add) | `application/admin/view/command/add.html` |
| View (detail) | `application/admin/view/command/detail.html` |
| Validate | `application/admin/validate/Command.php` (empty rules) |
| Lang | `application/admin/lang/zh-cn/command.php` |
### dashboard — 控制台
| Layer | File |
|-------|------|
| Controller | `application/admin/controller/Dashboard.php` |
| Model | *(multiple — Admin, User, Attachment, Category via direct queries)* |
| View (list) | `application/admin/view/dashboard/index.html` |
| Validate | *(none)* |
| Lang | `application/admin/lang/zh-cn/dashboard.php` |
### addon — 插件管理
| Layer | File |
|-------|------|
| Controller | `application/admin/controller/Addon.php` |
| Model | *(none — uses Service class directly)* |
| View (list) | `application/admin/view/addon/index.html` |
| View (add) | `application/admin/view/addon/add.html` |
| View (config) | `application/admin/view/addon/config.html` |
| Validate | *(none)* |
| Lang | `application/admin/lang/zh-cn/addon.php` |
### index (admin) — 后台首页/登录
| Layer | File |
|-------|------|
| Controller | `application/admin/controller/Index.php` |
| Model | `application/admin/model/Admin.php` |
| View (home) | `application/admin/view/index/index.html` |
| View (login) | `application/admin/view/index/login.html` |
| Validate | *(inline in login method)* |
| Lang | `application/admin/lang/zh-cn/index.php` |
### ajax (admin) — 通用异步接口
| Layer | File |
|-------|------|
| Controller | `application/admin/controller/Ajax.php` |
| Model | *(various — Attachment, Category, Area via Db queries)* |
| View | *(none — JSON responses only)* |
| Validate | *(none)* |
| Lang | `application/admin/lang/zh-cn/ajax.php` |
## Custom Domain Files (Lottery Feature)
| File | Purpose |
|------|---------|
| `application/index/controller/Index.php` | Homepage + `get_history()` scrapes Macau lottery data from macaumarksix.com |
| `application/admin/controller/History.php` | Admin CRUD for lottery history (read-only display) |
| `application/admin/controller/Num.php` | Num→color mapping API via `getColorMap()` |
| `application/admin/model/History.php` | History model: table `fa_history`, fields `expect`, `openTime`, `num1`~`num7` |
| `application/admin/model/Num.php` | Num model: table `fa_num`, fields `num`, `color` |
| `application/admin/validate/History.php` | Empty validator (no validation rules defined) |
| `application/admin/view/history/index.html` | List view with add/edit buttons hidden |
| `application/admin/view/history/add.html` | Add form template (unused) |
| `application/admin/view/history/edit.html` | Edit form template (unused) |
| `application/admin/lang/zh-cn/history.php` | Chinese language strings for history module |
## Common Models (Shared Across Modules)
| File | Table | Purpose |
|------|-------|---------|
| `application/common/model/User.php` | `fa_user` | Frontend user with money/score log hooks |
| `application/common/model/UserGroup.php` | `fa_user_group` | User group |
| `application/common/model/UserRule.php` | `fa_user_rule` | User permission rule |
| `application/common/model/Category.php` | `fa_category` | Hierarchical category system |
| `application/common/model/Config.php` | `fa_config` | System configuration |
| `application/common/model/Attachment.php` | `fa_attachment` | File upload metadata |
| `application/common/model/Attachment.php` | `fa_area` | Province/city/area data |
| `application/common/model/MoneyLog.php` | `fa_money_log` | User balance change log |
| `application/common/model/ScoreLog.php` | `fa_score_log` | User score change log |
| `application/common/model/Ems.php` | `fa_ems` | Email verification log |
| `application/common/model/Sms.php` | `fa_sms` | SMS verification log |
| `application/common/model/Version.php` | `fa_version` | Version info |
## API Controllers (Complete List)
| Controller | File | Key Methods |
|------------|------|-------------|
| Index | `application/api/controller/Index.php` | `index()` |
| User | `application/api/controller/User.php` | `login()`, `mobilelogin()`, `register()`, `logout()`, `profile()`, `changeemail()`, `changemobile()`, `third()`, `resetpwd()` |
| Token | `application/api/controller/Token.php` | Token management endpoints |
| Ems | `application/api/controller/Ems.php` | Email verification |
| Sms | `application/api/controller/Sms.php` | SMS verification |
| Common | `application/api/controller/Common.php` | Shared upload endpoint |
| Demo | `application/api/controller/Demo.php` | Demo/test endpoints |
| Validate | `application/api/controller/Validate.php` | Validation testing |
## Naming Conventions
**Files:**
- Controllers: PascalCase (`AuthRule.php`, `Dashboard.php`, `History.php`)
- Models: PascalCase (`AdminLog.php`, `UserGroup.php`, `AuthGroupAccess.php`)
- Validators: PascalCase matching model name (`Admin.php`, `User.php`, `History.php`)
- Libraries: PascalCase (`Upload.php`, `Auth.php`, `Email.php`)
- Views: lowercase with `.html` (`index.html`, `add.html`, `edit.html`)
- Language files: lowercase PHP (`user.php`, `category.php`)
- Commands: PascalCase (`Crud.php`, `Menu.php`, `Min.php`)
**Directories:**
- Controller subdirectories: lowercase (`auth/`, `general/`, `user/`)
- View subdirectories: mirror controller structure (`view/auth/admin/`, `view/general/config/`)
**Namespaces:**
- Controllers: `app\{module}\controller\{sub?}\{Name}`
- Models: `app\admin\model\{Name}` or `app\common\model\{Name}`
- Libraries: `app\admin\library\{Name}` or `app\common\library\{Name}`
- Validators: `app\admin\validate\{Name}`
- Commands: `app\admin\command\{Name}`
- Extensions: `fast\{Name}` (PSR-4 from `extend/fast/`)
## Where to Add New Code
**New Admin Module (CRUD):**
1. Controller: `application/admin/controller/{SubDir}/{Name}.php` — extend `app\common\controller\Backend`
2. Model: `application/admin/model/{Name}.php` — extend `think\Model`, set `$name` to table name
3. View: `application/admin/view/{subdir}/{name}/index.html` (list), `add.html`, `edit.html`
4. Validate: `application/admin/validate/{Name}.php` — extend `think\Validate`
5. Lang: `application/admin/lang/zh-cn/{subdir}/{name}.php`
6. Generate menu: run `php think menu --controller={subdir/name}`
**New Admin Module (Custom Logic):**
- Same as CRUD but override trait methods in controller as needed
- Set `$model` property in `_initialize()` to link controller to model
**New API Endpoint:**
- Controller: `application/api/controller/{Name}.php` — extend `app\common\controller\Api`
- Use `$noNeedLogin = ['*']` for public endpoints
- Return via `$this->success($data)` or `$this->error($msg)`
**New Frontend Page:**
- Controller: `application/index/controller/{Name}.php` — extend `app\common\controller\Frontend`
- View: `application/index/view/{name}/{action}.html`
**New Shared Model:**
- `application/common/model/{Name}.php` — extend `think\Model`
- Used by multiple modules (admin + index + api)
**New Library:**
- `application/common/library/{Name}.php` for cross-module utilities
- `application/admin/library/{Name}.php` for admin-only utilities
- `extend/fast/{Name}.php` for framework-level utilities
**New CLI Command:**
- `application/admin/command/{Name}.php` — extend `\think\console\Command`
- Register in `application/command.php`
## Special Directories
**`runtime/`:**
- Purpose: ThinkPHP runtime cache, logs, compiled templates
- Generated: Yes (by ThinkPHP)
- Committed: No
**`addons/`:**
- Purpose: Plugin directory for FastAdmin addon system
- Generated: Yes (when installing addons via admin panel)
- Committed: No
**`vendor/`:**
- Purpose: Composer dependencies
- Generated: Yes (`composer install`)
- Committed: No
**`extend/fast/`:**
- Purpose: FastAdmin core utilities not distributed via composer
- Committed: Yes (part of application)
- Key classes: `Tree`, `Auth` (RBAC base), `Date`, `Random`, `Http`
**`public/assets/`:**
- Purpose: Static frontend/backend assets (JS, CSS, images)
- `public/assets/js/backend/` — Admin panel JavaScript (matches controller names)
- `public/assets/js/frontend/` — Frontend JavaScript
- `public/assets/js/backend/command.js` — Command module JS
- `public/assets/js/backend/history.js` — History module JS
**`sql/`:**
- Purpose: SQL migration/schema files
- Contains database dumps and migration scripts
---
*Structure analysis: 2026-04-21*
+284
View File
@@ -0,0 +1,284 @@
# Testing Patterns
**Analysis Date:** 2026-04-21
## Test Framework
**Runner:**
- PHPUnit exists in the project only as part of the ThinkPHP framework (`thinkphp/phpunit.xml`)
- No project-level test runner configured
**Assertion Library:**
- PHPUnit's built-in assertions (only in framework's own tests)
**No Project Tests Exist.** After thorough exploration of the entire codebase:
- `application/**/*.test.php` -- None found
- `application/**/*.spec.php` -- None found
- `tests/` directory -- Does not exist at project root
- `phpunit.xml` -- Only exists at `thinkphp/phpunit.xml` (framework's own test suite)
- `.idea/phpunit.xml` -- IDE config pointing to `thinkphp/phpunit.xml` (for framework testing only)
**Framework PHPUnit Config (`thinkphp/phpunit.xml`):**
```xml
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="tests/mock.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false">
<testsuites>
<testsuite name="ThinkPHP Test Suite">
<directory>./tests/thinkphp/</directory>
</testsuite>
</testsuites>
<listeners>
<listener class="JohnKary\PHPUnit\Listener\SpeedTrapListener" />
</listeners>
<filter>
<whitelist>
<directory suffix=".php">./</directory>
<exclude>
<directory suffix=".php">tests</directory>
<directory suffix=".php">vendor</directory>
</exclude>
</whitelist>
</filter>
</phpunit>
```
This config is for testing the ThinkPHP framework itself, NOT the application code in `application/`.
**ThinkPHP Framework Tests (not application code):**
- `thinkphp/tests/` contains ~50 test files testing framework internals
- Covers: Cache, Config, Controller, DB, Debug, Exception, Hook, Lang, Loader, Log, Model, Paginate, Request, Response, Route, Session, Template, URL, Validate, View
- Example: `thinkphp/tests/thinkphp/library/think/validateTest.php`
**Vendor Tests (not application code):**
- `vendor/overtrue/socialite/tests/` -- OAuth provider tests
- `vendor/easywechat-composer/easywechat-composer/tests/` -- Composer plugin tests
- `vendor/pimple/pimple/.github/workflows/tests.yml` -- CI config
- `vendor/phpoffice/phpspreadsheet/` -- No test files included in dist
## Test File Organization
**Current State:** No test files exist for application code.
**Recommended Structure (if tests were to be added):**
```
tests/
├── bootstrap.php
├── admin/
│ ├── controller/
│ │ └── UserTest.php
│ ├── library/
│ │ └── AuthTest.php
│ └── validate/
│ └── UserTest.php
├── common/
│ ├── controller/
│ ├── library/
│ │ ├── AuthTest.php
│ │ └── UploadTest.php
│ └── model/
│ └── UserTest.php
├── api/
│ └── controller/
│ └── UserTest.php
├── extend/
│ └── fast/
│ └── RandomTest.php
├── fixtures/
│ └── database/
└── TestCase.php
```
## Mocking
**No mocking framework in use.**
**Dependencies that would need mocking for testing:**
- `think\Db` -- Database operations (query, startTrans, commit, rollback, name, table)
- `think\Config` -- Configuration access (`Config::get()`, `Config::set()`)
- `think\Request` -- HTTP request (`$this->request->post()`, `$this->request->isAjax()`)
- `think\Session` -- Session management
- `think\Cookie` -- Cookie operations
- `think\Hook` -- Event/hook system
- `think\Lang` -- Language/translation
- `think\Loader` -- Class autoloading
- `think\View` -- Template rendering
- `fast\Tree` -- Tree data structure
- `\app\common\library\Auth` -- Authentication singleton
- `\app\admin\library\Auth` -- Admin authentication singleton
- `GuzzleHttp\Client` -- HTTP client (used in `application/index/controller/Index.php`)
**Mocking Challenge:** The codebase relies heavily on ThinkPHP's singleton pattern (`Auth::instance()`) and static methods (`Db::name()`, `Config::get()`), making unit testing difficult without significant refactoring or a dedicated mocking framework like Mockery.
## Fixtures and Factories
**Current State:** No test fixtures or factories exist.
**Database fixtures would be needed for:**
- User records (admin users, regular users)
- Auth groups and rules
- Categories and attachments
- Config entries
## Coverage
**Current Coverage: 0%** -- No test coverage for any application code.
**Untested Modules (by priority):**
**High Priority (core business logic):**
| Module | File | Functionality |
|--------|------|---------------|
| Common Auth | `application/common/library/Auth.php` | User registration, login, token management, password encryption, email/mobile verification |
| Admin Auth | `application/admin/library/Auth.php` | Admin authentication, permission checking, breadcrumb generation |
| Backend Trait | `application/admin/library/traits/Backend.php` | CRUD operations: index, add, edit, del, multi, import, recyclebin, destroy, restore |
| Backend Controller | `application/common/controller/Backend.php` | `buildparams()` query building, `selectpage()` dropdown, data limit enforcement |
| Upload | `application/common/library/Upload.php` | File upload, validation, chunked upload, image processing |
| Token | `application/common/library/Token.php` | Token CRUD (MySQL/Redis drivers) |
| Security | `application/common/library/Security.php` | XSS cleaning, input sanitization |
**Medium Priority (data operations):**
| Module | File | Functionality |
|--------|------|---------------|
| User Model (common) | `application/common/model/User.php` | Money/score change logging, level calculation, avatar generation |
| User Model (admin) | `application/admin/model/User.php` | Password hashing on change, money/score audit logging |
| MoneyLog | `application/common/model/MoneyLog.php` | Financial transaction logging |
| ScoreLog | `application/common/model/ScoreLog.php` | Score transaction logging |
| User Controller | `application/admin/controller/user/User.php` | User management with avatar processing |
| Command Controller | `application/admin/controller/Command.php` | Online command generation and execution |
| Validators | `application/admin/validate/*.php` | Data validation rules for all entities |
**Low Priority (utilities):**
| Module | File |
|--------|------|
| Global Helpers | `application/common.php` (20+ functions) |
| Admin Helpers | `application/admin/common.php` |
| Random | `extend/fast/Random.php` |
| Date | `extend/fast/Date.php` |
| Tree | `extend/fast/Tree.php` |
| Rsa | `extend/fast/Rsa.php` |
| Form | `extend/fast/Form.php` |
| Http | `extend/fast/Http.php` |
## Code Quality Tools
**Static Analysis:**
- No PHPStan, Psalm, or PHPMD configured at project level
- PhpStorm `.idea/` directory contains transferred (inactive) configurations for PHPCS, PHPStan, and MessDetector
- No `.php-cs-fixer.php`, `phpcs.xml`, `phpmd.xml`, or `phpstan.neon` files
**Linting:**
- No ESLint, Prettier, or stylelint for frontend code
- `package.json` exists with Grunt build tasks only (minification, no linting)
**CI/CD:**
- No `.github/` directory (no GitHub Actions)
- No `.gitlab-ci.yml`
- No `Jenkinsfile`
- No `.travis.yml`
- No CI pipeline of any kind
**Build Tools:**
- Grunt for CSS/JS minification (`public/assets/js/*.min.js`, `public/assets/css/*.min.css`)
- `application/admin/command/Min.php` for asset minification
- `application/admin/command/Crud.php` for code generation
- `application/admin/command/Api.php` for API documentation generation
- `application/admin/command/Menu.php` for menu generation
- `application/admin/command/Install.php` for installation
**Composer Scripts:** None defined in `composer.json`
## Test Types
**Unit Tests:** Not used. No unit test files exist for any application code.
**Integration Tests:** Not used. No integration tests exist.
**E2E Tests:** Not used. No end-to-end testing framework (Selenium, Cypress, etc.) configured.
**Feature Tests:** Not used.
## Why No Tests
1. **FastAdmin Nature** -- This is a rapid development admin scaffold. FastAdmin projects prioritize speed over test coverage by design.
2. **No `require-dev`** -- `composer.json` has no testing-related dev dependencies (no PHPUnit, Mockery, etc.)
3. **No `scripts.test`** -- `composer.json` defines no test scripts
4. **No CI/CD** -- No continuous integration pipeline to enforce test execution
5. **Tight Coupling** -- Heavy reliance on ThinkPHP singletons and static methods makes unit testing difficult without significant refactoring
6. **Framework Philosophy** -- ThinkPHP 5.x ecosystem does not emphasize testing as a first-class concern
## Adding Tests: Recommended Setup
**Step 1: Add dev dependencies to `composer.json`:**
```json
"require-dev": {
"phpunit/phpunit": "^9.6",
"mockery/mockery": "^1.6"
}
```
**Step 2: Create `phpunit.xml` at project root:**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap.php"
colors="true"
stopOnFailure="false">
<testsuites>
<testsuite name="Application Test Suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<php>
<env name="APP_ENV" value="testing"/>
</php>
</phpunit>
```
**Step 3: Create `tests/bootstrap.php`:**
```php
<?php
// Load ThinkPHP bootstrap
define('APP_PATH', __DIR__ . '/../application/');
define('ROOT_PATH', __DIR__ . '/../');
define('RUNTIME_PATH', ROOT_PATH . 'runtime/');
require __DIR__ . '/../thinkphp/base.php';
// Set testing config
\think\Config::set('app_debug', false);
```
**Step 4: Add composer script:**
```json
"scripts": {
"test": "phpunit"
}
```
**Run Commands (after setup):**
```bash
composer install --dev # Install dev dependencies
vendor/bin/phpunit # Run all tests
vendor/bin/phpunit --coverage-html coverage/ # Generate coverage report
composer test # Run via composer script
```
## Testing Challenges Specific to This Codebase
1. **Singleton Auth** -- `Auth::instance()` is called directly throughout, requiring careful mock setup or refactoring to dependency injection
2. **Static Db Calls** -- `Db::name()`, `Db::query()`, `Db::startTrans()` used everywhere instead of injected connections
3. **Global Functions** -- `__()`, `cdnurl()`, etc. depend on ThinkPHP runtime being bootstrapped
4. **Request Context** -- Controllers depend on `$this->request` populated by framework routing
5. **Session/Cookie** -- Many operations depend on session/cookie state
6. **Config Dependency** -- Heavy use of `Config::get()` makes isolation testing difficult
7. **View Rendering** -- Some tests would need to verify HTML output from templates
---
*Testing analysis: 2026-04-21*
+15
View File
@@ -0,0 +1,15 @@
{
"mode": "yolo",
"granularity": "standard",
"parallelization": true,
"commit_docs": true,
"model_profile": "quality",
"workflow": {
"research": true,
"plan_check": true,
"verifier": true,
"nyquist_validation": true,
"auto_advance": true,
"_auto_chain_active": false
}
}
@@ -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-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>
@@ -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",默认值 10min=1max=100,宽度 120px 内联显示
- 查询按钮在输入框右侧,margin-left:10px
- 结果区域 id="missing-result",初始为空
- Layer.open 使用 type:1page 类型),area: ['650px', '550px']shadeClose:true(点击遮罩关闭)
- success 回调中绑定查询按钮事件,使用 layero 作为上下文选择器根(`$('#btn-missing-query', layero)`
- queryMissingNum 先检查 colorMapLoaded,未加载则先调用 loadColorMap
- AJAX 使用标准 $.ajaxurl 为 '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.jsplan 01-02 修改后的版本)
@D:/code/php/amlhc/application/admin/controller/History.phpplan 01-01 修改后的版本)
@D:/code/php/amlhc/application/admin/model/History.phpplan 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.jsplan 01-02 修改后的完整文件)
- application/admin/controller/History.phpplan 01-01 修改后的控制器)
- application/admin/model/History.phpplan 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)