1
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"directory": "public/assets/libs",
|
||||||
|
"ignoredDependencies": [
|
||||||
|
"es6-promise",
|
||||||
|
"file-saver",
|
||||||
|
"html2canvas",
|
||||||
|
"jspdf",
|
||||||
|
"jspdf-autotable",
|
||||||
|
"pdfmake"
|
||||||
|
],
|
||||||
|
"scripts":{
|
||||||
|
"postinstall": "node bower-cleanup.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
[app]
|
||||||
|
debug = false
|
||||||
|
trace = false
|
||||||
|
|
||||||
|
[database]
|
||||||
|
hostname = 127.0.0.1
|
||||||
|
database = fastadmin
|
||||||
|
username = root
|
||||||
|
password = root
|
||||||
|
hostport = 3306
|
||||||
|
prefix = fa_
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
/nbproject/
|
||||||
|
/thinkphp/
|
||||||
|
/vendor/
|
||||||
|
/runtime/*
|
||||||
|
/addons/*
|
||||||
|
/public/assets/libs/
|
||||||
|
/public/assets/addons/*
|
||||||
|
/public/uploads/*
|
||||||
|
.DS_Store
|
||||||
|
.idea
|
||||||
|
composer.lock
|
||||||
|
*.log
|
||||||
|
*.css.map
|
||||||
|
!.gitkeep
|
||||||
|
.env
|
||||||
|
.svn
|
||||||
|
.vscode
|
||||||
|
node_modules
|
||||||
|
.user.ini
|
||||||
|
|
||||||
|
.claude/
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# 使用自定义镜像源
|
||||||
|
registry=http://mirrors.tencent.com/npm/
|
||||||
|
#关闭SSL验证
|
||||||
|
strict-ssl=false
|
||||||
@@ -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
|
||||||
@@ -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*
|
||||||
@@ -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 弹窗 UI(toolbar 按钮、期数输入框、查询按钮、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 | - |
|
||||||
@@ -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
|
||||||
@@ -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*
|
||||||
@@ -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*
|
||||||
@@ -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*
|
||||||
@@ -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*
|
||||||
@@ -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*
|
||||||
@@ -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*
|
||||||
@@ -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*
|
||||||
@@ -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-482(fa_history 表结构,确认字段名和类型)
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
在 application/admin/model/History.php 中添加两个方法:
|
||||||
|
|
||||||
|
1. `getMissingNumbers($periods = 10)` — 公共方法,计算遗漏号码:
|
||||||
|
- 使用 `Db::name('history')->field('expect,num1,num2,num3,num4,num5,num6,num7')->order('openTime', 'desc')->limit($periods)->select()` 获取最近 $periods 期数据
|
||||||
|
- 遍历结果,将出现的号码收集到 `$appeared` 数组(key 为号码 int,value 为 true)
|
||||||
|
- 遍历 1-49,找出未在 `$appeared` 中的号码,收集到 `$missing` 数组
|
||||||
|
- 查询更多历史数据用于计算遗漏期数:`Db::name('history')->field('num1,num2,num3,num4,num5,num6,num7')->order('openTime', 'desc')->limit(500)->select()`
|
||||||
|
- 查询波色映射:`Db::name('num')->column('color', 'num')` 获取 `$colorMap`
|
||||||
|
- 对每个遗漏号码调用 `calcOmitCount($num, $allHistory)` 计算遗漏期数
|
||||||
|
- 组装结果:`['num' => $num, 'omit' => $omitCount, 'color' => $colorMap[$num] ?? '—']`
|
||||||
|
- 使用 `usort($result, function($a, $b) { return $b['omit'] - $a['omit']; })` 按 omit 降序排序
|
||||||
|
- 返回 `$result` 数组
|
||||||
|
|
||||||
|
2. `calcOmitCount($num, $allHistory)` — 私有方法,计算某个号码的遗漏期数:
|
||||||
|
- 遍历 `$allHistory`(已按 openTime DESC 排序),对每行检查 num1~num7 是否等于 $num
|
||||||
|
- 一旦找到,返回当前索引 $idx(即该号码最后一次出现距今多少期)
|
||||||
|
- 如果遍历完都没找到,返回 `count($allHistory)`(表示 500 期内都未出现)
|
||||||
|
|
||||||
|
关键实现细节:
|
||||||
|
- 必须使用 `use think\facade\Db;` 或 `\think\Db::name()` 来执行数据库查询(检查当前文件命名空间,如果模型已继承 think\Model,可直接用 `self::field(...)->select()` 或 `Db::name()`)
|
||||||
|
- 号码比较必须用 `(int)` 转换,因为数据库返回的 num1~num7 是 int 类型(schema 显示为 int(11)),但要确保一致
|
||||||
|
- $periods 参数范围校验:1-100,但模型层不校验(由控制器层校验),模型只负责计算
|
||||||
|
- 波色缺失时返回 `'—'` 字符串作为兜底
|
||||||
|
|
||||||
|
添加 `use think\facade\Db;` 到文件顶部(如果尚未存在)。
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- application/admin/model/History.php 包含 `public function getMissingNumbers($periods = 10)`
|
||||||
|
- application/admin/model/History.php 包含 `private function calcOmitCount($num, $allHistory)`
|
||||||
|
- getMissingNumbers 方法体包含 `Db::name('history')` 或 `self::` 查询
|
||||||
|
- getMissingNumbers 方法体包含 `Db::name('num')` 查询波色映射
|
||||||
|
- getMissingNumbers 方法体包含 `usort` 按 omit 降序排序
|
||||||
|
- 方法返回数组结构为 `[['num' => int, 'omit' => int, 'color' => string], ...]`
|
||||||
|
- 文件顶部有 `use think\facade\Db;` 或使用 `\think\Db::` 完整命名空间
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>grep -c "getMissingNumbers\|calcOmitCount" application/admin/model/History.php | grep "2"</automated>
|
||||||
|
</verify>
|
||||||
|
<done>History 模型有 getMissingNumbers($periods) 和 calcOmitCount($num, $allHistory) 两个方法,能查询 fa_history 表计算遗漏号码并返回按遗漏期数降序的 [{num, omit, color}] 数组</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="false">
|
||||||
|
<name>Task 2: 在 History 控制器中添加 missingNum() AJAX 接口方法</name>
|
||||||
|
<files>application/admin/controller/History.php</files>
|
||||||
|
<read_first>
|
||||||
|
- application/admin/controller/History.php(当前控制器结构)
|
||||||
|
- application/admin/controller/Num.php(参考 getColorMap 方法的响应模式)
|
||||||
|
- application/common/controller/Backend.php(确认 $noNeedRight 用法)
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
在 application/admin/controller/History.php 中添加 missingNum() 方法:
|
||||||
|
|
||||||
|
1. 在类中添加权限控制属性(在 _initialize() 方法上方或下方):
|
||||||
|
```php
|
||||||
|
// 无需额外权限检查(但仍在 admin 模块内,需要 admin 登录)
|
||||||
|
protected $noNeedRight = ['missingNum'];
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 添加 missingNum() 方法:
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* 查询遗漏号码
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function missingNum()
|
||||||
|
{
|
||||||
|
if ($this->request->isAjax()) {
|
||||||
|
$periods = $this->request->get('periods', 10, 'intval');
|
||||||
|
if ($periods < 1 || $periods > 100) {
|
||||||
|
$this->error('期数范围必须在 1-100 之间');
|
||||||
|
}
|
||||||
|
$result = $this->model->getMissingNumbers($periods);
|
||||||
|
$this->success('查询成功', $result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
关键实现细节:
|
||||||
|
- 使用 `$this->request->isAjax()` 判断是否为 AJAX 请求
|
||||||
|
- 使用 `$this->request->get('periods', 10, 'intval')` 获取参数,默认值 10,强制转为 int
|
||||||
|
- 参数校验:$periods < 1 或 > 100 时调用 `$this->error()` 返回错误
|
||||||
|
- 调用 `$this->model->getMissingNumbers($periods)` 获取结果
|
||||||
|
- 使用 `$this->success('查询成功', $result)` 返回标准 FastAdmin 响应格式 `{code: 1, msg: '查询成功', data: [...]}`
|
||||||
|
- 方法无返回值(void),通过 $this->success/error 输出 JSON
|
||||||
|
|
||||||
|
不要使用 `protected $noNeedRight = ['*']`,只放开 missingNum 一个方法即可(最小权限原则)。
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- application/admin/controller/History.php 包含 `protected $noNeedRight = ['missingNum']`
|
||||||
|
- application/admin/controller/History.php 包含 `public function missingNum()`
|
||||||
|
- missingNum 方法体包含 `$this->request->isAjax()` 判断
|
||||||
|
- missingNum 方法体包含 `$this->request->get('periods', 10, 'intval')`
|
||||||
|
- missingNum 方法体包含 `$periods < 1 || $periods > 100` 范围校验
|
||||||
|
- missingNum 方法体包含 `$this->model->getMissingNumbers($periods)` 调用
|
||||||
|
- missingNum 方法体包含 `$this->success('查询成功', $result)` 响应
|
||||||
|
- missingNum 方法体包含 `$this->error(...)` 错误响应
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>grep -c "missingNum\|noNeedRight" application/admin/controller/History.php | awk '$1 >= 2'</automated>
|
||||||
|
</verify>
|
||||||
|
<done>History 控制器有 missingNum() 方法,接受 AJAX GET 请求,校验 periods 参数 1-100,调用模型 getMissingNumbers() 返回标准 JSON 响应</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="false">
|
||||||
|
<name>Task 3: 添加遗漏号码相关 i18n 文本到语言文件</name>
|
||||||
|
<files>application/admin/lang/zh-cn/history.php</files>
|
||||||
|
<read_first>
|
||||||
|
- application/admin/lang/zh-cn/history.php(当前语言文件)
|
||||||
|
- application/admin/lang/zh-cn/num.php(参考格式,如果存在)
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
修改 application/admin/lang/zh-cn/history.php,在现有返回值数组中添加遗漏号码相关的语言键:
|
||||||
|
|
||||||
|
当前内容:
|
||||||
|
```php
|
||||||
|
return [
|
||||||
|
'Expect' => '期号',
|
||||||
|
'OpenTime' => '时间',
|
||||||
|
'Num7' => '特码',
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
修改为:
|
||||||
|
```php
|
||||||
|
return [
|
||||||
|
'Expect' => '期号',
|
||||||
|
'OpenTime' => '时间',
|
||||||
|
'Num7' => '特码',
|
||||||
|
'Missing Number Analysis' => '遗漏号码分析',
|
||||||
|
'Query Periods' => '查询期数',
|
||||||
|
'Missing' => '遗漏',
|
||||||
|
'periods' => '期',
|
||||||
|
'No missing numbers found' => '最近所有号码均出现过',
|
||||||
|
'Query failed' => '查询失败',
|
||||||
|
'Loading' => '查询中...',
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
保持与现有格式一致:单引号包裹 key 和 value,逗号结尾,缩进 4 空格。
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- application/admin/lang/zh-cn/history.php 包含键 'Missing Number Analysis',值为 '遗漏号码分析'
|
||||||
|
- application/admin/lang/zh-cn/history.php 包含键 'Query Periods',值为 '查询期数'
|
||||||
|
- application/admin/lang/zh-cn/history.php 包含键 'Missing',值为 '遗漏'
|
||||||
|
- application/admin/lang/zh-cn/history.php 包含键 'periods',值为 '期'
|
||||||
|
- 文件格式保持 `return [ ... ];` 结构,使用单引号
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>grep -c "Missing Number Analysis\|Query Periods\|Missing" application/admin/lang/zh-cn/history.php | awk '$1 >= 3'</automated>
|
||||||
|
</verify>
|
||||||
|
<done>语言文件包含遗漏号码相关的所有中文翻译键值对</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| Browser → Controller (AJAX GET) | 用户输入 periods 参数,可能注入非数值 |
|
||||||
|
| Controller → Model | 内部调用,信任已建立 |
|
||||||
|
| Model → Database (fa_history, fa_num) | ORM 查询,无原始 SQL 拼接 |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-01-01 | Tampering | History::missingNum() - periods 参数 | mitigate | 使用 `$this->request->get('periods', 10, 'intval')` 强制类型转换 + 范围校验 `$periods < 1 || $periods > 100` |
|
||||||
|
| T-01-02 | Tampering | History::getMissingNumbers() - SQL 查询 | mitigate | 使用 ThinkPHP ORM `Db::name()` 参数化查询,无原始 SQL 字符串拼接 |
|
||||||
|
| T-01-03 | Elevation of Privilege | History::missingNum() 权限绕过 | mitigate | 使用 `$noNeedRight = ['missingNum']`(非 `$noNeedLogin`),admin 登录仍然必需,仅跳过权限规则检查 |
|
||||||
|
| T-01-04 | Denial of Service | 超大 periods 值导致查询慢 | mitigate | 参数上限 100,模型层 LIMIT 500 查询历史,确保 O(500*7) 时间复杂度可接受 |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- 通过浏览器访问 admin history 页面,在地址栏直接请求 `history/missingNum?periods=10`,返回 JSON 且 code=1,data 为非空数组
|
||||||
|
- 请求 `history/missingNum?periods=0` 返回错误(code!=1)
|
||||||
|
- 请求 `history/missingNum?periods=200` 返回错误(code!=1)
|
||||||
|
- 返回数据中每个元素包含 num(int 1-49)、omit(int >=0)、color(string)
|
||||||
|
- 返回数组按 omit 字段降序排列
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- [ ] History 模型有 getMissingNumbers() 和 calcOmitCount() 方法
|
||||||
|
- [ ] History 控制器有 missingNum() AJAX 端点,带 periods 参数校验
|
||||||
|
- [ ] 请求返回标准 FastAdmin 格式 {code: 1, msg: '查询成功', data: [{num, omit, color}, ...]}
|
||||||
|
- [ ] 结果按遗漏期数 omit 从大到小排序
|
||||||
|
- [ ] 波色映射从 fa_num 表获取,缺失时显示 '—'
|
||||||
|
- [ ] 语言文件包含遗漏号码相关 i18n 键值对
|
||||||
|
- [ ] missingNum 方法仅需 admin 登录即可访问($noNeedRight 非 $noNeedLogin)
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/01-omitted-number-analysis/01-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
phase: 01
|
||||||
|
plan: 01
|
||||||
|
subsystem: admin
|
||||||
|
tags: [lottery, missing-number, backend]
|
||||||
|
dependency_graph:
|
||||||
|
requires: []
|
||||||
|
provides: [missingNum endpoint, getMissingNumbers model method, i18n keys]
|
||||||
|
affects: [application/admin/model/History.php, application/admin/controller/History.php, application/admin/lang/zh-cn/history.php]
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [FastAdmin Backend controller, ThinkPHP 5.x Model, Db facade]
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- application/admin/model/History.php
|
||||||
|
- application/admin/controller/History.php
|
||||||
|
- application/admin/lang/zh-cn/history.php
|
||||||
|
modified: []
|
||||||
|
decisions:
|
||||||
|
- Used $noNeedRight = ['missingNum'] instead of ['*'] for minimal permission bypass
|
||||||
|
- Model queries up to 500 historical records for true omission count calculation
|
||||||
|
- Color fallback uses '—' string when fa_num table has no mapping for a number
|
||||||
|
metrics:
|
||||||
|
duration: ~5min
|
||||||
|
completed: "2026-04-21"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 01 Plan 01: Backend Missing Number Logic Summary
|
||||||
|
|
||||||
|
Backend missing number calculation logic and AJAX endpoint — History model with `getMissingNumbers()` + `calcOmitCount()` methods, History controller with `missingNum()` endpoint, and i18n language keys for the missing number feature.
|
||||||
|
|
||||||
|
## Tasks Completed
|
||||||
|
|
||||||
|
| # | Task | Commit | Files |
|
||||||
|
|---|------|--------|-------|
|
||||||
|
| 1 | Add getMissingNumbers() and calcOmitCount() to History model | 6386a40 | application/admin/model/History.php |
|
||||||
|
| 2 | Add missingNum() AJAX endpoint to History controller | 15bb870 | application/admin/controller/History.php |
|
||||||
|
| 3 | Add i18n text keys to language file | 96d5e78 | application/admin/lang/zh-cn/history.php |
|
||||||
|
|
||||||
|
## One-liner
|
||||||
|
|
||||||
|
History model `getMissingNumbers()` computes missing lottery numbers (1-49) with true omission counts from up to 500 historical records, controller `missingNum()` validates periods (1-100) and returns JSON, i18n file provides Chinese translations.
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
- **$noNeedRight minimal scope**: Only `missingNum` method is exempted from permission checks (not `['*']`), following least-privilege principle.
|
||||||
|
- **500-record limit for omission calculation**: Queries up to 500 historical records to calculate true omission counts (last appearance distance), not just "appeared/not appeared" in the query window.
|
||||||
|
- **Color fallback**: Returns `'—'` when fa_num table lacks a wave color mapping for a given number.
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Known Stubs
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Threat Flags
|
||||||
|
|
||||||
|
None beyond what was identified in the plan's threat model (T-01-01 through T-01-04).
|
||||||
|
|
||||||
|
## Self-Check
|
||||||
|
|
||||||
|
- Model file exists with both methods: PASS
|
||||||
|
- Controller file exists with missingNum endpoint: PASS
|
||||||
|
- Lang file exists with all i18n keys: PASS
|
||||||
|
- All 3 commits present in git log: PASS
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
---
|
||||||
|
phase: 01
|
||||||
|
plan: 02
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- public/assets/js/backend/history.js
|
||||||
|
- application/admin/view/history/index.html
|
||||||
|
autonomous: true
|
||||||
|
requirements: [OMIT-01, OMIT-02, OMIT-03]
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "用户在 history 页面能看到'遗漏号码'按钮"
|
||||||
|
- "点击按钮后弹出 Layer 模态窗口"
|
||||||
|
- "弹窗内有期数输入框(默认值 10)和查询按钮"
|
||||||
|
- "点击查询后显示结果区域,包含遗漏号码、遗漏期数、波色球"
|
||||||
|
artifacts:
|
||||||
|
- path: "application/admin/view/history/index.html"
|
||||||
|
provides: "history 页面 toolbar 区域的'遗漏号码'按钮"
|
||||||
|
contains: "btn-missingnum"
|
||||||
|
- path: "public/assets/js/backend/history.js"
|
||||||
|
provides: "按钮点击处理、Layer 弹窗、AJAX 请求、结果渲染"
|
||||||
|
exports: ["showMissingNumDialog", "queryMissingNum", "renderMissingNum"]
|
||||||
|
key_links:
|
||||||
|
- from: "application/admin/view/history/index.html"
|
||||||
|
to: "public/assets/js/backend/history.js"
|
||||||
|
via: "toolbar 按钮 class .btn-missingnum 绑定 click 事件"
|
||||||
|
pattern: "btn-missingnum"
|
||||||
|
- from: "public/assets/js/backend/history.js"
|
||||||
|
to: "history/missingNum"
|
||||||
|
via: "$.ajax 请求后端接口"
|
||||||
|
pattern: "history/missingNum"
|
||||||
|
- from: "public/assets/js/backend/history.js"
|
||||||
|
to: "Controller.api.getColorByNum()"
|
||||||
|
via: "渲染波色球时复用已有颜色函数"
|
||||||
|
pattern: "getColorByNum"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
在 history 页面添加"遗漏号码"按钮和 Layer 弹窗 UI,包含期数输入框、查询按钮、结果展示区域和波色球渲染,复用已有的 getColorByNum() 颜色函数。
|
||||||
|
|
||||||
|
Purpose: 提供用户交互界面(per D-01: 按钮+弹窗形式,不新增独立页面/菜单;per D-03: Layer 弹窗展示)
|
||||||
|
Output: toolbar 按钮 + Layer 弹窗 HTML + 结果渲染逻辑
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@D:/code/php/amlhc/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@D:/code/php/amlhc/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/01-omitted-number-analysis/01-RESEARCH.md
|
||||||
|
@D:/code/php/amlhc/public/assets/js/backend/history.js
|
||||||
|
@D:/code/php/amlhc/application/admin/view/history/index.html
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
|
||||||
|
|
||||||
|
From public/assets/js/backend/history.js:
|
||||||
|
```javascript
|
||||||
|
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
|
||||||
|
var Controller = {
|
||||||
|
index: function () { ... },
|
||||||
|
add: function () { ... },
|
||||||
|
edit: function () { ... },
|
||||||
|
api: {
|
||||||
|
colorMap: {}, // 波色映射缓存 {num: colorText}
|
||||||
|
colorMapLoaded: false, // 是否已加载
|
||||||
|
loadColorMap: function (callback) { ... }, // 异步加载,完成后调用 callback
|
||||||
|
getColorByNum: function (num) { ... }, // 返回 CSS 颜色值字符串
|
||||||
|
formatter: {
|
||||||
|
numBall: function (value, row, index) { ... } // 表格内号码球渲染
|
||||||
|
},
|
||||||
|
bindevent: function () { ... }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return Controller;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
From application/admin/view/history/index.html:
|
||||||
|
- toolbar 区域 id="toolbar",已有刷新按钮
|
||||||
|
- 使用 Bootstrap 3 样式类:btn btn-primary, form-control, text-center, text-danger 等
|
||||||
|
- Layer 对象全局可用(通过 requirejs 加载 fastadmin-layer)
|
||||||
|
|
||||||
|
FastAdmin AJAX response format:
|
||||||
|
```json
|
||||||
|
{ "code": 1, "msg": "查询成功", "data": [...] } // success
|
||||||
|
{ "code": 0, "msg": "错误信息" } // error
|
||||||
|
```
|
||||||
|
code === 1 表示成功。
|
||||||
|
|
||||||
|
Layer.open() API:
|
||||||
|
```javascript
|
||||||
|
Layer.open({
|
||||||
|
type: 1, // 1 = page 类型,使用 content 中的 HTML
|
||||||
|
title: '标题',
|
||||||
|
area: ['650px', '550px'], // [width, height]
|
||||||
|
content: html, // HTML 字符串
|
||||||
|
shadeClose: true // 点击遮罩关闭
|
||||||
|
});
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="false">
|
||||||
|
<name>Task 1: 在 history 页面 toolbar 添加"遗漏号码"按钮</name>
|
||||||
|
<files>application/admin/view/history/index.html</files>
|
||||||
|
<read_first>
|
||||||
|
- application/admin/view/history/index.html(当前 toolbar 结构)
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
在 application/admin/view/history/index.html 的 toolbar 区域(id="toolbar")中添加"遗漏号码"按钮。
|
||||||
|
|
||||||
|
当前 toolbar 内容(第 8-11 行):
|
||||||
|
```html
|
||||||
|
<div id="toolbar" class="toolbar">
|
||||||
|
<a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
|
||||||
|
<!-- <a href="javascript:;" class="btn btn-success btn-add ...
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
在刷新按钮之后、注释的添加按钮之前,插入遗漏号码按钮:
|
||||||
|
```html
|
||||||
|
<a href="javascript:;" class="btn btn-warning btn-missingnum" title="{:__('Missing Number Analysis')}"><i class="fa fa-search"></i> {:__('Missing Number Analysis')}</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
完整 toolbar 变为:
|
||||||
|
```html
|
||||||
|
<div id="toolbar" class="toolbar">
|
||||||
|
<a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
|
||||||
|
<a href="javascript:;" class="btn btn-warning btn-missingnum" title="{:__('Missing Number Analysis')}"><i class="fa fa-search"></i> {:__('Missing Number Analysis')}</a>
|
||||||
|
<!-- <a href="javascript:;" class="btn btn-success btn-add {:$auth->check('history/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>-->
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
关键实现细节:
|
||||||
|
- 使用 `btn-warning` 样式(黄色按钮,区别于刷新按钮的蓝色)
|
||||||
|
- class 必须包含 `btn-missingnum`(JS 事件绑定选择器)
|
||||||
|
- 图标使用 `fa fa-search`(搜索图标,符合查询语义)
|
||||||
|
- 文本使用 `{:__('Missing Number Analysis')}` 通过 i18n 函数获取(对应 plan 01 task 3 添加的语言键)
|
||||||
|
- title 属性同样使用 i18n
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- application/admin/view/history/index.html 的 toolbar 中包含 class="btn-missingnum" 的 <a> 元素
|
||||||
|
- 按钮 class 包含 btn-warning
|
||||||
|
- 按钮包含 {:__('Missing Number Analysis')} 文本调用
|
||||||
|
- 按钮图标 class 为 fa fa-search
|
||||||
|
- 按钮位于 btn-refresh 之后
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>grep "btn-missingnum" application/admin/view/history/index.html</automated>
|
||||||
|
</verify>
|
||||||
|
<done>history 页面 toolbar 出现黄色"遗漏号码"按钮,使用 i18n 文本和搜索图标</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="false">
|
||||||
|
<name>Task 2: 在 history.js 中添加按钮事件绑定、Layer 弹窗和结果渲染逻辑</name>
|
||||||
|
<files>public/assets/js/backend/history.js</files>
|
||||||
|
<read_first>
|
||||||
|
- public/assets/js/backend/history.js(当前 JS 结构,特别是 Controller.api 对象)
|
||||||
|
- public/assets/js/backend/command.js(参考 Layer.alert / Layer.open 使用模式)
|
||||||
|
- public/assets/js/backend/general/config.js(参考 Layer 弹窗内 HTML + 事件绑定模式)
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
在 public/assets/js/backend/history.js 中添加遗漏号码弹窗相关代码。
|
||||||
|
|
||||||
|
**位置 1:在 Controller.index() 函数内,Table.api.bindevent(table) 调用之后,添加按钮事件绑定:**
|
||||||
|
|
||||||
|
在 `Table.api.bindevent(table);` 这一行之后添加:
|
||||||
|
```javascript
|
||||||
|
// 遗漏号码按钮事件
|
||||||
|
$(document).off('click', '.btn-missingnum').on('click', '.btn-missingnum', function () {
|
||||||
|
Controller.api.showMissingNumDialog();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
使用 `off().on()` 防止重复绑定(FastAdmin 在 tab 切换时可能重复初始化)。
|
||||||
|
|
||||||
|
**位置 2:在 Controller.api 对象中添加三个新方法(在 bindevent 方法之前):**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* 显示遗漏号码分析弹窗
|
||||||
|
*/
|
||||||
|
showMissingNumDialog: function () {
|
||||||
|
var html = '<div style="padding:20px;">' +
|
||||||
|
'<div class="form-group">' +
|
||||||
|
' <label>' + __('Query Periods') + ':</label>' +
|
||||||
|
' <input type="number" id="missing-periods" class="form-control" value="10" min="1" max="100" style="width:120px;display:inline-block;">' +
|
||||||
|
' <button class="btn btn-primary" id="btn-missing-query" style="margin-left:10px;"><i class="fa fa-search"></i> ' + __('Query') + '</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div id="missing-result" style="margin-top:15px;"></div>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
Layer.open({
|
||||||
|
type: 1,
|
||||||
|
title: __('Missing Number Analysis'),
|
||||||
|
area: ['650px', '550px'],
|
||||||
|
content: html,
|
||||||
|
shadeClose: true,
|
||||||
|
success: function (layero, index) {
|
||||||
|
// 绑定查询按钮事件
|
||||||
|
$('#btn-missing-query', layero).on('click', function () {
|
||||||
|
var periods = parseInt($('#missing-periods', layero).val()) || 10;
|
||||||
|
Controller.api.queryMissingNum(periods, layero);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询遗漏号码
|
||||||
|
*/
|
||||||
|
queryMissingNum: function (periods, layero) {
|
||||||
|
// 确保颜色映射已加载
|
||||||
|
if (!Controller.api.colorMapLoaded) {
|
||||||
|
Controller.api.loadColorMap(function () {
|
||||||
|
Controller.api._doQueryMissingNum(periods, layero);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Controller.api._doQueryMissingNum(periods, layero);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行遗漏号码查询(内部方法)
|
||||||
|
*/
|
||||||
|
_doQueryMissingNum: function (periods, layero) {
|
||||||
|
$('#missing-result', layero).html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> ' + __('Loading') + '</div>');
|
||||||
|
$.ajax({
|
||||||
|
url: 'history/missingNum',
|
||||||
|
type: 'GET',
|
||||||
|
data: { periods: periods },
|
||||||
|
dataType: 'json',
|
||||||
|
success: function (ret) {
|
||||||
|
if (ret.code == 1) {
|
||||||
|
Controller.api.renderMissingNum(ret.data, periods, layero);
|
||||||
|
} else {
|
||||||
|
$('#missing-result', layero).html('<div class="alert alert-danger">' + (ret.msg || __('Query failed')) + '</div>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
$('#missing-result', layero).html('<div class="alert alert-danger">' + __('Query failed') + '</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染遗漏号码结果
|
||||||
|
*/
|
||||||
|
renderMissingNum: function (data, periods, layero) {
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
$('#missing-result', layero).html('<div class="alert alert-info">' + __('No missing numbers found') + '</div>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var html = '<div style="display:flex;flex-wrap:wrap;gap:12px;">';
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
var color = Controller.api.getColorByNum(data[i].num);
|
||||||
|
html += '<div style="text-align:center;">' +
|
||||||
|
'<span class="num-ball" style="display:inline-block;width:48px;height:48px;line-height:48px;text-align:center;border-radius:50%;color:#fff;background-color:' + color + ';font-weight:bold;font-size:18px;">' + data[i].num + '</span>' +
|
||||||
|
'<div style="margin-top:5px;font-size:12px;color:#666;">' + __('Missing') + ' ' + data[i].omit + ' ' + __('periods') + '</div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
$('#missing-result', layero).html(html);
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
关键实现细节:
|
||||||
|
- 弹窗 HTML 使用 Bootstrap 3 form-group 和 form-control 样式
|
||||||
|
- 输入框 type="number",默认值 10,min=1,max=100,宽度 120px 内联显示
|
||||||
|
- 查询按钮在输入框右侧,margin-left:10px
|
||||||
|
- 结果区域 id="missing-result",初始为空
|
||||||
|
- Layer.open 使用 type:1(page 类型),area: ['650px', '550px'],shadeClose:true(点击遮罩关闭)
|
||||||
|
- success 回调中绑定查询按钮事件,使用 layero 作为上下文选择器根(`$('#btn-missing-query', layero)`)
|
||||||
|
- queryMissingNum 先检查 colorMapLoaded,未加载则先调用 loadColorMap
|
||||||
|
- AJAX 使用标准 $.ajax,url 为 'history/missingNum',type 为 'GET'
|
||||||
|
- 响应判断 `ret.code == 1`(FastAdmin 成功标志)
|
||||||
|
- 加载状态显示 spinner + 文字
|
||||||
|
- 渲染时使用 flex 布局(display:flex; flex-wrap:wrap; gap:12px)展示球网格
|
||||||
|
- 每个球 48x48px,圆角 50%,白色文字,背景色来自 getColorByNum()
|
||||||
|
- 球下方显示"遗漏 X 期"文字,12px 灰色字体
|
||||||
|
- 所有文本使用 __('key') 获取 i18n
|
||||||
|
- 空结果时显示 alert-info 提示
|
||||||
|
|
||||||
|
不要修改现有的 Controller.index、Controller.add、Controller.edit 方法签名。只在 index 函数内追加按钮事件绑定,在 api 对象内追加新方法。
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- public/assets/js/backend/history.js 的 Controller.index() 中包含 `$(document).off('click', '.btn-missingnum').on('click', '.btn-missingnum'`
|
||||||
|
- Controller.api 对象包含 `showMissingNumDialog` 方法
|
||||||
|
- Controller.api 对象包含 `queryMissingNum` 方法
|
||||||
|
- Controller.api 对象包含 `_doQueryMissingNum` 方法
|
||||||
|
- Controller.api 对象包含 `renderMissingNum` 方法
|
||||||
|
- showMissingNumDialog 调用 Layer.open({type: 1, ...})
|
||||||
|
- Layer.open 的 content 包含 input type="number" id="missing-periods"
|
||||||
|
- queryMissingNum 检查 colorMapLoaded 状态
|
||||||
|
- _doQueryMissingNum 使用 $.ajax 请求 url: 'history/missingNum'
|
||||||
|
- renderMissingNum 使用 flex-wrap 布局渲染球网格
|
||||||
|
- renderMissingNum 调用 Controller.api.getColorByNum() 获取颜色
|
||||||
|
- 结果球显示数字 + 下方"遗漏 X 期"文字
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>grep -c "showMissingNumDialog\|queryMissingNum\|renderMissingNum\|btn-missingnum" public/assets/js/backend/history.js | awk '$1 >= 4'</automated>
|
||||||
|
</verify>
|
||||||
|
<done>history.js 有完整的遗漏号码按钮处理、Layer 弹窗(含期数输入和查询按钮)、AJAX 请求、结果渲染(波色球+遗漏期数)逻辑</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| User input (number field) → AJAX request | 用户可能在浏览器开发者工具中修改 periods 值 |
|
||||||
|
| Layer dialog HTML injection | 弹窗 HTML 由 JS 拼接,无外部输入注入风险 |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-01-05 | Tampering | 前端 periods 输入值 | mitigate | 后端已有 range 校验 1-100;前端 HTML input min/max 为 UX 辅助,不依赖前端校验 |
|
||||||
|
| T-01-06 | Information Disclosure | Layer 弹窗内容 | accept | 仅展示遗漏号码统计数据,无敏感信息 |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- 在浏览器中访问 admin history 页面,确认 toolbar 出现黄色"遗漏号码"按钮
|
||||||
|
- 点击按钮,确认 Layer 弹窗打开,包含期数输入框(默认 10)和查询按钮
|
||||||
|
- 输入 10 点击查询,确认出现加载状态 → 结果网格(带颜色的球 + 遗漏期数文字)
|
||||||
|
- 输入 0 或 200 点击查询,确认后端返回错误提示
|
||||||
|
- 确认结果球颜色与 history 表格中的波色球一致(复用 getColorByNum)
|
||||||
|
- 确认结果按遗漏期数从大到小排列(视觉上最大 omit 的球在最左)
|
||||||
|
- 点击弹窗遮罩,确认弹窗关闭
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- [ ] history 页面 toolbar 有"遗漏号码"按钮(btn-warning 样式)
|
||||||
|
- [ ] 点击按钮弹出 Layer 弹窗,标题为"遗漏号码分析"
|
||||||
|
- [ ] 弹窗内有期数输入框(默认值 10,范围 1-100)和查询按钮
|
||||||
|
- [ ] 点击查询后显示加载状态,然后显示结果
|
||||||
|
- [ ] 结果以 flex 网格展示,每个球显示号码、波色、遗漏期数
|
||||||
|
- [ ] 波色球颜色与表格中一致(复用 getColorByNum)
|
||||||
|
- [ ] 无遗漏号码时显示友好提示
|
||||||
|
- [ ] 点击遮罩可关闭弹窗
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/01-omitted-number-analysis/01-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
phase: 01-omitted-number-analysis
|
||||||
|
plan: 02
|
||||||
|
subsystem: ui
|
||||||
|
tags: [layer, bootstrap, requirejs, jquery, fastadmin, thinkphp]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 01-omitted-number-analysis
|
||||||
|
provides: 01-01 backend missingNum endpoint (parallel wave, to be verified)
|
||||||
|
provides:
|
||||||
|
- History page toolbar "Missing Number Analysis" button
|
||||||
|
- Layer dialog with period input (default 10, range 1-100) and query button
|
||||||
|
- AJAX integration to history/missingNum endpoint
|
||||||
|
- Result rendering with colored num-balls and omission count display
|
||||||
|
- Reuse of existing getColorByNum() for color consistency
|
||||||
|
affects: [01-03 integration verification, future omission trend analysis]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "Layer.open({type:1}) with inline HTML for custom dialogs"
|
||||||
|
- "jQuery $(document).off().on() for delegated event binding to prevent duplicates"
|
||||||
|
- "Async color map loading guard before rendering colored elements"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- application/admin/view/history/index.html (added button to existing toolbar)
|
||||||
|
modified:
|
||||||
|
- public/assets/js/backend/history.js (added 4 new API methods + button handler)
|
||||||
|
- application/admin/lang/zh-cn/history.php (added i18n keys for dialog text)
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Used Layer.open type:1 with inline HTML instead of Layer.prompt or Fast.api.open (needs input + button + results area)"
|
||||||
|
- "Delegated event binding on document with .off().on() to prevent duplicate handlers on tab re-initialization"
|
||||||
|
- "queryMissingNum checks colorMapLoaded before AJAX to ensure balls render with correct colors"
|
||||||
|
- "All text uses __() i18n function with keys in lang/zh-cn/history.php"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Dialog pattern: Layer.open with success callback for binding events within layero context"
|
||||||
|
- "AJAX pattern: GET request to controller method, check ret.code==1 for success"
|
||||||
|
- "Rendering pattern: flex-wrap grid with inline-styled num-ball components"
|
||||||
|
|
||||||
|
requirements-completed: [OMIT-01, OMIT-02, OMIT-03]
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 5min
|
||||||
|
completed: 2026-04-21
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 01 Plan 02: History Toolbar Button + Layer Dialog UI Summary
|
||||||
|
|
||||||
|
**Missing number analysis button + Layer dialog with period input, AJAX query, and colored ball result rendering on history admin page**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** ~5 min
|
||||||
|
- **Started:** 2026-04-21T13:06:00Z
|
||||||
|
- **Completed:** 2026-04-21T13:11:00Z
|
||||||
|
- **Tasks:** 2
|
||||||
|
- **Files modified:** 3 (1 toolbar, 1 JS, 1 lang)
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
- Added "Missing Number Analysis" button (btn-warning, fa-search icon) to history page toolbar
|
||||||
|
- Implemented Layer dialog with period number input (default 10, range 1-100) and query button
|
||||||
|
- Built AJAX integration to `history/missingNum` endpoint with loading spinner and error handling
|
||||||
|
- Implemented flex-wrap grid rendering of missing numbers as colored balls with omission count labels
|
||||||
|
- Added all required i18n keys for dialog text in Chinese
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Add "遗漏号码" button to history page toolbar** - `4104746` (feat)
|
||||||
|
2. **Task 2: Button event binding, Layer dialog, and result rendering logic** - `538e414` (feat)
|
||||||
|
3. **Deviation: Add missing i18n language keys** - `637b847` (i18n)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
- `D:/code/php/amlhc/.claude/worktrees/agent-a5a02f12/application/admin/view/history/index.html` - Added btn-missingnum button to toolbar
|
||||||
|
- `D:/code/php/amlhc/.claude/worktrees/agent-a5a02f12/public/assets/js/backend/history.js` - Added showMissingNumDialog, queryMissingNum, _doQueryMissingNum, renderMissingNum methods
|
||||||
|
- `D:/code/php/amlhc/.claude/worktrees/agent-a5a02f12/application/admin/lang/zh-cn/history.php` - Added 8 i18n keys for dialog text
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
- Used `$(document).off('click', '.btn-missingnum').on('click', '.btn-missingnum', ...)` instead of `$('#toolbar').on('click', '.btn-missingnum', ...)` to prevent duplicate binding when FastAdmin reinitializes on tab switches
|
||||||
|
- Checked `colorMapLoaded` in `queryMissingNum` before calling `_doQueryMissingNum` to ensure balls render with correct colors even if color map hasn't been loaded yet
|
||||||
|
- All display text uses `__('key')` i18n function rather than hardcoded Chinese strings, matching project convention
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [Rule 2 - Missing Critical] Added i18n keys for dialog text**
|
||||||
|
- **Found during:** Task 2 (Layer dialog implementation)
|
||||||
|
- **Issue:** The lang/zh-cn/history.php file only had Expect, OpenTime, Num7 keys. The dialog uses __() calls for 8 additional keys (Missing Number Analysis, Query Periods, Query, Loading, Missing, periods, Query failed, No missing numbers found) which would display as raw English keys instead of Chinese text
|
||||||
|
- **Fix:** Added all 8 i18n keys to application/admin/lang/zh-cn/history.php
|
||||||
|
- **Files modified:** application/admin/lang/zh-cn/history.php
|
||||||
|
- **Verification:** Verified lang file contains all keys referenced by __() calls in history.js
|
||||||
|
- **Committed in:** `637b847` (separate commit)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Total deviations:** 1 auto-fixed (1 missing critical - i18n)
|
||||||
|
**Impact on plan:** Essential for correct display of Chinese text in dialog. No scope creep.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
- Worktree missing base files: The worktree was created from commit `e1cb014` which only contains planning files. All existing application code (controller, model, view, lang, JS) was untracked in the main repo and had to be copied into the worktree before modifications could be committed. Resolved by copying from the main repo's working directory.
|
||||||
|
|
||||||
|
## Threat Surface Scan
|
||||||
|
|
||||||
|
| Flag | File | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| threat_flag: XSS | public/assets/js/backend/history.js | Dialog HTML is JS-concatenated with no external input; safe. AJAX response data displayed directly — backend should sanitize (plan 01-01 responsibility) |
|
||||||
|
|
||||||
|
## Known Stubs
|
||||||
|
None. This plan delivers UI only; data rendering depends on backend endpoint from plan 01-01.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
- Frontend UI complete and ready for integration with backend `missingNum` endpoint (plan 01-01)
|
||||||
|
- Requires plan 01-01 to provide `History::missingNum()` controller method with proper input validation (periods 1-100)
|
||||||
|
- Plan 01-03 will verify end-to-end integration (button -> dialog -> AJAX -> backend -> result display)
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 01-omitted-number-analysis*
|
||||||
|
*Completed: 2026-04-21*
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
---
|
||||||
|
phase: 01
|
||||||
|
plan: 03
|
||||||
|
type: execute
|
||||||
|
wave: 2
|
||||||
|
depends_on: [01-01, 01-02]
|
||||||
|
files_modified:
|
||||||
|
- public/assets/js/backend/history.js
|
||||||
|
autonomous: false
|
||||||
|
requirements: [OMIT-02, OMIT-03, OMIT-04]
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "前后端联调通过:前端请求能正确到达后端接口并获取数据"
|
||||||
|
- "结果按遗漏期数从大到小正确渲染"
|
||||||
|
- "波色球着色与已有颜色映射一致"
|
||||||
|
artifacts:
|
||||||
|
- path: "public/assets/js/backend/history.js"
|
||||||
|
provides: "联调验证逻辑和边界情况处理"
|
||||||
|
contains: "history/missingNum"
|
||||||
|
key_links:
|
||||||
|
- from: "public/assets/js/backend/history.js"
|
||||||
|
to: "application/admin/controller/History.php::missingNum()"
|
||||||
|
via: "$.ajax GET history/missingNum?periods=X"
|
||||||
|
pattern: "history/missingNum"
|
||||||
|
- from: "public/assets/js/backend/history.js::renderMissingNum"
|
||||||
|
to: "后端返回 data[].omit"
|
||||||
|
via: "按 omit 降序渲染(后端已排序)"
|
||||||
|
pattern: "data\\[i\\]\\.omit"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
前后端联调验证:确保 history.js 的 AJAX 请求正确调用 history/missingNum 接口,结果按遗漏期数降序渲染,波色球着色与已有映射一致,并处理边界情况(无数据、加载失败、颜色映射未就绪)。
|
||||||
|
|
||||||
|
Purpose: 验证完整功能链路(per D-03: $.ajax 请求遗漏接口)
|
||||||
|
Output: 联调验证通过,边界情况处理完善
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@D:/code/php/amlhc/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@D:/code/php/amlhc/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/01-omitted-number-analysis/01-RESEARCH.md
|
||||||
|
@D:/code/php/amlhc/public/assets/js/backend/history.js(plan 01-02 修改后的版本)
|
||||||
|
@D:/code/php/amlhc/application/admin/controller/History.php(plan 01-01 修改后的版本)
|
||||||
|
@D:/code/php/amlhc/application/admin/model/History.php(plan 01-01 修改后的版本)
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- Key types and contracts from plans 01-01 and 01-02. -->
|
||||||
|
|
||||||
|
Backend endpoint (from plan 01-01):
|
||||||
|
```
|
||||||
|
GET /admin/history/missingNum?periods=X
|
||||||
|
Response success: {code: 1, msg: "查询成功", data: [{num: int, omit: int, color: string}, ...]}
|
||||||
|
Response error: {code: 0, msg: "期数范围必须在 1-100 之间"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Frontend API (from plan 01-02):
|
||||||
|
```javascript
|
||||||
|
Controller.api.showMissingNumDialog() // opens Layer dialog
|
||||||
|
Controller.api.queryMissingNum(periods, layero) // initiates AJAX
|
||||||
|
Controller.api.renderMissingNum(data, periods, layero) // renders result grid
|
||||||
|
Controller.api.colorMapLoaded // boolean: color map ready flag
|
||||||
|
Controller.api.getColorByNum(num) // returns CSS color string
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="false">
|
||||||
|
<name>Task 1: 验证联调链路并完善边界情况处理</name>
|
||||||
|
<files>public/assets/js/backend/history.js</files>
|
||||||
|
<read_first>
|
||||||
|
- public/assets/js/backend/history.js(plan 01-02 修改后的完整文件)
|
||||||
|
- application/admin/controller/History.php(plan 01-01 修改后的控制器)
|
||||||
|
- application/admin/model/History.php(plan 01-01 修改后的模型)
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
检查 plan 01-02 创建的 JS 代码,确保以下边界情况已正确处理。如果已有则跳过,如果缺失则补充。
|
||||||
|
|
||||||
|
**边界情况 1:colorMap 未加载时的处理**
|
||||||
|
确认 queryMissingNum 方法在 colorMapLoaded === false 时,先调用 loadColorMap 等待完成再发起请求。代码应类似:
|
||||||
|
```javascript
|
||||||
|
queryMissingNum: function (periods, layero) {
|
||||||
|
if (!Controller.api.colorMapLoaded) {
|
||||||
|
Controller.api.loadColorMap(function () {
|
||||||
|
Controller.api._doQueryMissingNum(periods, layero);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Controller.api._doQueryMissingNum(periods, layero);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
**边界情况 2:后端返回空数据(所有号码在最近 X 期都出现过)**
|
||||||
|
确认 renderMissingNum 方法在 data.length === 0 时显示友好提示而非空白:
|
||||||
|
```javascript
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
$('#missing-result', layero).html('<div class="alert alert-info">...</div>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**边界情况 3:AJAX 请求失败(网络错误、服务器 500)**
|
||||||
|
确认 _doQueryMissingNum 的 error 回调正确显示错误信息:
|
||||||
|
```javascript
|
||||||
|
error: function () {
|
||||||
|
$('#missing-result', layero).html('<div class="alert alert-danger">请求失败</div>');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**边界情况 4:波色球颜色兜底**
|
||||||
|
确认 renderMissingNum 中 getColorByNum 对未映射号码返回灰色 (#95a5a6)——这已在 getColorByNum 方法中实现,此处只需确保调用正确。
|
||||||
|
|
||||||
|
**边界情况 5:快速重复点击查询按钮**
|
||||||
|
在 _doQueryMissingNum 中,发起请求前禁用查询按钮,请求完成后恢复:
|
||||||
|
```javascript
|
||||||
|
var $btn = $('#btn-missing-query', layero);
|
||||||
|
$btn.prop('disabled', true);
|
||||||
|
$.ajax({
|
||||||
|
...
|
||||||
|
complete: function () {
|
||||||
|
$btn.prop('disabled', false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
如果上述边界情况在 plan 01-02 的代码中已经处理,本任务仅做验证性读取确认,不做修改。如果有缺失项,补充对应代码。
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- queryMissingNum 包含 `if (!Controller.api.colorMapLoaded)` 检查
|
||||||
|
- queryMissingNum 在 colorMapLoaded 为 false 时调用 `Controller.api.loadColorMap(function(){...})`
|
||||||
|
- renderMissingNum 包含 `data.length === 0` 的空数据处理分支
|
||||||
|
- _doQueryMissingNum 包含 error 回调函数
|
||||||
|
- _doQueryMissingNum 的 $.ajax 包含 complete 回调用于恢复按钮状态
|
||||||
|
- _doQueryMissingNum 在请求前设置 `$('#btn-missing-query', layero).prop('disabled', true)`
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>grep -c "colorMapLoaded\|data\.length === 0\|\.prop.*disabled\|complete:" public/assets/js/backend/history.js | awk '$1 >= 4'</automated>
|
||||||
|
</verify>
|
||||||
|
<done>所有边界情况(颜色映射未就绪、空数据、请求失败、按钮防重复点击、波色兜底)均已正确处理</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="checkpoint:human-verify" gate="blocking">
|
||||||
|
<name>Task 2: 人工验证完整功能链路</name>
|
||||||
|
<files>public/assets/js/backend/history.js, application/admin/controller/History.php, application/admin/model/History.php</files>
|
||||||
|
<what-built>
|
||||||
|
后端 missingNum 接口 + 前端弹窗 UI + AJAX 联调 + 边界情况处理
|
||||||
|
</what-built>
|
||||||
|
<how-to-verify>
|
||||||
|
按以下步骤在浏览器中验证(假设 admin 后台地址为 http://localhost/ByZjtVrKok.php):
|
||||||
|
|
||||||
|
1. **登录 admin 后台**,进入 history 页面
|
||||||
|
2. **检查按钮**:确认 toolbar 出现黄色"遗漏号码"按钮(带搜索图标)
|
||||||
|
3. **打开弹窗**:点击按钮,确认弹出 Layer 窗口,标题为"遗漏号码分析"
|
||||||
|
4. **检查弹窗内容**:确认有"查询期数:"标签、数字输入框(默认值 10)、查询按钮
|
||||||
|
5. **正常查询**:保持默认值 10,点击查询
|
||||||
|
- 确认出现"查询中..."加载提示(带 spinner)
|
||||||
|
- 确认加载完成后显示结果网格:每个球显示号码(带颜色)+ 下方"遗漏 X 期"文字
|
||||||
|
- 确认结果从左到右按遗漏期数从大到小排列(最左边 omit 最大)
|
||||||
|
- 确认球的颜色与 history 表格中的波色球一致
|
||||||
|
6. **边界值测试**:
|
||||||
|
- 输入 1 点击查询,确认返回结果
|
||||||
|
- 输入 100 点击查询,确认返回结果
|
||||||
|
- 输入 0 点击查询,确认显示错误提示"期数范围必须在 1-100 之间"
|
||||||
|
- 输入 200 点击查询,确认显示错误提示
|
||||||
|
7. **防重复点击**:点击查询按钮后,确认按钮变灰(disabled),请求完成后恢复可点击
|
||||||
|
8. **关闭弹窗**:点击弹窗外的遮罩区域,确认弹窗关闭
|
||||||
|
9. **重复打开**:再次点击"遗漏号码"按钮,确认弹窗正常打开且输入框恢复默认值 10
|
||||||
|
</how-to-verify>
|
||||||
|
<resume-signal>验证通过请回复"approved",如有问题请描述具体现象</resume-signal>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| AJAX response → DOM rendering | 后端返回的数据直接注入 DOM,需确保无 XSS 风险 |
|
||||||
|
| User rapid-click → multiple AJAX requests | 可能导致竞态条件或服务器负载 |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-01-07 | Tampering | renderMissingNum DOM 渲染 | mitigate | 使用 textContent/innerText 或 jQuery .text() 渲染号码数字,不使用 .html() 注入原始数据;球的颜色通过 style.backgroundColor 设置 |
|
||||||
|
| T-01-08 | Denial of Service | 快速重复点击 | mitigate | 请求期间禁用查询按钮(complete 回调恢复),防止并发请求 |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- 所有边界情况已在代码中处理(grep 验证通过)
|
||||||
|
- 人工验证 9 个步骤全部通过
|
||||||
|
- 无 JavaScript 控制台错误
|
||||||
|
- 后端无 PHP 错误日志
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- [ ] 前端 AJAX 请求正确调用后端 missingNum 接口
|
||||||
|
- [ ] 后端返回的 {num, omit, color} 数据正确渲染为波色球网格
|
||||||
|
- [ ] 结果按遗漏期数从大到小排列
|
||||||
|
- [ ] 波色球颜色与表格中一致
|
||||||
|
- [ ] 空数据时显示友好提示
|
||||||
|
- [ ] 参数超出范围时显示错误提示
|
||||||
|
- [ ] 请求期间按钮被禁用防止重复提交
|
||||||
|
- [ ] 点击遮罩可关闭弹窗
|
||||||
|
- [ ] 无 JavaScript 控制台错误
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/01-omitted-number-analysis/01-03-SUMMARY.md`
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
---
|
||||||
|
phase: 01-omitted-number-analysis
|
||||||
|
plan: 03
|
||||||
|
subsystem: integration
|
||||||
|
tags: [jquery, layer, fastadmin, thinkphp, ajax, xss-prevention]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 01-omitted-number-analysis
|
||||||
|
provides: 01-01 backend missingNum endpoint + 01-02 history toolbar button + Layer dialog UI
|
||||||
|
provides:
|
||||||
|
- Complete end-to-end integration: button -> dialog -> AJAX -> backend -> result display
|
||||||
|
- Boundary case handling: colorMap not loaded, empty data, AJAX failure, duplicate click prevention
|
||||||
|
- XSS mitigation: jQuery .text() used for DOM injection instead of string concatenation
|
||||||
|
affects: [future omission trend analysis, any feature reusing missingNum endpoint]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "Button disabled during AJAX request via $btn.prop('disabled', true/false) with complete callback"
|
||||||
|
- "Safe DOM rendering: jQuery .text() for numbers and labels, .css() for colors — no .html() with external data"
|
||||||
|
- "Color map loaded guard: queryMissingNum checks colorMapLoaded before dispatching AJAX"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created: []
|
||||||
|
modified:
|
||||||
|
- public/assets/js/backend/history.js (added button disable/restore, XSS-safe rendering via jQuery DOM methods)
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Used jQuery .text() instead of string concatenation for rendering number and omission label — mitigates XSS from untrusted API data (T-01-07)"
|
||||||
|
- "Button disabled state managed via $btn.prop('disabled', true) before AJAX, restored in complete callback — ensures single-request-at-a-time (T-01-08)"
|
||||||
|
- "No code changes needed for colorMapLoaded guard, empty data handling, or error callback — already correct from plan 01-02"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "All external data (numbers, omission counts) rendered via .text() — colors applied via .css('background-color', ...) — never .html() with API data"
|
||||||
|
- "AJAX request lifecycle: disable button -> show spinner -> request -> success/error -> complete restores button"
|
||||||
|
|
||||||
|
requirements-completed: [OMIT-02, OMIT-03, OMIT-04]
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 5min
|
||||||
|
completed: 2026-04-21
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 01 Plan 03: Integration Verification & Boundary Case Handling Summary
|
||||||
|
|
||||||
|
**End-to-end AJAX integration verified between history.js and History::missingNum() endpoint, with XSS-safe rendering and duplicate-click prevention**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** ~5 min
|
||||||
|
- **Started:** 2026-04-21T13:12:00Z
|
||||||
|
- **Completed:** 2026-04-21T13:17:00Z
|
||||||
|
- **Tasks:** 2
|
||||||
|
- **Files modified:** 1 (history.js)
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
- Verified all 5 boundary cases: colorMap not loaded, empty data, AJAX failure, button duplicate-click, color fallback
|
||||||
|
- Fixed XSS vulnerability in renderMissingNum by replacing string concatenation with jQuery .text() and .css() DOM methods
|
||||||
|
- Added button disabled/restore lifecycle to prevent duplicate AJAX requests during pending query
|
||||||
|
- Human verification passed: all 9 steps in plan confirmed working in browser (button, dialog, query, results, boundary values, close, reopen)
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Verify integration链路 and完善边界情况处理** - `bc8d38c` (fix)
|
||||||
|
- Added `$btn.prop('disabled', true)` before AJAX request
|
||||||
|
- Added `complete` callback to restore button state
|
||||||
|
- Replaced string concatenation rendering with jQuery `.text()` for XSS safety
|
||||||
|
2. **Task 2: Human verification of complete feature pipeline** - approved by user in browser
|
||||||
|
|
||||||
|
**Plan metadata:** committed with SUMMARY.md
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
- `D:/code/php/amlhc/.claude/worktrees/agent-a4fa6413/public/assets/js/backend/history.js` - Added button disable/restore, XSS-safe DOM rendering via jQuery .text()/.css()
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
- Used jQuery `.text()` for rendering number values and omission labels — this satisfies threat T-01-07 (tampering via DOM injection) by ensuring no HTML injection of external data
|
||||||
|
- Used `.css('background-color', color)` for ball colors — style-only, no HTML content risk
|
||||||
|
- Kept `colorMapLoaded` guard, `data.length === 0` check, and `error` callback as-is from plan 01-02 — all three were already correctly implemented
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [Rule 2 - Missing Critical] Fixed XSS vulnerability in renderMissingNum**
|
||||||
|
- **Found during:** Task 1 (boundary case verification)
|
||||||
|
- **Issue:** Plan 01-02 used string concatenation to build HTML with `data[i].num` and `data[i].omit` directly injected into `.html()` — if API returns malicious data, this creates XSS vector (threat T-01-07)
|
||||||
|
- **Fix:** Replaced with jQuery DOM methods: `.text(data[i].num)` for the ball number, `.text(__('Missing') + ' ' + data[i].omit + ' ' + __('periods'))` for the label, `.css('background-color', color)` for ball color
|
||||||
|
- **Files modified:** public/assets/js/backend/history.js
|
||||||
|
- **Verification:** Confirmed no `.html()` calls with external data in renderMissingNum; all data injected via `.text()` or `.css()`
|
||||||
|
- **Committed in:** `bc8d38c` (Task 1 commit)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Total deviations:** 1 auto-fixed (1 missing critical - XSS prevention)
|
||||||
|
**Impact on plan:** Essential for security. No scope creep — aligns with existing threat model T-01-07.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
- None
|
||||||
|
|
||||||
|
## Threat Surface Scan
|
||||||
|
|
||||||
|
| Flag | File | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| threat_flag: XSS (mitigated) | public/assets/js/backend/history.js | renderMissingNum now uses `.text()` for all external data injection — no `.html()` with API response data |
|
||||||
|
|
||||||
|
## Known Stubs
|
||||||
|
None. All data rendering is fully wired to the backend `missingNum` endpoint.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
- Full integration verified and working
|
||||||
|
- XSS mitigation in place for DOM rendering
|
||||||
|
- Ready for next phase (omission trend analysis or other lottery features)
|
||||||
|
- All 3 OMIT requirements (OMIT-02, OMIT-03, OMIT-04) satisfied
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 01-omitted-number-analysis*
|
||||||
|
*Completed: 2026-04-21*
|
||||||
@@ -0,0 +1,501 @@
|
|||||||
|
# Phase 1: 遗漏号码分析 - Research
|
||||||
|
|
||||||
|
**Researched:** 2026-04-21
|
||||||
|
**Domain:** FastAdmin 1.6 + ThinkPHP 5.x / AJAX endpoint + Layer modal
|
||||||
|
**Confidence:** HIGH
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This phase adds a "遗漏号码" (Missing Number) feature to the existing history admin page. The implementation requires three touchpoints: a toolbar button in the history view, a Layer dialog for user input, and a backend AJAX endpoint that calculates which numbers (1-49) did not appear in the last X periods. The missing number calculation runs entirely in PHP on the backend; the frontend only handles UI rendering.
|
||||||
|
|
||||||
|
**Primary recommendation:** Add `missingNum()` controller method in History.php with `$noNeedRight = ['*']`, use `Layer.open()` with inline HTML content for the dialog, and render results as a flex-wrapped grid of colored balls using the existing `getColorByNum()` logic already present in `history.js`.
|
||||||
|
|
||||||
|
## User Constraints (from CONTEXT.md / STATE.md)
|
||||||
|
|
||||||
|
### Locked Decisions
|
||||||
|
- 遗漏号码在 history 页面以按钮+弹窗形式展示,不新增独立页面/菜单
|
||||||
|
- 遗漏计算在后端完成,前端只负责展示
|
||||||
|
- 使用 $.ajax 请求遗漏接口,Layer 弹窗展示
|
||||||
|
|
||||||
|
### Claude's Discretion
|
||||||
|
- (None specified)
|
||||||
|
|
||||||
|
### Deferred Ideas (OUT OF SCOPE)
|
||||||
|
- 遗漏统计历史趋势图
|
||||||
|
- 遗漏号码的预测推荐
|
||||||
|
- 前台用户可见
|
||||||
|
|
||||||
|
## Phase Requirements
|
||||||
|
|
||||||
|
| ID | Description | Research Support |
|
||||||
|
|----|-------------|------------------|
|
||||||
|
| OMIT-01 | history 页面新增"遗漏号码"按钮,点击弹窗展示 | toolbar 按钮 + Layer.open() 方案 |
|
||||||
|
| OMIT-02 | 弹窗内可输入期数 X(默认 10),点击查询后展示最近 X 期未出现的号码 | Layer prompt HTML + 后端 missingNum 接口 |
|
||||||
|
| OMIT-03 | 展示内容为:遗漏号码 + 遗漏期数(多少期没出现)+ 波色球 | flex 网格 + 复用 getColorByNum() |
|
||||||
|
| OMIT-04 | 遗漏号码按遗漏期数从大到小排序 | PHP usort() / SQL ORDER BY |
|
||||||
|
| OMIT-05 | 后端接口支持查询最近 X 期开奖数据并计算遗漏号码(1-49 范围) | History::getMissingNumbers() model 方法 |
|
||||||
|
|
||||||
|
## Architectural Responsibility Map
|
||||||
|
|
||||||
|
| Capability | Primary Tier | Secondary Tier | Rationale |
|
||||||
|
|------------|-------------|----------------|-----------|
|
||||||
|
| 遗漏计算 | API / Backend | — | 需查询数据库 fa_history,属于业务逻辑 |
|
||||||
|
| 弹窗 UI | Browser / Client | — | Layer 弹窗,纯前端展示 |
|
||||||
|
| 波色球着色 | Browser / Client | API / Backend | 前端复用已有 getColorByNum() 映射 |
|
||||||
|
| 数据查询 | Database / Storage | — | SQL ORDER BY openTime DESC LIMIT X |
|
||||||
|
|
||||||
|
## Standard Stack
|
||||||
|
|
||||||
|
### Core
|
||||||
|
| Library | Version | Purpose | Why Standard |
|
||||||
|
|---------|---------|---------|--------------|
|
||||||
|
| ThinkPHP 5.x | dev-master (Gitee) | MVC framework | Project foundation, all controllers extend TP base |
|
||||||
|
| FastAdmin 1.6.1 | 1.6.2.20260323 | Admin framework | Provides Backend trait, Layer integration, Fast.api utilities |
|
||||||
|
|
||||||
|
### Supporting (Frontend)
|
||||||
|
| Library | Version | Purpose | When to Use |
|
||||||
|
|---------|---------|---------|-------------|
|
||||||
|
| fastadmin-layer | 3.5.6 | Modal/dialog overlay | All admin dialogs use Layer |
|
||||||
|
| jQuery | 3.7.1 | DOM manipulation + AJAX | Standard throughout project |
|
||||||
|
| Bootstrap 3.4.1 | via fastadmin-bootstrap | UI components | Grid, buttons, form controls |
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
No new packages needed — all dependencies already present in the project.
|
||||||
|
|
||||||
|
## Architecture Patterns
|
||||||
|
|
||||||
|
### System Architecture Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
[History Page] → User clicks "遗漏号码" button
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
[Layer.open() - Inline HTML]
|
||||||
|
│
|
||||||
|
├── Input: 期数 X (default: 10)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
[JS: $.ajax → history/missingNum]
|
||||||
|
│
|
||||||
|
│ params: { periods: X }
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
[History::missingNum() Controller]
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
[History::getMissingNumbers(periods) Model]
|
||||||
|
│
|
||||||
|
├── SELECT num1~num7 FROM fa_history ORDER BY openTime DESC LIMIT X
|
||||||
|
├── Build appeared_numbers set
|
||||||
|
├── For num 1..49: find last appeared period count (omission count)
|
||||||
|
├── Return: [{num, omit_count, color}, ...] sorted by omit_count DESC
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
[JS: Render HTML grid]
|
||||||
|
│
|
||||||
|
├── For each result: <span class="num-ball"> with background-color from getColorByNum()
|
||||||
|
└── Show: number + 遗漏X期 label
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended Project Structure
|
||||||
|
|
||||||
|
No new directories needed. Changes are localized to existing files:
|
||||||
|
|
||||||
|
```
|
||||||
|
application/admin/
|
||||||
|
├── controller/History.php # ADD: missingNum() method + $noNeedRight
|
||||||
|
├── model/History.php # ADD: getMissingNumbers($periods) method
|
||||||
|
├── lang/zh-cn/history.php # ADD: i18n strings for missing numbers
|
||||||
|
|
||||||
|
public/assets/js/
|
||||||
|
└── backend/history.js # ADD: button handler + dialog + render logic
|
||||||
|
|
||||||
|
application/admin/view/
|
||||||
|
└── history/index.html # ADD: "遗漏号码" button to toolbar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 1: Custom Controller Method with AJAX Response
|
||||||
|
|
||||||
|
**What:** Add a new public method to a Backend controller that returns JSON via `$this->success()`.
|
||||||
|
|
||||||
|
**When to use:** Any admin AJAX endpoint that doesn't fit standard CRUD.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```php
|
||||||
|
// application/admin/controller/History.php
|
||||||
|
class History extends Backend
|
||||||
|
{
|
||||||
|
// 无需登录即可访问(但仍在 admin 模块内,受 admin auth 保护)
|
||||||
|
protected $noNeedRight = ['*'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询遗漏号码
|
||||||
|
*/
|
||||||
|
public function missingNum()
|
||||||
|
{
|
||||||
|
if ($this->request->isAjax()) {
|
||||||
|
$periods = $this->request->get('periods', 10, 'intval');
|
||||||
|
if ($periods < 1 || $periods > 100) {
|
||||||
|
$this->error('期数范围必须在 1-100 之间');
|
||||||
|
}
|
||||||
|
$result = $this->model->getMissingNumbers($periods);
|
||||||
|
$this->success('查询成功', $result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Source: [VERIFIED: application/admin/controller/Ajax.php — standard $this->success()/$this->error() pattern]
|
||||||
|
|
||||||
|
### Pattern 2: Layer Dialog with Inline HTML
|
||||||
|
|
||||||
|
**What:** Use `Layer.open()` with `type: 1` to display a modal with custom HTML content.
|
||||||
|
|
||||||
|
**When to use:** When the dialog doesn't need a separate page/view file and contains custom layout.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```javascript
|
||||||
|
// public/assets/js/backend/history.js
|
||||||
|
$('#toolbar').on('click', '.btn-missingnum', function () {
|
||||||
|
var html = '<div style="padding:20px;">' +
|
||||||
|
'<div class="form-group">' +
|
||||||
|
' <label>查询期数:</label>' +
|
||||||
|
' <input type="number" id="missing-periods" class="form-control" value="10" min="1" max="100">' +
|
||||||
|
'</div>' +
|
||||||
|
'<button class="btn btn-primary" id="btn-missing-query">查询</button>' +
|
||||||
|
'<div id="missing-result" style="margin-top:15px;"></div>' +
|
||||||
|
'</div>';
|
||||||
|
Layer.open({
|
||||||
|
type: 1,
|
||||||
|
title: '遗漏号码分析',
|
||||||
|
area: ['600px', '500px'],
|
||||||
|
content: html,
|
||||||
|
shadeClose: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
Source: [VERIFIED: application/admin/controller/Ajax.php pattern + Layer 3.5.6 API]
|
||||||
|
|
||||||
|
### Anti-Patterns to Avoid
|
||||||
|
|
||||||
|
- **Don't create a new view file** — The phase requirement explicitly says "按钮+弹窗形式,不新增独立页面". Use `Layer.open({type: 1, content: html})` with inline HTML.
|
||||||
|
- **Don't use Bootstrap Table for results** — Overkill for a simple grid of 49 numbers. Use flex-wrapped div grid.
|
||||||
|
- **Don't calculate in frontend** — Already decided: backend calculation only. Frontend AJAX calls the endpoint.
|
||||||
|
- **Don't use `Fast.api.open()`** — That opens an iframe-based dialog pointing to a URL. We want a self-contained dialog with inline content, so use `Layer.open({type: 1})` directly.
|
||||||
|
|
||||||
|
## Don't Hand-Roll
|
||||||
|
|
||||||
|
| Problem | Don't Build | Use Instead | Why |
|
||||||
|
|---------|-------------|-------------|-----|
|
||||||
|
| 遗漏计算 | Manual array scanning without SQL | SQL ORDER BY + PHP array intersection | SQL does the sorting efficiently; PHP handles the set difference |
|
||||||
|
| 波色球着色 | Hard-coded color mapping | Reuse existing `getColorByNum()` in history.js | Already handles 红/蓝/绿→CSS color mapping |
|
||||||
|
| AJAX 请求 | Raw $.ajax with manual error handling | `Fast.api.ajax()` or standard $.ajax with FastAdmin response format | FastAdmin's response envelope `{code, msg, data}` is standard |
|
||||||
|
| 弹窗对话框 | Custom modal HTML/CSS | Layer.open() | Layer is the project's standard dialog system |
|
||||||
|
| i18n 文本 | Hard-coded Chinese strings | Use `__('key')` + lang file | Project uses `__()` function for translation |
|
||||||
|
|
||||||
|
**Key insight:** FastAdmin already provides every building block needed — Layer for dialogs, `Fast.api.ajax` for requests, `getColorByNum()` for rendering. The only new code is the missing number algorithm and the button handler.
|
||||||
|
|
||||||
|
## Runtime State Inventory
|
||||||
|
|
||||||
|
> This is a greenfield feature within an existing project — no rename/refactor/migration involved.
|
||||||
|
|
||||||
|
**N/A** — No existing state needs updating. This is a new feature addition.
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### Pitfall 1: Permission Block on Custom Controller Method
|
||||||
|
**What goes wrong:** Adding a new public method to a Backend controller but forgetting `$noNeedRight`, causing 403 errors.
|
||||||
|
**Why it happens:** FastAdmin's `Backend` trait auto-checks permissions via `Auth::check($path)` in `_initialize()`. Any method not in the auth rule table or `$noNeedRight` array will be blocked.
|
||||||
|
**How to avoid:** Set `protected $noNeedRight = ['missingNum']` or `protected $noNeedRight = ['*']` in the controller.
|
||||||
|
**Warning signs:** AJAX returns HTML login page or 403 error instead of JSON.
|
||||||
|
|
||||||
|
### Pitfall 2: Color Map Not Loaded Before Rendering
|
||||||
|
**What goes wrong:** Rendering colored balls before `loadColorMap()` completes, resulting in gray balls.
|
||||||
|
**Why it happens:** `Controller.api.loadColorMap()` is async — the callback fires after the AJAX succeeds.
|
||||||
|
**How to avoid:** The dialog's query handler should check `Controller.api.colorMapLoaded` and call `loadColorMap()` first if not ready.
|
||||||
|
**Warning signs:** Balls render with `#95a5a6` (default gray) instead of correct colors.
|
||||||
|
|
||||||
|
### Pitfall 3: Missing Number Calculation — Only Counting Appear/Not-Appear
|
||||||
|
**What goes wrong:** Returning numbers that never appeared in X periods, but not calculating how many periods each number has been missing.
|
||||||
|
**Why it happens:** Confusing "not appeared in X periods" with "omission count" (遗漏期数). A number might have appeared in period N-50 but not in the last 10 — its omission count should be 50, not 10.
|
||||||
|
**How to avoid:** Query more than X periods (e.g., last 200 periods or all records) to calculate true omission counts, then filter/sort by omission. The omission count = total periods since last appearance.
|
||||||
|
**Warning signs:** All missing numbers show the same omission count as the query period.
|
||||||
|
|
||||||
|
### Pitfall 4: Number Format Mismatch (String vs Int)
|
||||||
|
**What goes wrong:** Database `num1`~`num7` are strings, but color lookup uses integer keys.
|
||||||
|
**Why it happens:** ThinkPHP returns all DB values as strings by default. The `colorMap` from `num/getColorMap` uses string keys like `{"1": "红波"}`.
|
||||||
|
**How to avoid:** Always `parseInt()` the number before color lookup. The existing `getColorByNum()` already does this correctly.
|
||||||
|
**Warning signs:** `undefined` returned from `colorMap[num]`.
|
||||||
|
|
||||||
|
## Code Examples
|
||||||
|
|
||||||
|
### Backend: Missing Number Calculation (PHP)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// application/admin/model/History.php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算遗漏号码
|
||||||
|
* @param int $periods 查询最近多少期
|
||||||
|
* @return array [{num: 1, omit: 50, color: '红波'}, ...]
|
||||||
|
*/
|
||||||
|
public function getMissingNumbers($periods = 10)
|
||||||
|
{
|
||||||
|
// 查询最近 $periods 期开奖数据
|
||||||
|
$history = Db::name('history')
|
||||||
|
->field('num1,num2,num3,num4,num5,num6,num7')
|
||||||
|
->order('openTime', 'desc')
|
||||||
|
->limit($periods)
|
||||||
|
->select();
|
||||||
|
|
||||||
|
// 收集最近 $periods 期出现过的号码
|
||||||
|
$appeared = [];
|
||||||
|
foreach ($history as $row) {
|
||||||
|
for ($i = 1; $i <= 7; $i++) {
|
||||||
|
if ($row['num' . $i] !== null && $row['num' . $i] !== '') {
|
||||||
|
$appeared[(int)$row['num' . $i]] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取遗漏号码(1-49中未出现的)
|
||||||
|
$missing = [];
|
||||||
|
for ($num = 1; $num <= 49; $num++) {
|
||||||
|
if (!isset($appeared[$num])) {
|
||||||
|
$missing[] = $num;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取波色映射
|
||||||
|
$colorMap = Db::name('num')->column('color', 'num');
|
||||||
|
|
||||||
|
// 计算遗漏期数(需要查询更多历史数据)
|
||||||
|
$allHistory = Db::name('history')
|
||||||
|
->field('num1,num2,num3,num4,num5,num6,num7')
|
||||||
|
->order('openTime', 'desc')
|
||||||
|
->limit(500) // 最多查500期
|
||||||
|
->select();
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($missing as $num) {
|
||||||
|
$omitCount = $this->calcOmitCount($num, $allHistory);
|
||||||
|
$result[] = [
|
||||||
|
'num' => $num,
|
||||||
|
'omit' => $omitCount,
|
||||||
|
'color' => $colorMap[$num] ?? '—'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按遗漏期数降序排序
|
||||||
|
usort($result, function ($a, $b) {
|
||||||
|
return $b['omit'] - $a['omit'];
|
||||||
|
});
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算某个号码的遗漏期数
|
||||||
|
*/
|
||||||
|
private function calcOmitCount($num, $allHistory)
|
||||||
|
{
|
||||||
|
foreach ($allHistory as $idx => $row) {
|
||||||
|
for ($i = 1; $i <= 7; $i++) {
|
||||||
|
if ((int)$row['num' . $i] === $num) {
|
||||||
|
return $idx; // 当前索引即为遗漏期数
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count($allHistory); // 如果500期内都没出现,返回500+
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend: Button Handler + Dialog + Render
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// public/assets/js/backend/history.js — inside Controller.index()
|
||||||
|
|
||||||
|
// 添加遗漏号码按钮到 toolbar
|
||||||
|
$('#toolbar').append('<a href="javascript:;" class="btn btn-warning btn-missingnum"><i class="fa fa-search"></i> 遗漏号码</a>');
|
||||||
|
|
||||||
|
// 按钮点击事件
|
||||||
|
$(document).on('click', '.btn-missingnum', function () {
|
||||||
|
Controller.api.showMissingNumDialog();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// public/assets/js/backend/history.js — inside Controller.api
|
||||||
|
|
||||||
|
showMissingNumDialog: function () {
|
||||||
|
var html = '<div style="padding:20px;">' +
|
||||||
|
'<div class="form-group">' +
|
||||||
|
' <label>查询最近期数:</label>' +
|
||||||
|
' <input type="number" id="missing-periods" class="form-control" value="10" min="1" max="100" style="width:120px;display:inline-block;">' +
|
||||||
|
' <button class="btn btn-primary" id="btn-missing-query" style="margin-left:10px;"><i class="fa fa-search"></i> 查询</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div id="missing-result" style="margin-top:15px;"></div>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
Layer.open({
|
||||||
|
type: 1,
|
||||||
|
title: __('Missing Number Analysis'),
|
||||||
|
area: ['650px', '550px'],
|
||||||
|
content: html,
|
||||||
|
shadeClose: true,
|
||||||
|
success: function (layero, index) {
|
||||||
|
// 绑定查询按钮
|
||||||
|
$('#btn-missing-query', layero).on('click', function () {
|
||||||
|
var periods = parseInt($('#missing-periods', layero).val()) || 10;
|
||||||
|
Controller.api.queryMissingNum(periods, layero);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
queryMissingNum: function (periods, layero) {
|
||||||
|
$('#missing-result', layero).html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> 查询中...</div>');
|
||||||
|
$.ajax({
|
||||||
|
url: 'history/missingNum',
|
||||||
|
type: 'GET',
|
||||||
|
data: { periods: periods },
|
||||||
|
dataType: 'json',
|
||||||
|
success: function (ret) {
|
||||||
|
if (ret.code == 1) {
|
||||||
|
Controller.api.renderMissingNum(ret.data, layero);
|
||||||
|
} else {
|
||||||
|
$('#missing-result', layero).html('<div class="text-danger">' + ret.msg + '</div>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
$('#missing-result', layero).html('<div class="text-danger">请求失败</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderMissingNum: function (data, layero) {
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
$('#missing-result', layero).html('<div class="alert alert-info">最近 ' + periods + ' 期内所有号码均出现过</div>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var html = '<div style="display:flex;flex-wrap:wrap;gap:10px;">';
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
var color = Controller.api.getColorByNum(data[i].num);
|
||||||
|
html += '<div style="text-align:center;">' +
|
||||||
|
'<span class="num-ball" style="display:inline-block;width:48px;height:48px;line-height:48px;text-align:center;border-radius:50%;color:#fff;background-color:' + color + ';font-weight:bold;font-size:18px;">' + data[i].num + '</span>' +
|
||||||
|
'<div style="margin-top:5px;font-size:12px;color:#666;">遗漏 ' + data[i].omit + ' 期</div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
$('#missing-result', layero).html(html);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### i18n Language Strings
|
||||||
|
|
||||||
|
```php
|
||||||
|
// application/admin/lang/zh-cn/history.php
|
||||||
|
return [
|
||||||
|
'Expect' => '期号',
|
||||||
|
'OpenTime' => '时间',
|
||||||
|
'Num7' => '特码',
|
||||||
|
'Missing Number Analysis' => '遗漏号码分析',
|
||||||
|
'Query Periods' => '查询期数',
|
||||||
|
'Missing' => '遗漏',
|
||||||
|
'periods' => '期',
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## State of the Art
|
||||||
|
|
||||||
|
| Old Approach | Current Approach | Impact |
|
||||||
|
|--------------|------------------|--------|
|
||||||
|
| Custom modal HTML/CSS/JS | Layer.open({type: 1}) with inline content | Leverages existing dialog system |
|
||||||
|
| Raw $.ajax with manual response parsing | FastAdmin standard {code, msg, data} envelope | Consistent error handling |
|
||||||
|
| Hard-coded color map in JS | Reuse existing `getColorByNum()` from history.js | No duplication, single source of truth |
|
||||||
|
| Bootstrap Table for display | Flex-wrapped grid with inline-styled balls | Simpler, more appropriate for ball display |
|
||||||
|
|
||||||
|
**Outdated/avoided:**
|
||||||
|
- `Layer.prompt()`: Only supports a single text input. We need input + button + results area, so use `Layer.open({type: 1})` with custom HTML.
|
||||||
|
- `Fast.api.open()`: Opens iframe-based dialogs. Overkill for this use case since we don't need a separate view file.
|
||||||
|
|
||||||
|
## Assumptions Log
|
||||||
|
|
||||||
|
| # | Claim | Section | Risk if Wrong |
|
||||||
|
|---|-------|---------|---------------|
|
||||||
|
| A1 | fa_history 表字段为 `num1`~`num7`,类型为字符串 | Code Examples | 号码类型不匹配导致比较失败 |
|
||||||
|
| A2 | fa_history 按 `openTime` 降序排列可获取"最近 N 期" | Code Examples | 如果 openTime 不是开奖时间字段,排序会错 |
|
||||||
|
| A3 | fa_num 表包含 1-49 的所有波色映射 | Code Examples | 波色显示会缺失 |
|
||||||
|
| A4 | FastAdmin admin 模块下自定义方法无需额外路由注册 | Architecture | URL 无法访问到方法 |
|
||||||
|
| A5 | `protected $noNeedRight = ['*']` 可跳过权限检查 | Pitfall 1 | AJAX 返回 403 |
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. **遗漏期数的计算基准**:OMIT-03 要求的"遗漏期数"是指"该号码最后一次出现距今多少期",还是"该号码在最近 X 期中没出现的期数"?当前方案采用前者(全局遗漏),这是彩票分析的标准定义。
|
||||||
|
- Recommendation: 使用全局遗漏期数(最后一次出现距今多少期),这是行业标准。
|
||||||
|
|
||||||
|
2. **fa_num 表的波色数据完整性**:是否确保 1-49 每个数字都有波色记录?
|
||||||
|
- What we know: `fa_num` 表有 `num` 和 `color` 字段,由 Num 控制器维护
|
||||||
|
- Recommendation: 前端对缺失波色的号码显示灰色兜底(`#95a5a6`),已有此逻辑。
|
||||||
|
|
||||||
|
## Environment Availability
|
||||||
|
|
||||||
|
| Dependency | Required By | Available | Version | Fallback |
|
||||||
|
|------------|------------|-----------|---------|----------|
|
||||||
|
| PHP | Backend endpoint | ✓ | >= 7.4.0 | — |
|
||||||
|
| MySQL (fa_history, fa_num) | Data query | ✓ | — | — |
|
||||||
|
| jQuery 3.7.1 | AJAX + DOM | ✓ | 3.7.1 | — |
|
||||||
|
| Layer 3.5.6 | Dialog | ✓ | 3.5.6 (npm: fastadmin-layer) | — |
|
||||||
|
| FastAdmin Backend trait | Controller base | ✓ | 1.6.2.20260323 | — |
|
||||||
|
|
||||||
|
All dependencies are available — no missing tools.
|
||||||
|
|
||||||
|
## Validation Architecture
|
||||||
|
|
||||||
|
> **SKIPPED** — `workflow.nyquist_validation` is not configured in `.planning/config.json`, but no test infrastructure exists in the project (no PHPUnit, no `tests/` directory). This is a code-only admin feature with no automated tests. Manual testing via browser will be required.
|
||||||
|
|
||||||
|
## Security Domain
|
||||||
|
|
||||||
|
### Applicable ASVS Categories
|
||||||
|
|
||||||
|
| ASVS Category | Applies | Standard Control |
|
||||||
|
|---------------|---------|-----------------|
|
||||||
|
| V2 Authentication | yes | FastAdmin admin session auth (inherited from Backend base) |
|
||||||
|
| V5 Input Validation | yes | PHP `intval()` for periods parameter, range check 1-100 |
|
||||||
|
| V7 Error Handling | yes | `$this->error()` for invalid input, JSON response |
|
||||||
|
|
||||||
|
### Known Threat Patterns
|
||||||
|
|
||||||
|
| Pattern | STRIDE | Standard Mitigation |
|
||||||
|
|---------|--------|---------------------|
|
||||||
|
| SQL Injection | Tampering | ThinkPHP ORM `Db::name()` with parameterized queries — no raw SQL concatenation |
|
||||||
|
| Parameter Tampering | Tampering | Input validation: `intval()`, range check `$periods < 1 || $periods > 100` |
|
||||||
|
| Unauthorized Access | Elevation of privilege | `$noNeedRight` (not `$noNeedLogin`) — admin login still required, just skip permission rule check |
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
### Primary (HIGH confidence)
|
||||||
|
- [VERIFIED: Codebase] `application/admin/controller/History.php` — existing controller structure
|
||||||
|
- [VERIFIED: Codebase] `application/admin/model/History.php` — existing model with `$name = 'history'`
|
||||||
|
- [VERIFIED: Codebase] `public/assets/js/backend/history.js` — existing JS with `loadColorMap()`, `getColorByNum()`, `numBall` formatter
|
||||||
|
- [VERIFIED: Codebase] `application/admin/view/history/index.html` — existing toolbar structure
|
||||||
|
- [VERIFIED: Codebase] `application/admin/controller/Ajax.php` — standard `$this->success()/$this->error()` AJAX response pattern
|
||||||
|
- [VERIFIED: Codebase] `application/common/controller/Backend.php` — `$noNeedLogin`, `$noNeedRight` properties
|
||||||
|
- [VERIFIED: Codebase] `public/assets/js/fast.js` — `Fast.api.ajax()`, `Fast.api.open()`, `Layer` integration
|
||||||
|
- [VERIFIED: Codebase] `public/assets/js/backend/command.js` — `Layer.alert()` with custom content pattern
|
||||||
|
- [VERIFIED: Codebase] `public/assets/js/backend/general/config.js` — `Layer.prompt()` usage pattern
|
||||||
|
- [VERIFIED: npm registry] `fastadmin-layer@3.5.6`, `fastadmin-bootstraptable@1.11.12`
|
||||||
|
- [VERIFIED: Codebase] `.planning/codebase/ARCHITECTURE.md` — controller inheritance chain, RBAC auth
|
||||||
|
|
||||||
|
### Secondary (MEDIUM confidence)
|
||||||
|
- [VERIFIED: Codebase] `application/admin/controller/Num.php` — `getColorMap()` response format
|
||||||
|
- [VERIFIED: Codebase] `.planning/codebase/STACK.md` — PHP >= 7.4, ThinkPHP 5.x dev-master
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
**Confidence breakdown:**
|
||||||
|
- Standard stack: HIGH — verified against installed npm packages and composer.json
|
||||||
|
- Architecture: HIGH — verified against existing codebase patterns
|
||||||
|
- Pitfalls: HIGH — derived from actual FastAdmin source code analysis
|
||||||
|
|
||||||
|
**Research date:** 2026-04-21
|
||||||
|
**Valid until:** 2026-07-21 (90 days — stable codebase, no fast-moving dependencies)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<html>
|
||||||
|
<style>
|
||||||
|
.btlink {
|
||||||
|
color: #20a53a;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<html>
|
||||||
|
<head><title>404 Not Found</title></head>
|
||||||
|
<body>
|
||||||
|
<center><h1>404 Not Found</h1></center>
|
||||||
|
<hr>
|
||||||
|
<div style="text-align: center;font-size: 15px" >Power by <a class="btlink" href="https://www.bt.cn/?from=404" target="_blank">堡塔 (免费,高效和安全的托管控制面板)</a></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**FastAdmin 1.6.x** admin panel built on **ThinkPHP 5.x** (forked from gitee.com/fastadminnet/framework.git). The project is a Macau lottery (澳门六合彩) data tracking application named "amlhc".
|
||||||
|
|
||||||
|
- PHP >= 7.4.0, Apache-2.0 license
|
||||||
|
- Database: MySQL with `fa_` table prefix, utf8mb4 charset
|
||||||
|
- Frontend: RequireJS + Bootstrap 3.4 + AdminLTE skin
|
||||||
|
|
||||||
|
## Key Directories
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `application/admin/` | Admin backend — controllers, models, views, validates, lang |
|
||||||
|
| `application/index/` | Public-facing frontend |
|
||||||
|
| `application/api/` | REST API endpoints |
|
||||||
|
| `application/common/` | Shared base controllers, models, libraries, global helpers |
|
||||||
|
| `application/extra/` | Extra config: site.php, upload.php, queue.php, addons.php |
|
||||||
|
| `public/` | Web root — entry point `index.php`, static assets in `assets/` |
|
||||||
|
| `addons/` | Plugin directory (currently `command` addon installed) |
|
||||||
|
| `sql/` | SQL migration/DDL files |
|
||||||
|
| `thinkphp/` | Vendored ThinkPHP 5.x framework |
|
||||||
|
|
||||||
|
## Config Loading Chain
|
||||||
|
|
||||||
|
All files under `application/` are merged by ThinkPHP:
|
||||||
|
1. `config.php` — main config (debug, modules, URL, template, session, cookie, FastAdmin settings)
|
||||||
|
2. `database.php` — DB connection (reads from `.env`)
|
||||||
|
3. `extra/*.php` — site, upload, queue, addons config
|
||||||
|
4. `route.php` — URL routing (currently empty)
|
||||||
|
|
||||||
|
Environment variables are loaded from `.env` via `think\Env`.
|
||||||
|
|
||||||
|
## Admin Module Architecture
|
||||||
|
|
||||||
|
All admin controllers extend `app\common\controller\Backend`, which provides:
|
||||||
|
- RBAC auth via `app\admin\library\Auth` (salt double-MD5, session + cookie)
|
||||||
|
- CRUD base operations (index/add/edit/del/multi) via `app\admin\library\traits\Backend`
|
||||||
|
- Search/filtering, SelectPage, import/export
|
||||||
|
- Layout templating via `application/admin/view/layout/default.html`
|
||||||
|
|
||||||
|
**Standard controller pattern:** Each admin controller maps to a model + view directory + validate + lang file:
|
||||||
|
```
|
||||||
|
controller/X.php → model/X.php → view/x/ → validate/X.php → lang/zh-cn/x.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Domain Code (Lottery)
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `application/admin/controller/History.php` | Lottery history admin |
|
||||||
|
| `application/admin/model/History.php` | `fa_history` model |
|
||||||
|
| `application/admin/controller/Num.php` | `fa_num` color mapping API (`getColorMap`) |
|
||||||
|
| `application/admin/model/Num.php` | `fa_num` model |
|
||||||
|
| `application/index/controller/Index.php` | Scrapes lottery data from external API |
|
||||||
|
|
||||||
|
## Frontend Build System
|
||||||
|
|
||||||
|
**Grunt** (`Gruntfile.js`) is the build tool:
|
||||||
|
- `grunt deploy` — copy npm packages to `public/assets/libs/`
|
||||||
|
- `grunt backend:js` / `grunt frontend:js` — RequireJS optimizer for JS
|
||||||
|
- `grunt backend:css` / `grunt frontend:css` — Less → CSS compilation
|
||||||
|
|
||||||
|
JS uses RequireJS with separate entry points: `require-backend.js` and `require-frontend.js`.
|
||||||
|
|
||||||
|
## Common Development Commands
|
||||||
|
|
||||||
|
### Install dependencies
|
||||||
|
```bash
|
||||||
|
composer install
|
||||||
|
npm install
|
||||||
|
grunt deploy # copy npm deps to public/assets/libs/
|
||||||
|
grunt # build all JS/CSS
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run dev server
|
||||||
|
```bash
|
||||||
|
php -S 127.0.0.1:8000 -t public public/router.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generate CRUD scaffolding (built-in)
|
||||||
|
```bash
|
||||||
|
php think crud --table=fa_xxx --controller=xxx --model=xxx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generate menu for admin controller
|
||||||
|
```bash
|
||||||
|
php think menu --controller=xxx/xxx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run tests
|
||||||
|
```bash
|
||||||
|
php think unit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- Admin entry is `public/index.php` (the admin module is accessed via URL routing, not a separate `admin.php`)
|
||||||
|
- All database queries use ThinkPHP's query builder (`Db::name('table')`) or models
|
||||||
|
- Language files use key-value pairs; keys are English labels (e.g., `'Num1' => '号码1'`)
|
||||||
|
- The `.env` file contains actual credentials — never commit it
|
||||||
|
- Runtime cache lives in `runtime/` — clear it when config changes don't take effect
|
||||||
|
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
module.exports = function (grunt) {
|
||||||
|
|
||||||
|
grunt.initConfig({
|
||||||
|
pkg: grunt.file.readJSON('package.json'),
|
||||||
|
copy: {
|
||||||
|
main: {
|
||||||
|
files: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var build = function (module, type, callback) {
|
||||||
|
var config = {
|
||||||
|
compile: {
|
||||||
|
options: type === 'js' ? {
|
||||||
|
optimizeCss: "standard",
|
||||||
|
optimize: "uglify", //可使用uglify|closure|none
|
||||||
|
preserveLicenseComments: true,
|
||||||
|
removeCombined: false,
|
||||||
|
baseUrl: "./public/assets/js/", //JS文件所在的基础目录
|
||||||
|
name: "require-" + module, //来源文件,不包含后缀
|
||||||
|
out: "./public/assets/js/require-" + module + ".min.js" //目标文件
|
||||||
|
} : {
|
||||||
|
optimizeCss: "default",
|
||||||
|
optimize: "uglify", //可使用uglify|closure|none
|
||||||
|
cssIn: "./public/assets/css/" + module + ".css", //CSS文件所在的基础目录
|
||||||
|
out: "./public/assets/css/" + module + ".min.css" //目标文件
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var content = grunt.file.read("./public/assets/js/require-" + module + ".js"),
|
||||||
|
pattern = /^require\.config\(\{[\r\n]?[\n]?(.*?)[\r\n]?[\n]?}\);/is;
|
||||||
|
|
||||||
|
var matches = content.match(pattern);
|
||||||
|
if (matches) {
|
||||||
|
if (type === 'js') {
|
||||||
|
var data = matches[1].replaceAll(/(urlArgs|baseUrl):(.*)\n/gi, '');
|
||||||
|
const parse = require('parse-config-file'), fs = require('fs');
|
||||||
|
require('jsonminify');
|
||||||
|
|
||||||
|
data = JSON.minify("{\n" + data + "\n}");
|
||||||
|
let options = parse(data);
|
||||||
|
options.paths.tableexport = "empty:";
|
||||||
|
Object.assign(config.compile.options, options);
|
||||||
|
}
|
||||||
|
let requirejs = require("./application/admin/command/Min/r");
|
||||||
|
|
||||||
|
try {
|
||||||
|
requirejs.optimize(config.compile.options, function (buildResponse) {
|
||||||
|
// var contents = require('fs').readFileSync(config.compile.options.out, 'utf8');
|
||||||
|
callback();
|
||||||
|
}, function (err) {
|
||||||
|
console.error(err);
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载 "copy" 插件
|
||||||
|
grunt.loadNpmTasks('grunt-contrib-copy');
|
||||||
|
|
||||||
|
grunt.registerTask('frontend:js', 'build frontend js', function () {
|
||||||
|
var done = this.async();
|
||||||
|
build('frontend', 'js', done);
|
||||||
|
});
|
||||||
|
|
||||||
|
grunt.registerTask('backend:js', 'build backend js', function () {
|
||||||
|
var done = this.async();
|
||||||
|
build('backend', 'js', done);
|
||||||
|
});
|
||||||
|
|
||||||
|
grunt.registerTask('frontend:css', 'build frontend css', function () {
|
||||||
|
var done = this.async();
|
||||||
|
build('frontend', 'css', done);
|
||||||
|
});
|
||||||
|
|
||||||
|
grunt.registerTask('backend:css', 'build frontend css', function () {
|
||||||
|
var done = this.async();
|
||||||
|
build('backend', 'css', done);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册部署JS和CSS任务
|
||||||
|
grunt.registerTask('deploy', 'deploy', function () {
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require("path")
|
||||||
|
const nodeModulesDir = path.resolve(__dirname, "./node_modules");
|
||||||
|
|
||||||
|
const getAllFiles = function (dirPath, arrayOfFiles) {
|
||||||
|
files = fs.readdirSync(dirPath)
|
||||||
|
|
||||||
|
arrayOfFiles = arrayOfFiles || []
|
||||||
|
|
||||||
|
files.forEach(function (file) {
|
||||||
|
if (fs.statSync(dirPath + "/" + file).isDirectory()) {
|
||||||
|
arrayOfFiles = getAllFiles(dirPath + "/" + file, arrayOfFiles)
|
||||||
|
} else {
|
||||||
|
arrayOfFiles.push(path.join(__dirname, dirPath, "/", file))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return arrayOfFiles
|
||||||
|
};
|
||||||
|
const mainPackage = grunt.config.get('pkg');
|
||||||
|
let dists = mainPackage.dists || [];
|
||||||
|
let files = [];
|
||||||
|
|
||||||
|
// 兼容旧版本bower使用的目录
|
||||||
|
let specialKey = {
|
||||||
|
'fastadmin-bootstraptable': 'bootstrap-table',
|
||||||
|
'sortablejs': 'Sortable',
|
||||||
|
'tableexport.jquery.plugin': 'tableExport.jquery.plugin',
|
||||||
|
};
|
||||||
|
Object.keys(dists).forEach(key => {
|
||||||
|
let src = ["**/*LICENSE*", "**/*license*"];
|
||||||
|
src = src.concat(Array.isArray(dists[key]) ? dists[key] : [dists[key]]);
|
||||||
|
files.push({expand: true, cwd: nodeModulesDir + "/" + key, src: src, dest: 'public/assets/libs/' + (specialKey[key] || key) + "/"});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 兼容bower历史路径文件
|
||||||
|
files = [...files,
|
||||||
|
{src: nodeModulesDir + "/toastr/build/toastr.min.css", dest: "public/assets/libs/toastr/toastr.min.css"},
|
||||||
|
{src: nodeModulesDir + "/bootstrap-slider/dist/css/bootstrap-slider.css", dest: "public/assets/libs/bootstrap-slider/slider.css"},
|
||||||
|
{expand: true, cwd: nodeModulesDir + "/bootstrap-slider/dist", src: ["*.js"], dest: "public/assets/libs/bootstrap-slider/"}
|
||||||
|
]
|
||||||
|
|
||||||
|
grunt.config.set('copy.main.files', files);
|
||||||
|
grunt.task.run("copy:main");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册默认任务
|
||||||
|
grunt.registerTask('default', ['deploy', 'frontend:js', 'backend:js', 'frontend:css', 'backend:css']);
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction, and
|
||||||
|
distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by the copyright
|
||||||
|
owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all other entities
|
||||||
|
that control, are controlled by, or are under common control with that entity.
|
||||||
|
For the purposes of this definition, "control" means (i) the power, direct or
|
||||||
|
indirect, to cause the direction or management of such entity, whether by
|
||||||
|
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity exercising
|
||||||
|
permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications, including
|
||||||
|
but not limited to software source code, documentation source, and configuration
|
||||||
|
files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical transformation or
|
||||||
|
translation of a Source form, including but not limited to compiled object code,
|
||||||
|
generated documentation, and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or Object form, made
|
||||||
|
available under the License, as indicated by a copyright notice that is included
|
||||||
|
in or attached to the work (an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object form, that
|
||||||
|
is based on (or derived from) the Work and for which the editorial revisions,
|
||||||
|
annotations, elaborations, or other modifications represent, as a whole, an
|
||||||
|
original work of authorship. For the purposes of this License, Derivative Works
|
||||||
|
shall not include works that remain separable from, or merely link (or bind by
|
||||||
|
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including the original version
|
||||||
|
of the Work and any modifications or additions to that Work or Derivative Works
|
||||||
|
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||||
|
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||||
|
on behalf of the copyright owner. For the purposes of this definition,
|
||||||
|
"submitted" means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems, and
|
||||||
|
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||||
|
the purpose of discussing and improving the Work, but excluding communication
|
||||||
|
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||||
|
owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
|
||||||
|
of whom a Contribution has been received by Licensor and subsequently
|
||||||
|
incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License.
|
||||||
|
|
||||||
|
Subject to the terms and conditions of this License, each Contributor hereby
|
||||||
|
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||||
|
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||||
|
Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License.
|
||||||
|
|
||||||
|
Subject to the terms and conditions of this License, each Contributor hereby
|
||||||
|
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||||
|
irrevocable (except as stated in this section) patent license to make, have
|
||||||
|
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||||
|
such license applies only to those patent claims licensable by such Contributor
|
||||||
|
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||||
|
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||||
|
submitted. If You institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||||
|
Contribution incorporated within the Work constitutes direct or contributory
|
||||||
|
patent infringement, then any patent licenses granted to You under this License
|
||||||
|
for that Work shall terminate as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||||
|
in any medium, with or without modifications, and in Source or Object form,
|
||||||
|
provided that You meet the following conditions:
|
||||||
|
|
||||||
|
You must give any other recipients of the Work or Derivative Works a copy of
|
||||||
|
this License; and
|
||||||
|
You must cause any modified files to carry prominent notices stating that You
|
||||||
|
changed the files; and
|
||||||
|
You must retain, in the Source form of any Derivative Works that You distribute,
|
||||||
|
all copyright, patent, trademark, and attribution notices from the Source form
|
||||||
|
of the Work, excluding those notices that do not pertain to any part of the
|
||||||
|
Derivative Works; and
|
||||||
|
If the Work includes a "NOTICE" text file as part of its distribution, then any
|
||||||
|
Derivative Works that You distribute must include a readable copy of the
|
||||||
|
attribution notices contained within such NOTICE file, excluding those notices
|
||||||
|
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||||
|
following places: within a NOTICE text file distributed as part of the
|
||||||
|
Derivative Works; within the Source form or documentation, if provided along
|
||||||
|
with the Derivative Works; or, within a display generated by the Derivative
|
||||||
|
Works, if and wherever such third-party notices normally appear. The contents of
|
||||||
|
the NOTICE file are for informational purposes only and do not modify the
|
||||||
|
License. You may add Your own attribution notices within Derivative Works that
|
||||||
|
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||||
|
provided that such additional attribution notices cannot be construed as
|
||||||
|
modifying the License.
|
||||||
|
You may add Your own copyright statement to Your modifications and may provide
|
||||||
|
additional or different license terms and conditions for use, reproduction, or
|
||||||
|
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||||
|
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||||
|
with the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions.
|
||||||
|
|
||||||
|
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||||
|
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||||
|
conditions of this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||||
|
any separate license agreement you may have executed with Licensor regarding
|
||||||
|
such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks.
|
||||||
|
|
||||||
|
This License does not grant permission to use the trade names, trademarks,
|
||||||
|
service marks, or product names of the Licensor, except as required for
|
||||||
|
reasonable and customary use in describing the origin of the Work and
|
||||||
|
reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||||
|
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||||
|
including, without limitation, any warranties or conditions of TITLE,
|
||||||
|
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||||
|
solely responsible for determining the appropriateness of using or
|
||||||
|
redistributing the Work and assume any risks associated with Your exercise of
|
||||||
|
permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability.
|
||||||
|
|
||||||
|
In no event and under no legal theory, whether in tort (including negligence),
|
||||||
|
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||||
|
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special, incidental,
|
||||||
|
or consequential damages of any character arising as a result of this License or
|
||||||
|
out of the use or inability to use the Work (including but not limited to
|
||||||
|
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||||
|
any and all other commercial damages or losses), even if such Contributor has
|
||||||
|
been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability.
|
||||||
|
|
||||||
|
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||||
|
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||||
|
other liability obligations and/or rights consistent with this License. However,
|
||||||
|
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||||
|
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||||
|
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason of your
|
||||||
|
accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following boilerplate
|
||||||
|
notice, with the fields enclosed by brackets "{}" replaced with your own
|
||||||
|
identifying information. (Don't include the brackets!) The text should be
|
||||||
|
enclosed in the appropriate comment syntax for the file format. We also
|
||||||
|
recommend that a file or class name and description of purpose be included on
|
||||||
|
the same "printed page" as the copyright notice for easier identification within
|
||||||
|
third-party archives.
|
||||||
|
|
||||||
|
Copyright 2017 Karson
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
FastAdmin是一款基于ThinkPHP+Bootstrap的极速后台开发框架。
|
||||||
|
|
||||||
|
|
||||||
|
## 主要特性
|
||||||
|
|
||||||
|
* 基于`Auth`验证的权限管理系统
|
||||||
|
* 支持无限级父子级权限继承,父级的管理员可任意增删改子级管理员及权限设置
|
||||||
|
* 支持单管理员多角色
|
||||||
|
* 支持管理子级数据或个人数据
|
||||||
|
* 强大的一键生成功能
|
||||||
|
* 一键生成CRUD,包括控制器、模型、视图、JS、语言包、菜单、回收站等
|
||||||
|
* 一键压缩打包JS和CSS文件,一键CDN静态资源部署
|
||||||
|
* 一键生成控制器菜单和规则
|
||||||
|
* 一键生成API接口文档
|
||||||
|
* 完善的前端功能组件开发
|
||||||
|
* 基于`AdminLTE`二次开发
|
||||||
|
* 基于`Bootstrap`开发,自适应手机、平板、PC
|
||||||
|
* 基于`RequireJS`进行JS模块管理,按需加载
|
||||||
|
* 基于`Less`进行样式开发
|
||||||
|
* 强大的插件扩展功能,在线安装卸载升级插件
|
||||||
|
* 通用的会员模块和API模块
|
||||||
|
* 共用同一账号体系的Web端会员中心权限验证和API接口会员权限验证
|
||||||
|
* 二级域名部署支持,同时域名支持绑定到应用插件
|
||||||
|
* 多语言支持,服务端及客户端支持
|
||||||
|
* 支持大文件分片上传、剪切板粘贴上传、拖拽上传,进度条显示,图片上传前压缩
|
||||||
|
* 支持表格固定列、固定表头、跨页选择、Excel导出、模板渲染等功能
|
||||||
|
* 强大的第三方应用模块支持([CMS](https://www.fastadmin.net/store/cms.html)、[CRM](https://www.fastadmin.net/store/facrm.html)、[企业网站管理系统](https://www.fastadmin.net/store/ldcms.html)、[知识库文档系统](https://www.fastadmin.net/store/knowbase.html)、[在线投票系统](https://www.fastadmin.net/store/vote.html)、[B2C商城](https://www.fastadmin.net/store/shopro.html)、[B2B2C商城](https://www.fastadmin.net/store/wanlshop.html))
|
||||||
|
* 整合第三方短信接口(阿里云、腾讯云短信)
|
||||||
|
* 无缝整合第三方云存储(七牛云、阿里云OSS、腾讯云存储、又拍云)功能,支持云储存分片上传
|
||||||
|
* 第三方富文本编辑器支持(Summernote、百度编辑器)
|
||||||
|
* 第三方登录(QQ、微信、微博)整合
|
||||||
|
* 第三方支付(微信、支付宝)无缝整合,微信支持PC端扫码支付
|
||||||
|
* 丰富的插件应用市场
|
||||||
|
|
||||||
|
## 安装使用
|
||||||
|
|
||||||
|
https://doc.fastadmin.net
|
||||||
|
|
||||||
|
## 在线演示
|
||||||
|
|
||||||
|
https://demo.fastadmin.net
|
||||||
|
|
||||||
|
用户名:admin
|
||||||
|
|
||||||
|
密 码:123456
|
||||||
|
|
||||||
|
提 示:演示站数据无法进行修改,请下载源码安装体验全部功能
|
||||||
|
|
||||||
|
## 界面截图
|
||||||
|

|
||||||
|
|
||||||
|
## 问题反馈
|
||||||
|
|
||||||
|
在使用中有任何问题,请使用以下联系方式联系我们
|
||||||
|
|
||||||
|
问答社区: https://ask.fastadmin.net
|
||||||
|
|
||||||
|
Github: https://github.com/fastadminnet/fastadmin
|
||||||
|
|
||||||
|
Gitee: https://gitee.com/fastadminnet/fastadmin
|
||||||
|
|
||||||
|
## 特别鸣谢
|
||||||
|
|
||||||
|
感谢以下的项目,排名不分先后
|
||||||
|
|
||||||
|
ThinkPHP:http://www.thinkphp.cn
|
||||||
|
|
||||||
|
AdminLTE:https://adminlte.io
|
||||||
|
|
||||||
|
Bootstrap:http://getbootstrap.com
|
||||||
|
|
||||||
|
jQuery:http://jquery.com
|
||||||
|
|
||||||
|
Bootstrap-table:https://github.com/wenzhixin/bootstrap-table
|
||||||
|
|
||||||
|
Nice-validator: https://validator.niceue.com
|
||||||
|
|
||||||
|
SelectPage: https://github.com/TerryZ/SelectPage
|
||||||
|
|
||||||
|
Layer: https://layuion.com/layer/
|
||||||
|
|
||||||
|
DropzoneJS: https://www.dropzonejs.com
|
||||||
|
|
||||||
|
|
||||||
|
## 版权信息
|
||||||
|
|
||||||
|
FastAdmin遵循Apache2开源协议发布,并提供免费使用。
|
||||||
|
|
||||||
|
本项目包含的第三方源码和二进制文件之版权信息另行标注。
|
||||||
|
|
||||||
|
版权所有Copyright © 2017-2024 by FastAdmin (https://www.fastadmin.net)
|
||||||
|
|
||||||
|
All rights reserved。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
deny from all
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\behavior;
|
||||||
|
|
||||||
|
class AdminLog
|
||||||
|
{
|
||||||
|
public function run(&$response)
|
||||||
|
{
|
||||||
|
//只记录POST请求的日志
|
||||||
|
if (request()->isPost() && config('fastadmin.auto_record_log')) {
|
||||||
|
\app\admin\model\AdminLog::record();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\command;
|
||||||
|
|
||||||
|
use think\addons\AddonException;
|
||||||
|
use think\addons\Service;
|
||||||
|
use think\Config;
|
||||||
|
use think\console\Command;
|
||||||
|
use think\console\Input;
|
||||||
|
use think\console\input\Option;
|
||||||
|
use think\console\Output;
|
||||||
|
use think\Db;
|
||||||
|
use think\Exception;
|
||||||
|
use think\exception\PDOException;
|
||||||
|
|
||||||
|
class Addon extends Command
|
||||||
|
{
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setName('addon')
|
||||||
|
->addOption('name', 'a', Option::VALUE_REQUIRED, 'addon name', null)
|
||||||
|
->addOption('action', 'c', Option::VALUE_REQUIRED, 'action(create/enable/disable/uninstall/refresh/package/move)', 'create')
|
||||||
|
->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force override', null)
|
||||||
|
->addOption('release', 'r', Option::VALUE_OPTIONAL, 'addon release version', null)
|
||||||
|
->addOption('uid', 'u', Option::VALUE_OPTIONAL, 'fastadmin uid', null)
|
||||||
|
->addOption('token', 't', Option::VALUE_OPTIONAL, 'fastadmin token', null)
|
||||||
|
->addOption('domain', 'd', Option::VALUE_OPTIONAL, 'domain', null)
|
||||||
|
->addOption('local', 'l', Option::VALUE_OPTIONAL, 'local package', null)
|
||||||
|
->setDescription('Addon manager');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(Input $input, Output $output)
|
||||||
|
{
|
||||||
|
\think\Config::load(dirname(dirname(__FILE__)) . DS . 'config.php');
|
||||||
|
$name = $input->getOption('name') ?: '';
|
||||||
|
$action = $input->getOption('action') ?: '';
|
||||||
|
if (stripos($name, 'addons' . DS) !== false) {
|
||||||
|
$name = explode(DS, $name)[1];
|
||||||
|
}
|
||||||
|
//强制覆盖
|
||||||
|
$force = $input->getOption('force');
|
||||||
|
//版本
|
||||||
|
$release = $input->getOption('release') ?: '';
|
||||||
|
//uid
|
||||||
|
$uid = $input->getOption('uid') ?: '';
|
||||||
|
//token
|
||||||
|
$token = $input->getOption('token') ?: '';
|
||||||
|
|
||||||
|
include dirname(__DIR__) . DS . 'common.php';
|
||||||
|
|
||||||
|
if (!$name && !in_array($action, ['refresh'])) {
|
||||||
|
throw new Exception('Addon name could not be empty');
|
||||||
|
}
|
||||||
|
if (!$action || !in_array($action, ['create', 'disable', 'enable', 'install', 'uninstall', 'refresh', 'upgrade', 'package', 'move'])) {
|
||||||
|
throw new Exception('Please input correct action name');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询一次SQL,判断连接是否正常
|
||||||
|
Db::execute("SELECT 1");
|
||||||
|
|
||||||
|
$addonDir = ADDON_PATH . $name . DS;
|
||||||
|
switch ($action) {
|
||||||
|
case 'create':
|
||||||
|
//非覆盖模式时如果存在则报错
|
||||||
|
if (is_dir($addonDir) && !$force) {
|
||||||
|
throw new Exception("addon already exists!\nIf you need to create again, use the parameter --force=true ");
|
||||||
|
}
|
||||||
|
//如果存在先移除
|
||||||
|
if (is_dir($addonDir)) {
|
||||||
|
rmdirs($addonDir);
|
||||||
|
}
|
||||||
|
mkdir($addonDir, 0755, true);
|
||||||
|
mkdir($addonDir . DS . 'controller', 0755, true);
|
||||||
|
$menuList = \app\common\library\Menu::export($name);
|
||||||
|
$createMenu = $this->getCreateMenu($menuList);
|
||||||
|
$prefix = Config::get('database.prefix');
|
||||||
|
$createTableSql = '';
|
||||||
|
try {
|
||||||
|
$result = Db::query("SHOW CREATE TABLE `" . $prefix . $name . "`;");
|
||||||
|
if (isset($result[0]) && isset($result[0]['Create Table'])) {
|
||||||
|
$createTableSql = $result[0]['Create Table'];
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'name' => $name,
|
||||||
|
'addon' => $name,
|
||||||
|
'addonClassName' => ucfirst($name),
|
||||||
|
'addonInstallMenu' => $createMenu ? "\$menu = " . var_export_short($createMenu) . ";\n\tMenu::create(\$menu);" : '',
|
||||||
|
'addonUninstallMenu' => $menuList ? 'Menu::delete("' . $name . '");' : '',
|
||||||
|
'addonEnableMenu' => $menuList ? 'Menu::enable("' . $name . '");' : '',
|
||||||
|
'addonDisableMenu' => $menuList ? 'Menu::disable("' . $name . '");' : '',
|
||||||
|
];
|
||||||
|
$this->writeToFile("addon", $data, $addonDir . ucfirst($name) . '.php');
|
||||||
|
$this->writeToFile("config", $data, $addonDir . 'config.php');
|
||||||
|
$this->writeToFile("info", $data, $addonDir . 'info.ini');
|
||||||
|
$this->writeToFile("controller", $data, $addonDir . 'controller' . DS . 'Index.php');
|
||||||
|
if ($createTableSql) {
|
||||||
|
$createTableSql = str_replace("`" . $prefix, '`__PREFIX__', $createTableSql);
|
||||||
|
file_put_contents($addonDir . 'install.sql', $createTableSql);
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->info("Create Successed!");
|
||||||
|
break;
|
||||||
|
case 'disable':
|
||||||
|
case 'enable':
|
||||||
|
try {
|
||||||
|
//调用启用、禁用的方法
|
||||||
|
Service::$action($name, 0);
|
||||||
|
} catch (AddonException $e) {
|
||||||
|
if ($e->getCode() != -3) {
|
||||||
|
throw new Exception($e->getMessage());
|
||||||
|
}
|
||||||
|
if (!$force) {
|
||||||
|
//如果有冲突文件则提醒
|
||||||
|
$data = $e->getData();
|
||||||
|
foreach ($data['conflictlist'] as $k => $v) {
|
||||||
|
$output->warning($v);
|
||||||
|
}
|
||||||
|
$output->info("Are you sure you want to " . ($action == 'enable' ? 'override' : 'delete') . " all those files? Type 'yes' to continue: ");
|
||||||
|
$line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r'));
|
||||||
|
if (trim($line) != 'yes') {
|
||||||
|
throw new Exception("Operation is aborted!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//调用启用、禁用的方法
|
||||||
|
Service::$action($name, 1);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
throw new Exception($e->getMessage());
|
||||||
|
}
|
||||||
|
$output->info(ucfirst($action) . " Successed!");
|
||||||
|
break;
|
||||||
|
case 'uninstall':
|
||||||
|
//非覆盖模式时如果存在则报错
|
||||||
|
if (!$force) {
|
||||||
|
throw new Exception("If you need to uninstall addon, use the parameter --force=true ");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Service::uninstall($name, 0);
|
||||||
|
} catch (AddonException $e) {
|
||||||
|
if ($e->getCode() != -3) {
|
||||||
|
throw new Exception($e->getMessage());
|
||||||
|
}
|
||||||
|
if (!$force) {
|
||||||
|
//如果有冲突文件则提醒
|
||||||
|
$data = $e->getData();
|
||||||
|
foreach ($data['conflictlist'] as $k => $v) {
|
||||||
|
$output->warning($v);
|
||||||
|
}
|
||||||
|
$output->info("Are you sure you want to delete all those files? Type 'yes' to continue: ");
|
||||||
|
$line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r'));
|
||||||
|
if (trim($line) != 'yes') {
|
||||||
|
throw new Exception("Operation is aborted!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Service::uninstall($name, 1);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
throw new Exception($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->info("Uninstall Successed!");
|
||||||
|
break;
|
||||||
|
case 'refresh':
|
||||||
|
Service::refresh();
|
||||||
|
$output->info("Refresh Successed!");
|
||||||
|
break;
|
||||||
|
case 'package':
|
||||||
|
$infoFile = $addonDir . 'info.ini';
|
||||||
|
if (!is_file($infoFile)) {
|
||||||
|
throw new Exception(__('Addon info file was not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$info = get_addon_info($name);
|
||||||
|
if (!$info) {
|
||||||
|
throw new Exception(__('Addon info file data incorrect'));
|
||||||
|
}
|
||||||
|
$infoname = $info['name'] ?? '';
|
||||||
|
if (!$infoname || !preg_match("/^[a-z]+$/i", $infoname) || $infoname != $name) {
|
||||||
|
throw new Exception(__('Addon info name incorrect'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$infoversion = $info['version'] ?? '';
|
||||||
|
if (!$infoversion || !preg_match("/^\d+\.\d+\.\d+$/i", $infoversion)) {
|
||||||
|
throw new Exception(__('Addon info version incorrect'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$addonTmpDir = RUNTIME_PATH . 'addons' . DS;
|
||||||
|
if (!is_dir($addonTmpDir)) {
|
||||||
|
@mkdir($addonTmpDir, 0755, true);
|
||||||
|
}
|
||||||
|
$addonFile = $addonTmpDir . $infoname . '-' . $infoversion . '.zip';
|
||||||
|
if (!class_exists('ZipArchive')) {
|
||||||
|
throw new Exception(__('ZinArchive not install'));
|
||||||
|
}
|
||||||
|
$zip = new \ZipArchive;
|
||||||
|
$zip->open($addonFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
|
||||||
|
|
||||||
|
$files = new \RecursiveIteratorIterator(
|
||||||
|
new \RecursiveDirectoryIterator($addonDir), \RecursiveIteratorIterator::LEAVES_ONLY
|
||||||
|
);
|
||||||
|
|
||||||
|
$addonDir = str_replace(DS, '/', $addonDir);
|
||||||
|
$excludeDirRegex = "/\/(\.git|\.svn|\.vscode|\.idea|unpackage)\//i";
|
||||||
|
foreach ($files as $name => $file) {
|
||||||
|
$filePath = str_replace(DS, '/', $file->getPathname());
|
||||||
|
if ($file->isDir() || preg_match($excludeDirRegex, $filePath))
|
||||||
|
continue;
|
||||||
|
$relativePath = substr($filePath, strlen($addonDir));
|
||||||
|
if (!in_array($file->getFilename(), ['.DS_Store', 'Thumbs.db'])) {
|
||||||
|
$zip->addFile($filePath, $relativePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->close();
|
||||||
|
$output->info("Package Resource Path:" . $addonFile);
|
||||||
|
$output->info("Package Successed!");
|
||||||
|
break;
|
||||||
|
case 'move':
|
||||||
|
$movePath = [
|
||||||
|
'adminOnlySelfDir' => ['admin/behavior', 'admin/controller', 'admin/library', 'admin/model', 'admin/validate', 'admin/view'],
|
||||||
|
'adminAllSubDir' => ['admin/lang'],
|
||||||
|
'publicDir' => ['public/assets/addons', 'public/assets/js/backend']
|
||||||
|
];
|
||||||
|
$paths = [];
|
||||||
|
$appPath = str_replace('/', DS, APP_PATH);
|
||||||
|
$rootPath = str_replace('/', DS, ROOT_PATH);
|
||||||
|
foreach ($movePath as $k => $items) {
|
||||||
|
switch ($k) {
|
||||||
|
case 'adminOnlySelfDir':
|
||||||
|
foreach ($items as $v) {
|
||||||
|
$v = str_replace('/', DS, $v);
|
||||||
|
$oldPath = $appPath . $v . DS . $name;
|
||||||
|
$newPath = $rootPath . "addons" . DS . $name . DS . "application" . DS . $v . DS . $name;
|
||||||
|
$paths[$oldPath] = $newPath;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'adminAllSubDir':
|
||||||
|
foreach ($items as $v) {
|
||||||
|
$v = str_replace('/', DS, $v);
|
||||||
|
$vPath = $appPath . $v;
|
||||||
|
$list = scandir($vPath);
|
||||||
|
foreach ($list as $_v) {
|
||||||
|
if (!in_array($_v, ['.', '..']) && is_dir($vPath . DS . $_v)) {
|
||||||
|
$oldPath = $appPath . $v . DS . $_v . DS . $name;
|
||||||
|
$newPath = $rootPath . "addons" . DS . $name . DS . "application" . DS . $v . DS . $_v . DS . $name;
|
||||||
|
$paths[$oldPath] = $newPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'publicDir':
|
||||||
|
foreach ($items as $v) {
|
||||||
|
$v = str_replace('/', DS, $v);
|
||||||
|
$oldPath = $rootPath . $v . DS . $name;
|
||||||
|
$newPath = $rootPath . 'addons' . DS . $name . DS . $v . DS . $name;
|
||||||
|
$paths[$oldPath] = $newPath;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($paths as $oldPath => $newPath) {
|
||||||
|
if (is_dir($oldPath)) {
|
||||||
|
if ($force) {
|
||||||
|
if (is_dir($newPath)) {
|
||||||
|
$list = scandir($newPath);
|
||||||
|
foreach ($list as $_v) {
|
||||||
|
if (!in_array($_v, ['.', '..'])) {
|
||||||
|
$file = $newPath . DS . $_v;
|
||||||
|
@chmod($file, 0777);
|
||||||
|
@unlink($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@rmdir($newPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
copydirs($oldPath, $newPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取创建菜单的数组
|
||||||
|
* @param array $menu
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function getCreateMenu($menu)
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
foreach ($menu as $k => & $v) {
|
||||||
|
$arr = [
|
||||||
|
'name' => $v['name'],
|
||||||
|
'title' => $v['title'],
|
||||||
|
];
|
||||||
|
if ($v['icon'] != 'fa fa-circle-o') {
|
||||||
|
$arr['icon'] = $v['icon'];
|
||||||
|
}
|
||||||
|
if ($v['ismenu']) {
|
||||||
|
$arr['ismenu'] = $v['ismenu'];
|
||||||
|
}
|
||||||
|
if (isset($v['childlist']) && $v['childlist']) {
|
||||||
|
$arr['sublist'] = $this->getCreateMenu($v['childlist']);
|
||||||
|
}
|
||||||
|
$result[] = $arr;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入到文件
|
||||||
|
* @param string $name
|
||||||
|
* @param array $data
|
||||||
|
* @param string $pathname
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
protected function writeToFile($name, $data, $pathname)
|
||||||
|
{
|
||||||
|
$search = $replace = [];
|
||||||
|
foreach ($data as $k => $v) {
|
||||||
|
$search[] = "{%{$k}%}";
|
||||||
|
$replace[] = $v;
|
||||||
|
}
|
||||||
|
$stub = file_get_contents($this->getStub($name));
|
||||||
|
$content = str_replace($search, $replace, $stub);
|
||||||
|
|
||||||
|
if (!is_dir(dirname($pathname))) {
|
||||||
|
mkdir(strtolower(dirname($pathname)), 0755, true);
|
||||||
|
}
|
||||||
|
return file_put_contents($pathname, $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取基础模板
|
||||||
|
* @param string $name
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getStub($name)
|
||||||
|
{
|
||||||
|
return __DIR__ . '/Addon/stubs/' . $name . '.stub';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace addons\{%name%};
|
||||||
|
|
||||||
|
use app\common\library\Menu;
|
||||||
|
use think\Addons;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件
|
||||||
|
*/
|
||||||
|
class {%addonClassName%} extends Addons
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件安装方法
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function install()
|
||||||
|
{
|
||||||
|
{%addonInstallMenu%}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件卸载方法
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function uninstall()
|
||||||
|
{
|
||||||
|
{%addonUninstallMenu%}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件启用方法
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function enable()
|
||||||
|
{
|
||||||
|
{%addonEnableMenu%}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件禁用方法
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function disable()
|
||||||
|
{
|
||||||
|
{%addonDisableMenu%}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
//配置唯一标识
|
||||||
|
'name' => 'username',
|
||||||
|
//显示的标题
|
||||||
|
'title' => '用户名',
|
||||||
|
//类型
|
||||||
|
'type' => 'string',
|
||||||
|
//分组
|
||||||
|
'group' => '',
|
||||||
|
//动态显示
|
||||||
|
'visible' => '',
|
||||||
|
//数据字典
|
||||||
|
'content' => [
|
||||||
|
],
|
||||||
|
//值
|
||||||
|
'value' => '',
|
||||||
|
//验证规则
|
||||||
|
'rule' => 'required',
|
||||||
|
//错误消息
|
||||||
|
'msg' => '',
|
||||||
|
//提示消息
|
||||||
|
'tip' => '',
|
||||||
|
//成功消息
|
||||||
|
'ok' => '',
|
||||||
|
//扩展信息
|
||||||
|
'extend' => ''
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'password',
|
||||||
|
'title' => '密码',
|
||||||
|
'type' => 'string',
|
||||||
|
'content' => [
|
||||||
|
],
|
||||||
|
'value' => '',
|
||||||
|
'rule' => 'required',
|
||||||
|
'msg' => '',
|
||||||
|
'tip' => '',
|
||||||
|
'ok' => '',
|
||||||
|
'extend' => ''
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace addons\{%addon%}\controller;
|
||||||
|
|
||||||
|
use think\addons\Controller;
|
||||||
|
|
||||||
|
class Index extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$this->error("当前插件暂无前台页面");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
name = {%name%}
|
||||||
|
title = 插件名称{%name%}
|
||||||
|
intro = 插件介绍
|
||||||
|
author = yourname
|
||||||
|
website = https://www.fastadmin.net
|
||||||
|
version = 1.0.0
|
||||||
|
state = 1
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\command;
|
||||||
|
|
||||||
|
use app\admin\command\Api\library\Builder;
|
||||||
|
use think\Config;
|
||||||
|
use think\console\Command;
|
||||||
|
use think\console\Input;
|
||||||
|
use think\console\input\Option;
|
||||||
|
use think\console\Output;
|
||||||
|
use think\Exception;
|
||||||
|
|
||||||
|
class Api extends Command
|
||||||
|
{
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$site = Config::get('site');
|
||||||
|
$this
|
||||||
|
->setName('api')
|
||||||
|
->addOption('url', 'u', Option::VALUE_OPTIONAL, 'default api url', '')
|
||||||
|
->addOption('cdnurl', 'd', Option::VALUE_OPTIONAL, 'default cdn url', '')
|
||||||
|
->addOption('module', 'm', Option::VALUE_OPTIONAL, 'module name(admin/index/api)', 'api')
|
||||||
|
->addOption('output', 'o', Option::VALUE_OPTIONAL, 'output index file name', 'api.html')
|
||||||
|
->addOption('template', 'e', Option::VALUE_OPTIONAL, '', 'index.html')
|
||||||
|
->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force override general file', false)
|
||||||
|
->addOption('title', 't', Option::VALUE_OPTIONAL, 'document title', $site['name'] ?? '')
|
||||||
|
->addOption('class', 'c', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'extend class', null)
|
||||||
|
->addOption('language', 'l', Option::VALUE_OPTIONAL, 'language', 'zh-cn')
|
||||||
|
->addOption('addon', 'a', Option::VALUE_OPTIONAL, 'addon name', null)
|
||||||
|
->addOption('controller', 'r', Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, 'controller name', null)
|
||||||
|
->setDescription('Build Api document from controller');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(Input $input, Output $output)
|
||||||
|
{
|
||||||
|
$apiDir = __DIR__ . DS . 'Api' . DS;
|
||||||
|
|
||||||
|
$force = $input->getOption('force');
|
||||||
|
$url = $input->getOption('url');
|
||||||
|
$cdnurl = $input->getOption('cdnurl');
|
||||||
|
$language = $input->getOption('language');
|
||||||
|
$template = $input->getOption('template');
|
||||||
|
if (!preg_match("/^([a-z0-9]+)\.html\$/i", $template)) {
|
||||||
|
throw new Exception('template file not correct');
|
||||||
|
}
|
||||||
|
$language = $language ? $language : 'zh-cn';
|
||||||
|
$langFile = $apiDir . 'lang' . DS . $language . '.php';
|
||||||
|
if (!is_file($langFile)) {
|
||||||
|
throw new Exception('language file not found');
|
||||||
|
}
|
||||||
|
$lang = include_once $langFile;
|
||||||
|
// 目标目录
|
||||||
|
$output_dir = ROOT_PATH . 'public' . DS;
|
||||||
|
$output_file = $output_dir . $input->getOption('output');
|
||||||
|
if (is_file($output_file) && !$force) {
|
||||||
|
throw new Exception("api index file already exists!\nIf you need to rebuild again, use the parameter --force=true ");
|
||||||
|
}
|
||||||
|
// 模板文件
|
||||||
|
$template_dir = $apiDir . 'template' . DS;
|
||||||
|
$template_file = $template_dir . $template;
|
||||||
|
if (!is_file($template_file)) {
|
||||||
|
throw new Exception('template file not found');
|
||||||
|
}
|
||||||
|
// 额外的类
|
||||||
|
$classes = $input->getOption('class');
|
||||||
|
// 标题
|
||||||
|
$title = $input->getOption('title');
|
||||||
|
// 模块
|
||||||
|
$module = $input->getOption('module');
|
||||||
|
// 插件
|
||||||
|
$addon = $input->getOption('addon');
|
||||||
|
|
||||||
|
$moduleDir = $addonDir = '';
|
||||||
|
if ($addon) {
|
||||||
|
$addonInfo = get_addon_info($addon);
|
||||||
|
if (!$addonInfo) {
|
||||||
|
throw new Exception('addon not found');
|
||||||
|
}
|
||||||
|
$moduleDir = ADDON_PATH . $addon . DS;
|
||||||
|
} else {
|
||||||
|
$moduleDir = APP_PATH . $module . DS;
|
||||||
|
}
|
||||||
|
if (!is_dir($moduleDir)) {
|
||||||
|
throw new Exception('module not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version_compare(PHP_VERSION, '7.0.0', '<')) {
|
||||||
|
throw new Exception("Requires PHP version 7.0 or newer");
|
||||||
|
}
|
||||||
|
|
||||||
|
//控制器名
|
||||||
|
$controller = $input->getOption('controller') ?: [];
|
||||||
|
if (!$controller) {
|
||||||
|
$controllerDir = $moduleDir . Config::get('url_controller_layer') . DS;
|
||||||
|
$files = new \RecursiveIteratorIterator(
|
||||||
|
new \RecursiveDirectoryIterator($controllerDir),
|
||||||
|
\RecursiveIteratorIterator::LEAVES_ONLY
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($files as $name => $file) {
|
||||||
|
if (!$file->isDir() && $file->getExtension() == 'php') {
|
||||||
|
$filePath = $file->getRealPath();
|
||||||
|
$className = $this->getClassFromFile($filePath);
|
||||||
|
if ($className) {
|
||||||
|
$classes[] = $className;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
foreach ($controller as $index => $item) {
|
||||||
|
$filePath = $moduleDir . Config::get('url_controller_layer') . DS . $item . '.php';
|
||||||
|
$className = $this->getClassFromFile($filePath);
|
||||||
|
if ($className) {
|
||||||
|
$classes[] = $className;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$classes = array_unique(array_filter($classes));
|
||||||
|
|
||||||
|
$cdnurl = $cdnurl ? : Config::get('site.cdnurl');
|
||||||
|
|
||||||
|
$config = [
|
||||||
|
'sitename' => config('site.name'),
|
||||||
|
'title' => $title,
|
||||||
|
'author' => config('site.name'),
|
||||||
|
'description' => '',
|
||||||
|
'apiurl' => $url,
|
||||||
|
'cdnurl' => $cdnurl,
|
||||||
|
'language' => $language,
|
||||||
|
];
|
||||||
|
|
||||||
|
Config::set('view_replace_str.__CDN__', $cdnurl);
|
||||||
|
$builder = new Builder($classes);
|
||||||
|
$content = $builder->render($template_file, ['config' => $config, 'lang' => $lang]);
|
||||||
|
|
||||||
|
if (!file_put_contents($output_file, $content)) {
|
||||||
|
throw new Exception('Cannot save the content to ' . $output_file);
|
||||||
|
}
|
||||||
|
$output->info("Build Successed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从文件获取命名空间和类名
|
||||||
|
*
|
||||||
|
* @param string $filename
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getClassFromFile($filename)
|
||||||
|
{
|
||||||
|
$getNext = null;
|
||||||
|
$isNamespace = false;
|
||||||
|
$skipNext = false;
|
||||||
|
$namespace = '';
|
||||||
|
$class = '';
|
||||||
|
foreach (\PhpToken::tokenize(file_get_contents($filename)) as $token) {
|
||||||
|
if (!$token->isIgnorable()) {
|
||||||
|
$name = $token->getTokenName();
|
||||||
|
switch ($name) {
|
||||||
|
case 'T_NAMESPACE':
|
||||||
|
$isNamespace = true;
|
||||||
|
break;
|
||||||
|
case 'T_EXTENDS':
|
||||||
|
case 'T_USE':
|
||||||
|
case 'T_IMPLEMENTS':
|
||||||
|
$skipNext = true;
|
||||||
|
break;
|
||||||
|
case 'T_CLASS':
|
||||||
|
if ($skipNext) {
|
||||||
|
$skipNext = false;
|
||||||
|
} else {
|
||||||
|
$getNext = strtolower(substr($name, 2));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'T_NAME_QUALIFIED':
|
||||||
|
case 'T_NS_SEPARATOR':
|
||||||
|
case 'T_STRING':
|
||||||
|
case ';':
|
||||||
|
if ($isNamespace) {
|
||||||
|
if ($name == ';') {
|
||||||
|
$isNamespace = false;
|
||||||
|
} else {
|
||||||
|
$namespace .= $token->text;
|
||||||
|
}
|
||||||
|
} elseif ($skipNext) {
|
||||||
|
$skipNext = false;
|
||||||
|
} elseif ($getNext == 'class') {
|
||||||
|
$class = $token->text;
|
||||||
|
$getNext = null;
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$getNext = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$className = $namespace . '\\' . $class;
|
||||||
|
return preg_match('/([a-z0-9_\\]+)([a-z0-9_]+)$/i', $className) ? $className : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'Info' => '基础信息',
|
||||||
|
'Sandbox' => '在线测试',
|
||||||
|
'Sampleoutput' => '返回示例',
|
||||||
|
'Headers' => 'Headers',
|
||||||
|
'Parameters' => '参数',
|
||||||
|
'Body' => '正文',
|
||||||
|
'Name' => '名称',
|
||||||
|
'Type' => '类型',
|
||||||
|
'Required' => '必选',
|
||||||
|
'Description' => '描述',
|
||||||
|
'Send' => '提交',
|
||||||
|
'Reset' => '重置',
|
||||||
|
'Tokentips' => 'Token在会员注册或登录后都会返回,WEB端同时存在于Cookie中',
|
||||||
|
'Apiurltips' => 'API接口URL',
|
||||||
|
'Savetips' => '点击保存后Token和Api url都将保存在本地Localstorage中',
|
||||||
|
'Authorization' => '权限',
|
||||||
|
'NeedLogin' => '登录',
|
||||||
|
'NeedRight' => '鉴权',
|
||||||
|
'ReturnHeaders' => '响应头',
|
||||||
|
'ReturnParameters' => '返回参数',
|
||||||
|
'Response' => '响应输出',
|
||||||
|
];
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\command\Api\library;
|
||||||
|
|
||||||
|
use think\Config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @website https://github.com/calinrada/php-apidoc
|
||||||
|
* @author Calin Rada <rada.calin@gmail.com>
|
||||||
|
* @author Karson <karson@fastadmin.net>
|
||||||
|
*/
|
||||||
|
class Builder
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @var \think\View
|
||||||
|
*/
|
||||||
|
public $view = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* parse classes
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $classes = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param array $classes
|
||||||
|
*/
|
||||||
|
public function __construct($classes = [])
|
||||||
|
{
|
||||||
|
$this->classes = array_merge($this->classes, $classes);
|
||||||
|
$this->view = new \think\View(Config::get('template'), Config::get('view_replace_str'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function extractAnnotations()
|
||||||
|
{
|
||||||
|
foreach ($this->classes as $class) {
|
||||||
|
$classAnnotation = Extractor::getClassAnnotations($class);
|
||||||
|
// 如果忽略
|
||||||
|
if (isset($classAnnotation['ApiInternal'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Extractor::getClassMethodAnnotations($class);
|
||||||
|
//Extractor::getClassPropertyValues($class);
|
||||||
|
}
|
||||||
|
$allClassAnnotation = Extractor::getAllClassAnnotations();
|
||||||
|
$allClassMethodAnnotation = Extractor::getAllClassMethodAnnotations();
|
||||||
|
//$allClassPropertyValue = Extractor::getAllClassPropertyValues();
|
||||||
|
|
||||||
|
// foreach ($allClassMethodAnnotation as $className => &$methods) {
|
||||||
|
// foreach ($methods as &$method) {
|
||||||
|
// //权重判断
|
||||||
|
// if ($method && !isset($method['ApiWeigh']) && isset($allClassAnnotation[$className]['ApiWeigh'])) {
|
||||||
|
// $method['ApiWeigh'] = $allClassAnnotation[$className]['ApiWeigh'];
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// unset($methods);
|
||||||
|
return [$allClassAnnotation, $allClassMethodAnnotation];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function generateHeadersTemplate($docs)
|
||||||
|
{
|
||||||
|
if (!isset($docs['ApiHeaders'])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$headerslist = array();
|
||||||
|
foreach ($docs['ApiHeaders'] as $params) {
|
||||||
|
$tr = array(
|
||||||
|
'name' => $params['name'] ?? '',
|
||||||
|
'type' => $params['type'] ?? 'string',
|
||||||
|
'sample' => $params['sample'] ?? '',
|
||||||
|
'required' => $params['required'] ?? false,
|
||||||
|
'description' => $params['description'] ?? '',
|
||||||
|
);
|
||||||
|
$headerslist[] = $tr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $headerslist;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function generateParamsTemplate($docs)
|
||||||
|
{
|
||||||
|
if (!isset($docs['ApiParams'])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$typeArr = [
|
||||||
|
'integer' => 'number',
|
||||||
|
'file' => 'file',
|
||||||
|
];
|
||||||
|
$paramslist = array();
|
||||||
|
foreach ($docs['ApiParams'] as $params) {
|
||||||
|
$type = strtolower($params['type'] ?? 'string');
|
||||||
|
$inputtype = $typeArr[$type] ?? ($params['name'] == 'password' ? 'password' : 'text');
|
||||||
|
$tr = array(
|
||||||
|
'name' => $params['name'],
|
||||||
|
'type' => $type,
|
||||||
|
'inputtype' => $inputtype,
|
||||||
|
'sample' => $params['sample'] ?? '',
|
||||||
|
'required' => $params['required'] ?? true,
|
||||||
|
'description' => $params['description'] ?? '',
|
||||||
|
);
|
||||||
|
$paramslist[] = $tr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $paramslist;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function generateReturnHeadersTemplate($docs)
|
||||||
|
{
|
||||||
|
if (!isset($docs['ApiReturnHeaders'])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$headerslist = array();
|
||||||
|
foreach ($docs['ApiReturnHeaders'] as $params) {
|
||||||
|
$tr = array(
|
||||||
|
'name' => $params['name'] ?? '',
|
||||||
|
'type' => 'string',
|
||||||
|
'sample' => $params['sample'] ?? '',
|
||||||
|
'required' => isset($params['required']) && $params['required'] ? 'Yes' : 'No',
|
||||||
|
'description' => $params['description'] ?? '',
|
||||||
|
);
|
||||||
|
$headerslist[] = $tr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $headerslist;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function generateReturnParamsTemplate($st_params)
|
||||||
|
{
|
||||||
|
if (!isset($st_params['ApiReturnParams'])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$paramslist = array();
|
||||||
|
foreach ($st_params['ApiReturnParams'] as $params) {
|
||||||
|
$tr = array(
|
||||||
|
'name' => $params['name'] ?? '',
|
||||||
|
'type' => $params['type'] ?? 'string',
|
||||||
|
'sample' => $params['sample'] ?? '',
|
||||||
|
'description' => $params['description'] ?? '',
|
||||||
|
);
|
||||||
|
$paramslist[] = $tr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $paramslist;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function generateBadgeForMethod($data)
|
||||||
|
{
|
||||||
|
$method = strtoupper(is_array($data['ApiMethod'][0]) ? $data['ApiMethod'][0]['data'] : $data['ApiMethod'][0]);
|
||||||
|
$labes = array(
|
||||||
|
'POST' => 'label-primary',
|
||||||
|
'GET' => 'label-success',
|
||||||
|
'PUT' => 'label-warning',
|
||||||
|
'DELETE' => 'label-danger',
|
||||||
|
'PATCH' => 'label-default',
|
||||||
|
'OPTIONS' => 'label-info'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $labes[$method] ?? $labes['GET'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function parse()
|
||||||
|
{
|
||||||
|
list($allClassAnnotations, $allClassMethodAnnotations) = $this->extractAnnotations();
|
||||||
|
|
||||||
|
$sectorArr = [];
|
||||||
|
foreach ($allClassAnnotations as $index => &$allClassAnnotation) {
|
||||||
|
// 如果设置隐藏,则不显示在文档
|
||||||
|
if (isset($allClassAnnotation['ApiInternal'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$sector = isset($allClassAnnotation['ApiSector']) ? $allClassAnnotation['ApiSector'][0] : $allClassAnnotation['ApiTitle'][0];
|
||||||
|
$sectorArr[$sector] = isset($allClassAnnotation['ApiWeigh']) ? $allClassAnnotation['ApiWeigh'][0] : 0;
|
||||||
|
}
|
||||||
|
unset($allClassAnnotation);
|
||||||
|
|
||||||
|
arsort($sectorArr);
|
||||||
|
$routes = include_once CONF_PATH . 'route.php';
|
||||||
|
$subdomain = false;
|
||||||
|
if (config('url_domain_deploy') && isset($routes['__domain__']) && isset($routes['__domain__']['api']) && $routes['__domain__']['api']) {
|
||||||
|
$subdomain = true;
|
||||||
|
}
|
||||||
|
$counter = 0;
|
||||||
|
$section = null;
|
||||||
|
$weigh = 0;
|
||||||
|
$docsList = [];
|
||||||
|
foreach ($allClassMethodAnnotations as $class => $methods) {
|
||||||
|
foreach ($methods as $name => $docs) {
|
||||||
|
if (isset($docs['ApiSector'][0])) {
|
||||||
|
$section = is_array($docs['ApiSector'][0]) ? $docs['ApiSector'][0]['data'] : $docs['ApiSector'][0];
|
||||||
|
} else {
|
||||||
|
$section = $class;
|
||||||
|
}
|
||||||
|
if (0 === count($docs)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$route = is_array($docs['ApiRoute'][0]) ? $docs['ApiRoute'][0]['data'] : $docs['ApiRoute'][0];
|
||||||
|
if ($subdomain) {
|
||||||
|
$route = substr($route, 4);
|
||||||
|
}
|
||||||
|
$docsList[$section][$name] = [
|
||||||
|
'id' => $counter,
|
||||||
|
'method' => is_array($docs['ApiMethod'][0]) ? $docs['ApiMethod'][0]['data'] : $docs['ApiMethod'][0],
|
||||||
|
'methodLabel' => $this->generateBadgeForMethod($docs),
|
||||||
|
'section' => $section,
|
||||||
|
'route' => $route,
|
||||||
|
'title' => is_array($docs['ApiTitle'][0]) ? $docs['ApiTitle'][0]['data'] : $docs['ApiTitle'][0],
|
||||||
|
'summary' => is_array($docs['ApiSummary'][0]) ? $docs['ApiSummary'][0]['data'] : $docs['ApiSummary'][0],
|
||||||
|
'body' => isset($docs['ApiBody'][0]) ? (is_array($docs['ApiBody'][0]) ? $docs['ApiBody'][0]['data'] : $docs['ApiBody'][0]) : '',
|
||||||
|
'headersList' => $this->generateHeadersTemplate($docs),
|
||||||
|
'paramsList' => $this->generateParamsTemplate($docs),
|
||||||
|
'returnHeadersList' => $this->generateReturnHeadersTemplate($docs),
|
||||||
|
'returnParamsList' => $this->generateReturnParamsTemplate($docs),
|
||||||
|
'weigh' => is_array($docs['ApiWeigh'][0]) ? $docs['ApiWeigh'][0]['data'] : $docs['ApiWeigh'][0],
|
||||||
|
'return' => isset($docs['ApiReturn']) ? (is_array($docs['ApiReturn'][0]) ? $docs['ApiReturn'][0]['data'] : $docs['ApiReturn'][0]) : '',
|
||||||
|
'needLogin' => $docs['ApiPermissionLogin'][0],
|
||||||
|
'needRight' => $docs['ApiPermissionRight'][0],
|
||||||
|
];
|
||||||
|
$counter++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//重建排序
|
||||||
|
foreach ($docsList as $index => &$methods) {
|
||||||
|
$methodSectorArr = [];
|
||||||
|
foreach ($methods as $name => $method) {
|
||||||
|
$methodSectorArr[$name] = $method['weigh'] ?? 0;
|
||||||
|
}
|
||||||
|
arsort($methodSectorArr);
|
||||||
|
$methods = array_merge(array_flip(array_keys($methodSectorArr)), $methods);
|
||||||
|
}
|
||||||
|
$docsList = array_merge(array_flip(array_keys($sectorArr)), $docsList);
|
||||||
|
return $docsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getView()
|
||||||
|
{
|
||||||
|
return $this->view;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染
|
||||||
|
* @param string $template
|
||||||
|
* @param array $vars
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function render($template, $vars = [])
|
||||||
|
{
|
||||||
|
$docsList = $this->parse();
|
||||||
|
return $this->view->display(file_get_contents($template), array_merge($vars, ['docsList' => $docsList]));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,544 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\command\Api\library;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class imported from https://github.com/eriknyk/Annotations
|
||||||
|
* @author Erik Amaru Ortiz https://github.com/eriknyk
|
||||||
|
*
|
||||||
|
* @license http://opensource.org/licenses/bsd-license.php The BSD License
|
||||||
|
* @author Calin Rada <rada.calin@gmail.com>
|
||||||
|
*/
|
||||||
|
class Extractor
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static array to store already parsed annotations
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private static $annotationCache;
|
||||||
|
|
||||||
|
private static $classAnnotationCache;
|
||||||
|
|
||||||
|
private static $classMethodAnnotationCache;
|
||||||
|
|
||||||
|
private static $classPropertyValueCache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that annotations should has strict behavior, 'false' by default
|
||||||
|
* @var boolean
|
||||||
|
*/
|
||||||
|
private $strict = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the default namespace for Objects instance, usually used on methods like getMethodAnnotationsObjects()
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $defaultNamespace = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets strict variable to true/false
|
||||||
|
* @param bool $value boolean value to indicate that annotations to has strict behavior
|
||||||
|
*/
|
||||||
|
public function setStrict($value)
|
||||||
|
{
|
||||||
|
$this->strict = (bool)$value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets default namespace to use in object instantiation
|
||||||
|
* @param string $namespace default namespace
|
||||||
|
*/
|
||||||
|
public function setDefaultNamespace($namespace)
|
||||||
|
{
|
||||||
|
$this->defaultNamespace = $namespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets default namespace used in object instantiation
|
||||||
|
* @return string $namespace default namespace
|
||||||
|
*/
|
||||||
|
public function getDefaultAnnotationNamespace()
|
||||||
|
{
|
||||||
|
return $this->defaultNamespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all anotations with pattern @SomeAnnotation() from a given class
|
||||||
|
*
|
||||||
|
* @param string $className class name to get annotations
|
||||||
|
* @return array self::$classAnnotationCache all annotated elements
|
||||||
|
*/
|
||||||
|
public static function getClassAnnotations($className)
|
||||||
|
{
|
||||||
|
if (!isset(self::$classAnnotationCache[$className])) {
|
||||||
|
$class = new \ReflectionClass($className);
|
||||||
|
$annotationArr = self::parseAnnotations($class->getDocComment());
|
||||||
|
$annotationArr['ApiTitle'] = !isset($annotationArr['ApiTitle'][0]) || !trim($annotationArr['ApiTitle'][0]) ? [$class->getShortName()] : $annotationArr['ApiTitle'];
|
||||||
|
self::$classAnnotationCache[$className] = $annotationArr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$classAnnotationCache[$className];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取类所有方法的属性配置
|
||||||
|
* @param $className
|
||||||
|
* @return mixed
|
||||||
|
* @throws \ReflectionException
|
||||||
|
*/
|
||||||
|
public static function getClassMethodAnnotations($className)
|
||||||
|
{
|
||||||
|
$class = new \ReflectionClass($className);
|
||||||
|
|
||||||
|
foreach ($class->getMethods() as $object) {
|
||||||
|
self::$classMethodAnnotationCache[$className][$object->name] = self::getMethodAnnotations($className, $object->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$classMethodAnnotationCache[$className];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getClassPropertyValues($className)
|
||||||
|
{
|
||||||
|
$class = new \ReflectionClass($className);
|
||||||
|
|
||||||
|
foreach ($class->getProperties() as $object) {
|
||||||
|
self::$classPropertyValueCache[$className][$object->name] = self::getClassPropertyValue($className, $object->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$classMethodAnnotationCache[$className];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getAllClassAnnotations()
|
||||||
|
{
|
||||||
|
return self::$classAnnotationCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getAllClassMethodAnnotations()
|
||||||
|
{
|
||||||
|
return self::$classMethodAnnotationCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getAllClassPropertyValues()
|
||||||
|
{
|
||||||
|
return self::$classPropertyValueCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getClassPropertyValue($className, $property)
|
||||||
|
{
|
||||||
|
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||||
|
$reflectionClass = new \ReflectionClass($className);
|
||||||
|
$reflectionProperty = $reflectionClass->getProperty($property);
|
||||||
|
$reflectionProperty->setAccessible(true);
|
||||||
|
return $reflectionProperty->getValue($reflectionClass->newInstanceWithoutConstructor());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all anotations with pattern @SomeAnnotation() from a determinated method of a given class
|
||||||
|
*
|
||||||
|
* @param string $className class name
|
||||||
|
* @param string $methodName method name to get annotations
|
||||||
|
* @return array self::$annotationCache all annotated elements of a method given
|
||||||
|
*/
|
||||||
|
public static function getMethodAnnotations($className, $methodName)
|
||||||
|
{
|
||||||
|
if (!isset(self::$annotationCache[$className . '::' . $methodName])) {
|
||||||
|
try {
|
||||||
|
$method = new \ReflectionMethod($className, $methodName);
|
||||||
|
$class = new \ReflectionClass($className);
|
||||||
|
if (!$method->isPublic() || $method->isConstructor()) {
|
||||||
|
$annotations = array();
|
||||||
|
} else {
|
||||||
|
$annotations = self::consolidateAnnotations($method, $class);
|
||||||
|
}
|
||||||
|
} catch (\ReflectionException $e) {
|
||||||
|
$annotations = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$annotationCache[$className . '::' . $methodName] = $annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$annotationCache[$className . '::' . $methodName];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all anotations with pattern @SomeAnnotation() from a determinated method of a given class
|
||||||
|
* and instance its abcAnnotation class
|
||||||
|
*
|
||||||
|
* @param string $className class name
|
||||||
|
* @param string $methodName method name to get annotations
|
||||||
|
* @return array self::$annotationCache all annotated objects of a method given
|
||||||
|
*/
|
||||||
|
public function getMethodAnnotationsObjects($className, $methodName)
|
||||||
|
{
|
||||||
|
$annotations = $this->getMethodAnnotations($className, $methodName);
|
||||||
|
$objects = array();
|
||||||
|
|
||||||
|
$i = 0;
|
||||||
|
|
||||||
|
foreach ($annotations as $annotationClass => $listParams) {
|
||||||
|
$annotationClass = ucfirst($annotationClass);
|
||||||
|
$class = $this->defaultNamespace . $annotationClass . 'Annotation';
|
||||||
|
|
||||||
|
// verify is the annotation class exists, depending if Annotations::strict is true
|
||||||
|
// if not, just skip the annotation instance creation.
|
||||||
|
if (!class_exists($class)) {
|
||||||
|
if ($this->strict) {
|
||||||
|
throw new Exception(sprintf('Runtime Error: Annotation Class Not Found: %s', $class));
|
||||||
|
} else {
|
||||||
|
// silent skip & continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($objects[$annotationClass])) {
|
||||||
|
$objects[$annotationClass] = new $class();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($listParams as $params) {
|
||||||
|
if (is_array($params)) {
|
||||||
|
foreach ($params as $key => $value) {
|
||||||
|
$objects[$annotationClass]->set($key, $value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$objects[$annotationClass]->set($i++, $params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $objects;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function consolidateAnnotations($method, $class)
|
||||||
|
{
|
||||||
|
$dockblockClass = $class->getDocComment();
|
||||||
|
$docblockMethod = $method->getDocComment();
|
||||||
|
$methodName = $method->getName();
|
||||||
|
|
||||||
|
$methodAnnotations = self::parseAnnotations($docblockMethod);
|
||||||
|
$methodAnnotations['ApiTitle'] = !isset($methodAnnotations['ApiTitle'][0]) || !trim($methodAnnotations['ApiTitle'][0]) ? [$method->getName()] : $methodAnnotations['ApiTitle'];
|
||||||
|
|
||||||
|
$classAnnotations = self::parseAnnotations($dockblockClass);
|
||||||
|
$classAnnotations['ApiTitle'] = !isset($classAnnotations['ApiTitle'][0]) || !trim($classAnnotations['ApiTitle'][0]) ? [$class->getShortName()] : $classAnnotations['ApiTitle'];
|
||||||
|
|
||||||
|
if (isset($methodAnnotations['ApiInternal']) || $methodName == '_initialize' || $methodName == '_empty') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$properties = $class->getDefaultProperties();
|
||||||
|
$noNeedLogin = isset($properties['noNeedLogin']) ? (is_array($properties['noNeedLogin']) ? $properties['noNeedLogin'] : [$properties['noNeedLogin']]) : [];
|
||||||
|
$noNeedRight = isset($properties['noNeedRight']) ? (is_array($properties['noNeedRight']) ? $properties['noNeedRight'] : [$properties['noNeedRight']]) : [];
|
||||||
|
|
||||||
|
preg_match_all("/\*[\s]+(.*)(\\r\\n|\\r|\\n)/U", str_replace('/**', '', $docblockMethod), $methodArr);
|
||||||
|
preg_match_all("/\*[\s]+(.*)(\\r\\n|\\r|\\n)/U", str_replace('/**', '', $dockblockClass), $classArr);
|
||||||
|
|
||||||
|
if (!isset($methodAnnotations['ApiMethod'])) {
|
||||||
|
$methodAnnotations['ApiMethod'] = ['get'];
|
||||||
|
}
|
||||||
|
if (!isset($methodAnnotations['ApiWeigh'])) {
|
||||||
|
$methodAnnotations['ApiWeigh'] = [0];
|
||||||
|
}
|
||||||
|
if (!isset($methodAnnotations['ApiSummary'])) {
|
||||||
|
$methodAnnotations['ApiSummary'] = $methodAnnotations['ApiTitle'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($methodAnnotations) {
|
||||||
|
foreach ($classAnnotations as $name => $valueClass) {
|
||||||
|
if (count($valueClass) !== 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($name === 'ApiRoute') {
|
||||||
|
if (isset($methodAnnotations[$name])) {
|
||||||
|
$methodAnnotations[$name] = [rtrim($valueClass[0], '/') . $methodAnnotations[$name][0]];
|
||||||
|
} else {
|
||||||
|
$methodAnnotations[$name] = [rtrim($valueClass[0], '/') . '/' . $method->getName()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($name === 'ApiSector') {
|
||||||
|
$methodAnnotations[$name] = $valueClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isset($methodAnnotations['ApiRoute'])) {
|
||||||
|
$urlArr = [];
|
||||||
|
$className = $class->getName();
|
||||||
|
|
||||||
|
list($prefix, $suffix) = explode('\\' . \think\Config::get('url_controller_layer') . '\\', $className);
|
||||||
|
$prefixArr = explode('\\', $prefix);
|
||||||
|
$suffixArr = explode('\\', $suffix);
|
||||||
|
if ($prefixArr[0] == \think\Config::get('app_namespace')) {
|
||||||
|
$prefixArr[0] = '';
|
||||||
|
}
|
||||||
|
$urlArr = array_merge($urlArr, $prefixArr);
|
||||||
|
$urlArr[] = implode('.', array_map(function ($item) {
|
||||||
|
return \think\Loader::parseName($item);
|
||||||
|
}, $suffixArr));
|
||||||
|
$urlArr[] = $method->getName();
|
||||||
|
|
||||||
|
$methodAnnotations['ApiRoute'] = [implode('/', $urlArr)];
|
||||||
|
}
|
||||||
|
if (!isset($methodAnnotations['ApiSector'])) {
|
||||||
|
$methodAnnotations['ApiSector'] = isset($classAnnotations['ApiSector']) ? $classAnnotations['ApiSector'] : $classAnnotations['ApiTitle'];
|
||||||
|
}
|
||||||
|
if (!isset($methodAnnotations['ApiParams'])) {
|
||||||
|
$params = self::parseCustomAnnotations($docblockMethod, 'param');
|
||||||
|
foreach ($params as $k => $v) {
|
||||||
|
$arr = explode(' ', preg_replace("/[\s]+/", " ", $v));
|
||||||
|
$methodAnnotations['ApiParams'][] = [
|
||||||
|
'name' => isset($arr[1]) ? str_replace('$', '', $arr[1]) : '',
|
||||||
|
'nullable' => false,
|
||||||
|
'type' => isset($arr[0]) ? $arr[0] : 'string',
|
||||||
|
'description' => isset($arr[2]) ? $arr[2] : ''
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$methodAnnotations['ApiPermissionLogin'] = [!in_array('*', $noNeedLogin) && !in_array($methodName, $noNeedLogin)];
|
||||||
|
$methodAnnotations['ApiPermissionRight'] = !$methodAnnotations['ApiPermissionLogin'][0] ? [false] : [!in_array('*', $noNeedRight) && !in_array($methodName, $noNeedRight)];
|
||||||
|
return $methodAnnotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse annotations
|
||||||
|
*
|
||||||
|
* @param string $docblock
|
||||||
|
* @param string $name
|
||||||
|
* @return array parsed annotations params
|
||||||
|
*/
|
||||||
|
private static function parseCustomAnnotations($docblock, $name = 'param')
|
||||||
|
{
|
||||||
|
$annotations = array();
|
||||||
|
|
||||||
|
$docblock = substr($docblock, 3, -2);
|
||||||
|
if (preg_match_all('/@' . $name . '(?:\s*(?:\(\s*)?(.*?)(?:\s*\))?)??\s*(?:\n|\*\/)/', $docblock, $matches)) {
|
||||||
|
foreach ($matches[1] as $k => $v) {
|
||||||
|
$annotations[] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse annotations
|
||||||
|
*
|
||||||
|
* @param string $docblock
|
||||||
|
* @return array parsed annotations params
|
||||||
|
*/
|
||||||
|
private static function parseAnnotations($docblock)
|
||||||
|
{
|
||||||
|
$annotations = array();
|
||||||
|
|
||||||
|
// Strip away the docblock header and footer to ease parsing of one line annotations
|
||||||
|
$docblock = substr($docblock, 3, -2);
|
||||||
|
if (preg_match_all('/@(?<name>[A-Za-z_-]+)[\s\t]*\((?<args>(?:(?!\)).)*)\)\r?/s', $docblock, $matches)) {
|
||||||
|
$numMatches = count($matches[0]);
|
||||||
|
for ($i = 0; $i < $numMatches; ++$i) {
|
||||||
|
$name = $matches['name'][$i];
|
||||||
|
$value = '';
|
||||||
|
// annotations has arguments
|
||||||
|
if (isset($matches['args'][$i])) {
|
||||||
|
$argsParts = trim($matches['args'][$i]);
|
||||||
|
if ($name == 'ApiReturn') {
|
||||||
|
$value = $argsParts;
|
||||||
|
} elseif ($matches['args'][$i] != '') {
|
||||||
|
$argsParts = preg_replace("/\{(\w+)\}/", '#$1#', $argsParts);
|
||||||
|
$value = self::parseArgs($argsParts);
|
||||||
|
if (is_string($value)) {
|
||||||
|
$value = preg_replace("/\#(\w+)\#/", '{$1}', $argsParts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$annotations[$name][] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stripos($docblock, '@ApiInternal') !== false) {
|
||||||
|
$annotations['ApiInternal'] = [true];
|
||||||
|
}
|
||||||
|
if (!isset($annotations['ApiTitle'])) {
|
||||||
|
preg_match_all("/\*[\s]+(.*)(\\r\\n|\\r|\\n)/U", str_replace('/**', '', $docblock), $matchArr);
|
||||||
|
$title = isset($matchArr[1]) && isset($matchArr[1][0]) ? $matchArr[1][0] : '';
|
||||||
|
$annotations['ApiTitle'] = [$title];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse individual annotation arguments
|
||||||
|
*
|
||||||
|
* @param string $content arguments string
|
||||||
|
* @return array annotated arguments
|
||||||
|
*/
|
||||||
|
private static function parseArgs($content)
|
||||||
|
{
|
||||||
|
// Replace initial stars
|
||||||
|
$content = preg_replace('/^\s*\*/m', '', $content);
|
||||||
|
|
||||||
|
$data = array();
|
||||||
|
$len = strlen($content);
|
||||||
|
$i = 0;
|
||||||
|
$var = '';
|
||||||
|
$val = '';
|
||||||
|
$level = 1;
|
||||||
|
|
||||||
|
$prevDelimiter = '';
|
||||||
|
$nextDelimiter = '';
|
||||||
|
$nextToken = '';
|
||||||
|
$composing = false;
|
||||||
|
$type = 'plain';
|
||||||
|
$delimiter = null;
|
||||||
|
$quoted = false;
|
||||||
|
$tokens = array('"', '"', '{', '}', ',', '=');
|
||||||
|
|
||||||
|
while ($i <= $len) {
|
||||||
|
$prev_c = substr($content, $i - 1, 1);
|
||||||
|
$c = substr($content, $i++, 1);
|
||||||
|
|
||||||
|
if ($c === '"' && $prev_c !== "\\") {
|
||||||
|
$delimiter = $c;
|
||||||
|
//open delimiter
|
||||||
|
if (!$composing && empty($prevDelimiter) && empty($nextDelimiter)) {
|
||||||
|
$prevDelimiter = $nextDelimiter = $delimiter;
|
||||||
|
$val = '';
|
||||||
|
$composing = true;
|
||||||
|
$quoted = true;
|
||||||
|
} else {
|
||||||
|
// close delimiter
|
||||||
|
if ($c !== $nextDelimiter) {
|
||||||
|
throw new Exception(sprintf(
|
||||||
|
"Parse Error: enclosing error -> expected: [%s], given: [%s]",
|
||||||
|
$nextDelimiter,
|
||||||
|
$c
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// validating syntax
|
||||||
|
if ($i < $len) {
|
||||||
|
if (',' !== substr($content, $i, 1) && '\\' !== $prev_c) {
|
||||||
|
throw new Exception(sprintf(
|
||||||
|
"Parse Error: missing comma separator near: ...%s<--",
|
||||||
|
substr($content, ($i - 10), $i)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$prevDelimiter = $nextDelimiter = '';
|
||||||
|
$composing = false;
|
||||||
|
$delimiter = null;
|
||||||
|
}
|
||||||
|
} elseif (!$composing && in_array($c, $tokens)) {
|
||||||
|
switch ($c) {
|
||||||
|
case '=':
|
||||||
|
$prevDelimiter = $nextDelimiter = '';
|
||||||
|
$level = 2;
|
||||||
|
$composing = false;
|
||||||
|
$type = 'assoc';
|
||||||
|
$quoted = false;
|
||||||
|
break;
|
||||||
|
case ',':
|
||||||
|
$level = 3;
|
||||||
|
|
||||||
|
// If composing flag is true yet,
|
||||||
|
// it means that the string was not enclosed, so it is parsing error.
|
||||||
|
if ($composing === true && !empty($prevDelimiter) && !empty($nextDelimiter)) {
|
||||||
|
throw new Exception(sprintf(
|
||||||
|
"Parse Error: enclosing error -> expected: [%s], given: [%s]",
|
||||||
|
$nextDelimiter,
|
||||||
|
$c
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$prevDelimiter = $nextDelimiter = '';
|
||||||
|
break;
|
||||||
|
case '{':
|
||||||
|
$subc = '';
|
||||||
|
$subComposing = true;
|
||||||
|
|
||||||
|
while ($i <= $len) {
|
||||||
|
$c = substr($content, $i++, 1);
|
||||||
|
|
||||||
|
if (isset($delimiter) && $c === $delimiter) {
|
||||||
|
throw new Exception(sprintf(
|
||||||
|
"Parse Error: Composite variable is not enclosed correctly."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($c === '}') {
|
||||||
|
$subComposing = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$subc .= $c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the string is composing yet means that the structure of var. never was enclosed with '}'
|
||||||
|
if ($subComposing) {
|
||||||
|
throw new Exception(sprintf(
|
||||||
|
"Parse Error: Composite variable is not enclosed correctly. near: ...%s'",
|
||||||
|
$subc
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$val = self::parseArgs($subc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($level == 1) {
|
||||||
|
$var .= $c;
|
||||||
|
} elseif ($level == 2) {
|
||||||
|
$val .= $c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($level === 3 || $i === $len) {
|
||||||
|
if ($type == 'plain' && $i === $len) {
|
||||||
|
$data = self::castValue($var);
|
||||||
|
} else {
|
||||||
|
$data[trim($var)] = self::castValue($val, !$quoted);
|
||||||
|
}
|
||||||
|
|
||||||
|
$level = 1;
|
||||||
|
$var = $val = '';
|
||||||
|
$composing = false;
|
||||||
|
$quoted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try determinate the original type variable of a string
|
||||||
|
*
|
||||||
|
* @param string $val string containing possibles variables that can be cast to bool or int
|
||||||
|
* @param boolean $trim indicate if the value passed should be trimmed after to try cast
|
||||||
|
* @return mixed returns the value converted to original type if was possible
|
||||||
|
*/
|
||||||
|
private static function castValue($val, $trim = false)
|
||||||
|
{
|
||||||
|
if (is_array($val)) {
|
||||||
|
foreach ($val as $key => $value) {
|
||||||
|
$val[$key] = self::castValue($value);
|
||||||
|
}
|
||||||
|
} elseif (is_string($val)) {
|
||||||
|
if ($trim) {
|
||||||
|
$val = trim($val);
|
||||||
|
}
|
||||||
|
$val = stripslashes($val);
|
||||||
|
$tmp = strtolower($val);
|
||||||
|
|
||||||
|
if ($tmp === 'false' || $tmp === 'true') {
|
||||||
|
$val = $tmp === 'true';
|
||||||
|
} elseif (is_numeric($val)) {
|
||||||
|
return $val + 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $val;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,648 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<title>{$config.title}</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap Core CSS -->
|
||||||
|
<link href="{$config.cdnurl|default=''}/assets/libs/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Plugin CSS -->
|
||||||
|
<link href="{$config.cdnurl|default=''}/assets/libs/font-awesome/css/font-awesome.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
padding-top: 70px; margin-bottom: 15px;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
font-family: "Roboto", "SF Pro SC", "SF Pro Display", "SF Pro Icons", "PingFang SC", BlinkMacSystemFont, -apple-system, "Segoe UI", "Microsoft Yahei", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
h2 { font-size: 1.2em; }
|
||||||
|
hr { margin-top: 10px; }
|
||||||
|
.tab-pane { padding-top: 10px; }
|
||||||
|
.mt0 { margin-top: 0px; }
|
||||||
|
.footer { font-size: 12px; color: #666; }
|
||||||
|
.docs-list .label { display: inline-block; min-width: 65px; padding: 0.3em 0.6em 0.3em; }
|
||||||
|
.string { color: green; }
|
||||||
|
.number { color: darkorange; }
|
||||||
|
.boolean { color: blue; }
|
||||||
|
.null { color: magenta; }
|
||||||
|
.key { color: red; }
|
||||||
|
.popover { max-width: 400px; max-height: 400px; overflow-y: auto;}
|
||||||
|
.list-group.panel > .list-group-item {
|
||||||
|
}
|
||||||
|
.list-group-item:last-child {
|
||||||
|
border-radius:0;
|
||||||
|
}
|
||||||
|
h4.panel-title a {
|
||||||
|
font-weight:normal;
|
||||||
|
font-size:14px;
|
||||||
|
}
|
||||||
|
h4.panel-title a .text-muted {
|
||||||
|
font-size:12px;
|
||||||
|
font-weight:normal;
|
||||||
|
font-family: 'Verdana';
|
||||||
|
}
|
||||||
|
#sidebar {
|
||||||
|
width: 220px;
|
||||||
|
position: fixed;
|
||||||
|
margin-left: -240px;
|
||||||
|
overflow-y:auto;
|
||||||
|
}
|
||||||
|
#sidebar > .list-group {
|
||||||
|
margin-bottom:0;
|
||||||
|
}
|
||||||
|
#sidebar > .list-group > a{
|
||||||
|
text-indent:0;
|
||||||
|
}
|
||||||
|
#sidebar .child > a .tag{
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 11px;
|
||||||
|
}
|
||||||
|
#sidebar .child > a .pull-right{
|
||||||
|
margin-left:3px;
|
||||||
|
}
|
||||||
|
#sidebar .child {
|
||||||
|
border:1px solid #ddd;
|
||||||
|
border-bottom:none;
|
||||||
|
}
|
||||||
|
#sidebar .child:last-child {
|
||||||
|
border-bottom:1px solid #ddd;
|
||||||
|
}
|
||||||
|
#sidebar .child > a {
|
||||||
|
border:0;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
#sidebar .list-group a.current {
|
||||||
|
background:#f5f5f5;
|
||||||
|
}
|
||||||
|
@media (max-width: 1620px){
|
||||||
|
#sidebar {
|
||||||
|
margin:0;
|
||||||
|
}
|
||||||
|
#accordion {
|
||||||
|
padding-left:235px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 768px){
|
||||||
|
#sidebar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#accordion {
|
||||||
|
padding-left:0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.label-primary {
|
||||||
|
background-color: #248aff;
|
||||||
|
}
|
||||||
|
.docs-list .panel .panel-body .table {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Fixed navbar -->
|
||||||
|
<div class="navbar navbar-default navbar-fixed-top" role="navigation">
|
||||||
|
<div class="container">
|
||||||
|
<div class="navbar-header">
|
||||||
|
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
|
||||||
|
<span class="sr-only">Toggle navigation</span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
</button>
|
||||||
|
<a class="navbar-brand" href="./" target="_blank">{$config.title}</a>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-collapse collapse">
|
||||||
|
<form class="navbar-form navbar-right">
|
||||||
|
<div class="form-group">
|
||||||
|
Token:
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" class="form-control input-sm" data-toggle="tooltip" title="{$lang.Tokentips}" placeholder="token" id="token" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
Apiurl:
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input id="apiUrl" type="text" class="form-control input-sm" data-toggle="tooltip" title="{$lang.Apiurltips}" placeholder="https://api.example.com" value="{$config.apiurl}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="button" class="btn btn-success btn-sm" data-toggle="tooltip" title="{$lang.Savetips}" id="save_data">
|
||||||
|
<span class="glyphicon glyphicon-floppy-disk" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div><!--/.nav-collapse -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<!-- menu -->
|
||||||
|
<div id="sidebar">
|
||||||
|
<div class="list-group panel">
|
||||||
|
{foreach name="docsList" id="docs"}
|
||||||
|
<a href="#{$key|md5|substr=0,8}" class="list-group-item" data-toggle="collapse" data-parent="#sidebar">{$key} <i class="fa fa-caret-down"></i></a>
|
||||||
|
<div class="child collapse" id="{$key|md5|substr=0,8}">
|
||||||
|
{foreach name="docs" id="api" }
|
||||||
|
<a href="javascript:;" data-id="{$api.id}" class="list-group-item">{$api.title}
|
||||||
|
<span class="tag">
|
||||||
|
{if $api.needRight}
|
||||||
|
<span class="label label-danger pull-right">鉴</span>
|
||||||
|
{/if}
|
||||||
|
{if $api.needLogin}
|
||||||
|
<span class="label label-success pull-right noneedlogin">登</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{/foreach}
|
||||||
|
</div>
|
||||||
|
{/foreach}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-group docs-list" id="accordion">
|
||||||
|
{foreach name="docsList" id="docs"}
|
||||||
|
<h2>{$key}</h2>
|
||||||
|
<hr>
|
||||||
|
{foreach name="docs" id="api" }
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading" id="heading-{$api.id}">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<span class="label {$api.methodLabel}">{$api.method|strtoupper}</span>
|
||||||
|
<a data-toggle="collapse" data-parent="#accordion{$api.id}" href="#collapseOne{$api.id}"> {$api.title} <span class="text-muted">{$api.route}</span></a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="collapseOne{$api.id}" class="panel-collapse collapse">
|
||||||
|
<div class="panel-body">
|
||||||
|
|
||||||
|
<!-- Nav tabs -->
|
||||||
|
<ul class="nav nav-tabs" id="doctab{$api.id}">
|
||||||
|
<li class="active"><a href="#info{$api.id}" data-toggle="tab">{$lang.Info}</a></li>
|
||||||
|
<li><a href="#sandbox{$api.id}" data-toggle="tab">{$lang.Sandbox}</a></li>
|
||||||
|
<li><a href="#sample{$api.id}" data-toggle="tab">{$lang.Sampleoutput}</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Tab panes -->
|
||||||
|
<div class="tab-content">
|
||||||
|
|
||||||
|
<div class="tab-pane active" id="info{$api.id}">
|
||||||
|
<div class="well">
|
||||||
|
{$api.summary}
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>{$lang.Authorization}</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{$lang.NeedLogin}</td>
|
||||||
|
<td>{$api.needLogin?'是':'否'}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{$lang.NeedRight}</td>
|
||||||
|
<td>{$api.needRight?'是':'否'}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>{$lang.Headers}</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{if $api.headersList}
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{$lang.Name}</th>
|
||||||
|
<th>{$lang.Type}</th>
|
||||||
|
<th>{$lang.Required}</th>
|
||||||
|
<th>{$lang.Description}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{foreach name="api['headersList']" id="header"}
|
||||||
|
<tr>
|
||||||
|
<td>{$header.name}</td>
|
||||||
|
<td>{$header.type}</td>
|
||||||
|
<td>{$header.required?'是':'否'}</td>
|
||||||
|
<td>{$header.description}</td>
|
||||||
|
</tr>
|
||||||
|
{/foreach}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{else /}
|
||||||
|
无
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>{$lang.Parameters}</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{if $api.paramsList}
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{$lang.Name}</th>
|
||||||
|
<th>{$lang.Type}</th>
|
||||||
|
<th>{$lang.Required}</th>
|
||||||
|
<th>{$lang.Description}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{foreach name="api['paramsList']" id="param"}
|
||||||
|
<tr>
|
||||||
|
<td>{$param.name}</td>
|
||||||
|
<td>{$param.type}</td>
|
||||||
|
<td>{:$param.required?'是':'否'}</td>
|
||||||
|
<td>{$param.description}</td>
|
||||||
|
</tr>
|
||||||
|
{/foreach}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{else /}
|
||||||
|
无
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>{$lang.Body}</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{$api.body|default='无'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><!-- #info -->
|
||||||
|
|
||||||
|
<div class="tab-pane" id="sandbox{$api.id}">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
{if $api.headersList}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>{$lang.Headers}</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="headers">
|
||||||
|
{foreach name="api['headersList']" id="param"}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label" for="{$param.name}">{$param.name}</label>
|
||||||
|
<input type="{$param.inputtype|default='text'}" class="form-control input-sm" id="{$param.name}" {if $param.required}required{/if} placeholder="{$param.description} - Ex: {$param.sample}" name="{$param.name}">
|
||||||
|
</div>
|
||||||
|
{/foreach}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>{$lang.Parameters}</strong>
|
||||||
|
<div class="pull-right">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-info btn-append">追加</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<form enctype="application/x-www-form-urlencoded" role="form" action="{$api.route}" method="{$api.method}" name="form{$api.id}" id="form{$api.id}">
|
||||||
|
{if $api.paramsList}
|
||||||
|
{foreach name="api['paramsList']" id="param"}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label" for="{$param.name}">{$param.name}</label>
|
||||||
|
<input type="{$param.inputtype|default='text'}" class="form-control input-sm" id="{$param.name}" {if $param.required}required{/if} placeholder="{$param.description}{if $param.sample} - 例: {$param.sample}{/if}" name="{$param.name}">
|
||||||
|
</div>
|
||||||
|
{/foreach}
|
||||||
|
{else /}
|
||||||
|
<div class="form-group">
|
||||||
|
无
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="form-group form-group-submit">
|
||||||
|
<button type="submit" class="btn btn-success send" rel="{$api.id}">{$lang.Send}</button>
|
||||||
|
<button type="reset" class="btn btn-info" rel="{$api.id}">{$lang.Reset}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>{$lang.Response}</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12" style="overflow-x:auto">
|
||||||
|
<pre id="response_headers{$api.id}"></pre>
|
||||||
|
<pre id="response{$api.id}"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>{$lang.ReturnParameters}</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{if $api.returnParamsList}
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{$lang.Name}</th>
|
||||||
|
<th>{$lang.Type}</th>
|
||||||
|
<th>{$lang.Description}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{foreach name="api['returnParamsList']" id="param"}
|
||||||
|
<tr>
|
||||||
|
<td>{$param.name}</td>
|
||||||
|
<td>{$param.type}</td>
|
||||||
|
<td>{$param.description}</td>
|
||||||
|
</tr>
|
||||||
|
{/foreach}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{else /}
|
||||||
|
无
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><!-- #sandbox -->
|
||||||
|
|
||||||
|
<div class="tab-pane" id="sample{$api.id}">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<pre id="sample_response{$api.id}">{$api.return|default='无'}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><!-- #sample -->
|
||||||
|
|
||||||
|
</div><!-- .tab-content -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/foreach}
|
||||||
|
{/foreach}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="row mt0 footer">
|
||||||
|
<div class="col-md-6" align="left">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6" align="right">
|
||||||
|
Generated on {:date('Y-m-d H:i:s')} <a href="./" target="_blank">{$config.sitename}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div> <!-- /container -->
|
||||||
|
|
||||||
|
<!-- jQuery -->
|
||||||
|
<script src="{$config.cdnurl|default=''}/assets/libs/jquery/dist/jquery.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Bootstrap Core JavaScript -->
|
||||||
|
<script src="{$config.cdnurl|default=''}/assets/libs/bootstrap/dist/js/bootstrap.min.js"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
function syntaxHighlight(json) {
|
||||||
|
if (typeof json != 'string') {
|
||||||
|
json = JSON.stringify(json, undefined, 2);
|
||||||
|
}
|
||||||
|
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
|
||||||
|
var cls = 'number';
|
||||||
|
if (/^"/.test(match)) {
|
||||||
|
if (/:$/.test(match)) {
|
||||||
|
cls = 'key';
|
||||||
|
} else {
|
||||||
|
cls = 'string';
|
||||||
|
}
|
||||||
|
} else if (/true|false/.test(match)) {
|
||||||
|
cls = 'boolean';
|
||||||
|
} else if (/null/.test(match)) {
|
||||||
|
cls = 'null';
|
||||||
|
}
|
||||||
|
return '<span class="' + cls + '">' + match + '</span>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareStr(str) {
|
||||||
|
try {
|
||||||
|
return syntaxHighlight(JSON.stringify(JSON.parse(str.replace(/'/g, '"')), null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var storage = (function () {
|
||||||
|
var uid = new Date;
|
||||||
|
var storage;
|
||||||
|
var result;
|
||||||
|
try {
|
||||||
|
(storage = window.localStorage).setItem(uid, uid);
|
||||||
|
result = storage.getItem(uid) == uid;
|
||||||
|
storage.removeItem(uid);
|
||||||
|
return result && storage;
|
||||||
|
} catch (exception) {
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
|
||||||
|
$.fn.serializeObject = function ()
|
||||||
|
{
|
||||||
|
var o = {};
|
||||||
|
var a = this.serializeArray();
|
||||||
|
$.each(a, function () {
|
||||||
|
if (!this.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (o[this.name] !== undefined) {
|
||||||
|
if (!o[this.name].push) {
|
||||||
|
o[this.name] = [o[this.name]];
|
||||||
|
}
|
||||||
|
o[this.name].push(this.value || '');
|
||||||
|
} else {
|
||||||
|
o[this.name] = this.value || '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return o;
|
||||||
|
};
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
|
||||||
|
if (storage) {
|
||||||
|
storage.getItem('token') && $('#token').val(storage.getItem('token'));
|
||||||
|
storage.getItem('apiUrl') && $('#apiUrl').val(storage.getItem('apiUrl'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$('[data-toggle="tooltip"]').tooltip({
|
||||||
|
placement: 'bottom'
|
||||||
|
});
|
||||||
|
|
||||||
|
$(window).on("resize", function(){
|
||||||
|
$("#sidebar").css("max-height", $(window).height()-80);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(window).trigger("resize");
|
||||||
|
|
||||||
|
$(document).on("click", "#sidebar .list-group > .list-group-item", function(){
|
||||||
|
$("#sidebar .list-group > .list-group-item").removeClass("current");
|
||||||
|
$(this).addClass("current");
|
||||||
|
});
|
||||||
|
$(document).on("click", "#sidebar .child a", function(){
|
||||||
|
var heading = $("#heading-"+$(this).data("id"));
|
||||||
|
if(!heading.next().hasClass("in")){
|
||||||
|
$("a", heading).trigger("click");
|
||||||
|
}
|
||||||
|
$("html,body").animate({scrollTop:heading.offset().top-70});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('code[id^=response]').hide();
|
||||||
|
|
||||||
|
$.each($('pre[id^=sample_response],pre[id^=sample_post_body]'), function () {
|
||||||
|
if ($(this).html() == 'NA') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var str = prepareStr($(this).html());
|
||||||
|
$(this).html(str);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("[data-toggle=popover]").popover({placement: 'right'});
|
||||||
|
|
||||||
|
$('[data-toggle=popover]').on('shown.bs.popover', function () {
|
||||||
|
var $sample = $(this).parent().find(".popover-content"),
|
||||||
|
str = $(this).data('content');
|
||||||
|
if (typeof str == "undefined" || str === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var str = prepareStr(str);
|
||||||
|
$sample.html('<pre>' + str + '</pre>');
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '#save_data', function (e) {
|
||||||
|
if (storage) {
|
||||||
|
storage.setItem('token', $('#token').val());
|
||||||
|
storage.setItem('apiUrl', $('#apiUrl').val());
|
||||||
|
} else {
|
||||||
|
alert('Your browser does not support local storage');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$(document).on('click', '.btn-append', function (e) {
|
||||||
|
$($("#appendtpl").html()).insertBefore($(this).closest(".panel").find(".form-group-submit"));
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
$(document).on('click', '.btn-remove', function (e) {
|
||||||
|
$(this).closest(".form-group").remove();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
$(document).on('keyup', '.input-custom-name', function (e) {
|
||||||
|
$(this).closest(".row").find(".input-custom-value").attr("name", $(this).val());
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.send', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var form = $(this).closest('form');
|
||||||
|
//added /g to get all the matched params instead of only first
|
||||||
|
var matchedParamsInRoute = $(form).attr('action').match(/[^{]+(?=\})/g);
|
||||||
|
var theId = $(this).attr('rel');
|
||||||
|
//keep a copy of action attribute in order to modify the copy
|
||||||
|
//instead of the initial attribute
|
||||||
|
var url = $(form).attr('action');
|
||||||
|
var method = $(form).prop('method').toLowerCase() || 'get';
|
||||||
|
|
||||||
|
var formData = new FormData();
|
||||||
|
|
||||||
|
$(form).find('input').each(function (i, input) {
|
||||||
|
if ($(input).attr('type').toLowerCase() == 'file') {
|
||||||
|
formData.append($(input).attr('name'), $(input)[0].files[0]);
|
||||||
|
method = 'post';
|
||||||
|
} else {
|
||||||
|
formData.append($(input).attr('name'), $(input).val())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var index, key, value;
|
||||||
|
|
||||||
|
if (matchedParamsInRoute) {
|
||||||
|
var params = {};
|
||||||
|
formData.forEach(function(value, key){
|
||||||
|
params[key] = value;
|
||||||
|
});
|
||||||
|
for (index = 0; index < matchedParamsInRoute.length; ++index) {
|
||||||
|
try {
|
||||||
|
key = matchedParamsInRoute[index];
|
||||||
|
value = params[key];
|
||||||
|
if (typeof value == "undefined")
|
||||||
|
value = "";
|
||||||
|
url = url.replace("\{" + key + "\}", value);
|
||||||
|
formData.delete(key);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var headers = {};
|
||||||
|
|
||||||
|
var token = $('#token').val();
|
||||||
|
if (token.length > 0) {
|
||||||
|
headers['token'] = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#sandbox" + theId + " .headers input[type=text]").each(function () {
|
||||||
|
val = $(this).val();
|
||||||
|
if (val.length > 0) {
|
||||||
|
headers[$(this).prop('name')] = val;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: $('#apiUrl').val() + url,
|
||||||
|
data: method == 'get' ? $(form).serialize() : formData,
|
||||||
|
type: method,
|
||||||
|
dataType: 'json',
|
||||||
|
contentType: false,
|
||||||
|
processData: false,
|
||||||
|
headers: headers,
|
||||||
|
xhrFields: {
|
||||||
|
withCredentials: true
|
||||||
|
},
|
||||||
|
success: function (data, textStatus, xhr) {
|
||||||
|
if (typeof data === 'object') {
|
||||||
|
var str = JSON.stringify(data, null, 2);
|
||||||
|
$('#response' + theId).html(syntaxHighlight(str));
|
||||||
|
} else {
|
||||||
|
$('#response' + theId).html(data || '');
|
||||||
|
}
|
||||||
|
$('#response_headers' + theId).html('HTTP ' + xhr.status + ' ' + xhr.statusText + '<br/><br/>' + xhr.getAllResponseHeaders());
|
||||||
|
$('#response' + theId).show();
|
||||||
|
},
|
||||||
|
error: function (xhr, textStatus, error) {
|
||||||
|
try {
|
||||||
|
var str = JSON.stringify($.parseJSON(xhr.responseText), null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
var str = xhr.responseText;
|
||||||
|
}
|
||||||
|
$('#response_headers' + theId).html('HTTP ' + xhr.status + ' ' + xhr.statusText + '<br/><br/>' + xhr.getAllResponseHeaders());
|
||||||
|
$('#response' + theId).html(syntaxHighlight(str));
|
||||||
|
$('#response' + theId).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script type="text/html" id="appendtpl">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label">自定义</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-4">
|
||||||
|
<input type="text" class="form-control input-sm input-custom-name" placeholder="名称">
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-6">
|
||||||
|
<input type="text" class="form-control input-sm input-custom-value" placeholder="值">
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-2 text-center">
|
||||||
|
<a href="javascript:" class="btn btn-sm btn-danger btn-remove">删除</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
|||||||
|
<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
|
||||||
|
|
||||||
|
{%addList%}
|
||||||
|
<div class="form-group layer-footer">
|
||||||
|
<label class="control-label col-xs-12 col-sm-2"></label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace {%controllerNamespace%};
|
||||||
|
|
||||||
|
use app\common\controller\Backend;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {%tableComment%}
|
||||||
|
*
|
||||||
|
* @icon {%iconName%}
|
||||||
|
*/
|
||||||
|
class {%controllerName%} extends Backend
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {%modelName%}模型对象
|
||||||
|
* @var \{%modelNamespace%}\{%modelName%}
|
||||||
|
*/
|
||||||
|
protected $model = null;
|
||||||
|
|
||||||
|
public function _initialize()
|
||||||
|
{
|
||||||
|
parent::_initialize();
|
||||||
|
$this->model = new \{%modelNamespace%}\{%modelName%};
|
||||||
|
{%controllerAssignList%}
|
||||||
|
}
|
||||||
|
|
||||||
|
{%controllerImport%}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认生成的控制器所继承的父类中有index/add/edit/del/multi五个基础方法、destroy/restore/recyclebin三个回收站方法
|
||||||
|
* 因此在当前控制器中可不用编写增删改查的代码,除非需要自己控制这部分逻辑
|
||||||
|
* 需要将application/admin/library/traits/Backend.php中对应的方法复制到当前控制器,然后进行修改
|
||||||
|
*/
|
||||||
|
|
||||||
|
{%controllerIndex%}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* 查看
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
//当前是否为关联查询
|
||||||
|
$this->relationSearch = {%relationSearch%};
|
||||||
|
//设置过滤方法
|
||||||
|
$this->request->filter(['strip_tags', 'trim']);
|
||||||
|
if ($this->request->isAjax()) {
|
||||||
|
//如果发送的来源是Selectpage,则转发到Selectpage
|
||||||
|
if ($this->request->request('keyField')) {
|
||||||
|
return $this->selectpage();
|
||||||
|
}
|
||||||
|
list($where, $sort, $order, $offset, $limit) = $this->buildparams();
|
||||||
|
|
||||||
|
$list = $this->model
|
||||||
|
{%relationWithList%}
|
||||||
|
->where($where)
|
||||||
|
->order($sort, $order)
|
||||||
|
->paginate($limit);
|
||||||
|
|
||||||
|
foreach ($list as $row) {
|
||||||
|
{%visibleFieldList%}
|
||||||
|
{%relationVisibleFieldList%}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = array("total" => $list->total(), "rows" => $list->items());
|
||||||
|
|
||||||
|
return json($result);
|
||||||
|
}
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
|
||||||
|
|
||||||
|
{%editList%}
|
||||||
|
<div class="form-group layer-footer">
|
||||||
|
<label class="control-label col-xs-12 col-sm-2"></label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
<div class="checkbox">
|
||||||
|
{foreach name="{%fieldList%}" item="vo"}
|
||||||
|
<label for="{%fieldName%}-{$key}"><input id="{%fieldName%}-{$key}" name="{%fieldName%}" type="checkbox" value="{$key}" {in name="key" value="{%selectedValue%}"}checked{/in} /> {$vo}</label>
|
||||||
|
{/foreach}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
<dl class="list-unstyled fieldlist" data-name="{%fieldName%}" data-template="{%fieldName%}tpl">
|
||||||
|
<dd>
|
||||||
|
<ins>{:__('{%itemValue%}')}</ins>
|
||||||
|
</dd>
|
||||||
|
<dd>
|
||||||
|
<ins><a href="javascript:;" class="btn btn-sm btn-success btn-append"><i class="fa fa-plus"></i> {:__('Append')}</a></ins>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<textarea name="{%fieldName%}" class="form-control hide" cols="30" rows="5">{%fieldValue%}</textarea>
|
||||||
|
<script id="{%fieldName%}tpl" type="text/html">
|
||||||
|
<dd class="form-inline">
|
||||||
|
<ins><input type="text" name="<%=name%>[<%=index%>][value]" class="form-control" size="15" value="<%=row%>"/></ins>
|
||||||
|
<ins>
|
||||||
|
<span class="btn btn-sm btn-danger btn-remove"><i class="fa fa-times"></i></span>
|
||||||
|
<span class="btn btn-sm btn-primary btn-dragsort"><i class="fa fa-arrows"></i></span>
|
||||||
|
</ins>
|
||||||
|
</dd>
|
||||||
|
</script>
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
<table class="table fieldlist" data-name="{%fieldName%}" data-template="{%fieldName%}tpl">
|
||||||
|
<tr>
|
||||||
|
{%theadList%}
|
||||||
|
<td width="90">{:__('Operate')}</td>
|
||||||
|
</tr>
|
||||||
|
<tr><td colspan="{%colspan%}">
|
||||||
|
<a href="javascript:;" class="btn btn-sm btn-success btn-append"><i class="fa fa-plus"></i> {:__('Append')}</a>
|
||||||
|
<textarea name="{%fieldName%}" class="form-control hide" cols="30" rows="5">{%fieldValue%}</textarea>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
<script type="text/html" id="{%fieldName%}tpl">
|
||||||
|
<tr>
|
||||||
|
{%tbodyList%}
|
||||||
|
<td width="90">
|
||||||
|
<span class="btn btn-sm btn-danger btn-remove"><i class="fa fa-times"></i></span>
|
||||||
|
<span class="btn btn-sm btn-primary btn-dragsort"><i class="fa fa-arrows"></i></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
<dl class="fieldlist" data-name="{%fieldName%}">
|
||||||
|
<dd>
|
||||||
|
<ins>{:__('{%itemKey%}')}</ins>
|
||||||
|
<ins>{:__('{%itemValue%}')}</ins>
|
||||||
|
</dd>
|
||||||
|
<dd><a href="javascript:;" class="btn btn-sm btn-success btn-append"><i class="fa fa-plus"></i> {:__('Append')}</a></dd>
|
||||||
|
<textarea name="{%fieldName%}" class="form-control hide" cols="30" rows="5">{%fieldValue%}</textarea>
|
||||||
|
</dl>
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
<div class="panel-heading">
|
||||||
|
{:build_heading(null,FALSE)}
|
||||||
|
<ul class="nav nav-tabs" data-field="{%field%}">
|
||||||
|
<li class="{:$Think.get.{%field%} === null ? 'active' : ''}"><a href="#t-all" data-value="" data-toggle="tab">{:__('All')}</a></li>
|
||||||
|
{foreach name="{%fieldName%}List" item="vo"}
|
||||||
|
<li class="{:$Think.get.{%field%} === (string)$key ? 'active' : ''}"><a href="#t-{$key}" data-value="{$key}" data-toggle="tab">{$vo}</a></li>
|
||||||
|
{/foreach}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<div class="dropdown btn-group {:$auth->check('{%controllerUrl%}/multi')?'':'hide'}">
|
||||||
|
<a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown"><i class="fa fa-cog"></i> {:__('More')}</a>
|
||||||
|
<ul class="dropdown-menu text-left" role="menu">
|
||||||
|
{foreach name="{%fieldName%}List" item="vo"}
|
||||||
|
<li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:" data-params="{%field%}={$key}">{:__('Set {%field%} to ' . $key)}</a></li>
|
||||||
|
{/foreach}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
<div class="radio">
|
||||||
|
{foreach name="{%fieldList%}" item="vo"}
|
||||||
|
<label for="{%fieldName%}-{$key}"><input id="{%fieldName%}-{$key}" name="{%fieldName%}" type="radio" value="{$key}" {in name="key" value="{%selectedValue%}"}checked{/in} /> {$vo}</label>
|
||||||
|
{/foreach}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<a class="btn btn-success btn-recyclebin btn-dialog {:$auth->check('{%controllerUrl%}/recyclebin')?'':'hide'}" href="{%controllerUrl%}/recyclebin" title="{:__('Recycle bin')}"><i class="fa fa-recycle"></i> {:__('Recycle bin')}</a>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
<select {%attrStr%}>
|
||||||
|
{foreach name="{%fieldList%}" item="vo"}
|
||||||
|
<option value="{$key}" {in name="key" value="{%selectedValue%}"}selected{/in}>{$vo}</option>
|
||||||
|
{/foreach}
|
||||||
|
</select>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
<input {%attrStr%} name="{%fieldName%}" type="hidden" value="{%fieldValue%}">
|
||||||
|
<a href="javascript:;" data-toggle="switcher" class="btn-switcher" data-input-id="c-{%field%}" data-yes="{%fieldYes%}" data-no="{%fieldNo%}" >
|
||||||
|
<i class="fa fa-toggle-on text-success {%fieldSwitchClass%} fa-2x"></i>
|
||||||
|
</a>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<div class="panel panel-default panel-intro">
|
||||||
|
{%headingHtml%}
|
||||||
|
|
||||||
|
<div class="panel-body">
|
||||||
|
<div id="myTabContent" class="tab-content">
|
||||||
|
<div class="tab-pane fade active in" id="one">
|
||||||
|
<div class="widget-body no-padding">
|
||||||
|
<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 {:$auth->check('{%controllerUrl%}/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>
|
||||||
|
<a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('{%controllerUrl%}/edit')?'':'hide'}" title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>
|
||||||
|
<a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('{%controllerUrl%}/del')?'':'hide'}" title="{:__('Delete')}" ><i class="fa fa-trash"></i> {:__('Delete')}</a>
|
||||||
|
{%importHtml%}
|
||||||
|
|
||||||
|
{%multipleHtml%}
|
||||||
|
|
||||||
|
{%recyclebinHtml%}
|
||||||
|
</div>
|
||||||
|
<table id="table" class="table table-striped table-bordered table-hover table-nowrap"
|
||||||
|
data-operate-edit="{:$auth->check('{%controllerUrl%}/edit')}"
|
||||||
|
data-operate-del="{:$auth->check('{%controllerUrl%}/del')}"
|
||||||
|
width="100%">
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
|
||||||
|
|
||||||
|
var Controller = {
|
||||||
|
index: function () {
|
||||||
|
// 初始化表格参数配置
|
||||||
|
Table.api.init({
|
||||||
|
extend: {
|
||||||
|
index_url: '{%controllerUrl%}/index' + location.search,
|
||||||
|
add_url: '{%controllerUrl%}/add',
|
||||||
|
edit_url: '{%controllerUrl%}/edit',
|
||||||
|
del_url: '{%controllerUrl%}/del',
|
||||||
|
multi_url: '{%controllerUrl%}/multi',
|
||||||
|
import_url: '{%controllerUrl%}/import',
|
||||||
|
table: '{%table%}',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var table = $("#table");
|
||||||
|
|
||||||
|
// 初始化表格
|
||||||
|
table.bootstrapTable({
|
||||||
|
url: $.fn.bootstrapTable.defaults.extend.index_url,
|
||||||
|
pk: '{%pk%}',
|
||||||
|
sortName: '{%order%}',{%fixedColumnsJs%}
|
||||||
|
columns: [
|
||||||
|
[
|
||||||
|
{%javascriptList%}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 为表格绑定事件
|
||||||
|
Table.api.bindevent(table);
|
||||||
|
},{%recyclebinJs%}
|
||||||
|
add: function () {
|
||||||
|
Controller.api.bindevent();
|
||||||
|
},
|
||||||
|
edit: function () {
|
||||||
|
Controller.api.bindevent();
|
||||||
|
},
|
||||||
|
api: {
|
||||||
|
bindevent: function () {
|
||||||
|
Form.api.bindevent($("form[role=form]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return Controller;
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
{%langList%}
|
||||||
|
];
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
public function {%methodName%}($value, $data)
|
||||||
|
{
|
||||||
|
$value = $value ?: ($data['{%field%}'] ?? '');
|
||||||
|
$valueArr = explode(',', $value);
|
||||||
|
$list = $this->{%listMethodName%}();
|
||||||
|
return implode(',', array_intersect_key($list, array_flip($valueArr)));
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
public function {%methodName%}($value, $data)
|
||||||
|
{
|
||||||
|
$value = $value ?: ($data['{%field%}'] ?? '');
|
||||||
|
return is_numeric($value) ? date("Y-m-d H:i:s", $value) : $value;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
public function import()
|
||||||
|
{
|
||||||
|
parent::import();
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
protected static function init()
|
||||||
|
{
|
||||||
|
self::afterInsert(function ($row) {
|
||||||
|
if (!$row['{%order%}']) {
|
||||||
|
$pk = $row->getPk();
|
||||||
|
$row->getQuery()->where($pk, $row[$pk])->update(['{%order%}' => $row[$pk]]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
public function {%relationMethod%}s()
|
||||||
|
{
|
||||||
|
return $this->{%relationMode%}('{%relationClassName%}', '{%relationForeignKey%}', '{%relationPrimaryKey%}');
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
public function {%relationMethod%}()
|
||||||
|
{
|
||||||
|
return $this->{%relationMode%}('{%relationClassName%}', '{%relationForeignKey%}', '{%relationPrimaryKey%}', [], 'LEFT')->setEagerlyType(0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
public function {%methodName%}($value, $data)
|
||||||
|
{
|
||||||
|
$value = $value ?: ($data['{%field%}'] ?? '');
|
||||||
|
$valueArr = explode(',', $value);
|
||||||
|
$list = $this->{%listMethodName%}();
|
||||||
|
return implode(',', array_intersect_key($list, array_flip($valueArr)));
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
public function {%methodName%}($value, $data)
|
||||||
|
{
|
||||||
|
$value = $value ?: ($data['{%field%}'] ?? '');
|
||||||
|
$list = $this->{%listMethodName%}();
|
||||||
|
return $list[$value] ?? '';
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
|
||||||
|
recyclebin: function () {
|
||||||
|
// 初始化表格参数配置
|
||||||
|
Table.api.init({
|
||||||
|
extend: {
|
||||||
|
'dragsort_url': ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var table = $("#table");
|
||||||
|
|
||||||
|
// 初始化表格
|
||||||
|
table.bootstrapTable({
|
||||||
|
url: '{%controllerUrl%}/recyclebin' + location.search,
|
||||||
|
pk: 'id',
|
||||||
|
sortName: 'id',
|
||||||
|
columns: [
|
||||||
|
[
|
||||||
|
{checkbox: true},
|
||||||
|
{field: 'id', title: __('Id')},{%recyclebinTitleJs%}
|
||||||
|
{
|
||||||
|
field: '{%deleteTimeField%}',
|
||||||
|
title: __('Deletetime'),
|
||||||
|
operate: 'RANGE',
|
||||||
|
addclass: 'datetimerange',
|
||||||
|
formatter: Table.api.formatter.datetime
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'operate',
|
||||||
|
width: '140px',
|
||||||
|
title: __('Operate'),
|
||||||
|
table: table,
|
||||||
|
events: Table.api.events.operate,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
name: 'Restore',
|
||||||
|
text: __('Restore'),
|
||||||
|
classname: 'btn btn-xs btn-info btn-ajax btn-restoreit',
|
||||||
|
icon: 'fa fa-rotate-left',
|
||||||
|
url: '{%controllerUrl%}/restore',
|
||||||
|
refresh: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Destroy',
|
||||||
|
text: __('Destroy'),
|
||||||
|
classname: 'btn btn-xs btn-danger btn-ajax btn-destroyit',
|
||||||
|
icon: 'fa fa-times',
|
||||||
|
url: '{%controllerUrl%}/destroy',
|
||||||
|
refresh: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
formatter: Table.api.formatter.operate
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 为表格绑定事件
|
||||||
|
Table.api.bindevent(table);
|
||||||
|
},
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
public function {%methodName%}($value, $data)
|
||||||
|
{
|
||||||
|
$value = $value ?: ($data['{%field%}'] ?? '');
|
||||||
|
$list = $this->{%listMethodName%}();
|
||||||
|
return $list[$value] ?? '';
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace {%modelNamespace%};
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
{%softDeleteClassPath%}
|
||||||
|
|
||||||
|
class {%modelName%} extends Model
|
||||||
|
{
|
||||||
|
|
||||||
|
{%softDelete%}
|
||||||
|
|
||||||
|
{%modelConnection%}
|
||||||
|
|
||||||
|
// 表名
|
||||||
|
protected ${%modelTableType%} = '{%modelTableTypeName%}';
|
||||||
|
|
||||||
|
// 自动写入时间戳字段
|
||||||
|
protected $autoWriteTimestamp = {%modelAutoWriteTimestamp%};
|
||||||
|
|
||||||
|
// 定义时间戳字段名
|
||||||
|
protected $createTime = {%createTime%};
|
||||||
|
protected $updateTime = {%updateTime%};
|
||||||
|
protected $deleteTime = {%deleteTime%};
|
||||||
|
|
||||||
|
// 追加属性
|
||||||
|
protected $append = [
|
||||||
|
{%appendAttrList%}
|
||||||
|
];
|
||||||
|
|
||||||
|
{%modelInit%}
|
||||||
|
|
||||||
|
{%getEnumList%}
|
||||||
|
|
||||||
|
{%getAttrList%}
|
||||||
|
|
||||||
|
{%setAttrList%}
|
||||||
|
|
||||||
|
{%relationMethodList%}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<div class="panel panel-default panel-intro">
|
||||||
|
{:build_heading()}
|
||||||
|
|
||||||
|
<div class="panel-body">
|
||||||
|
<div id="myTabContent" class="tab-content">
|
||||||
|
<div class="tab-pane fade active in" id="one">
|
||||||
|
<div class="widget-body no-padding">
|
||||||
|
<div id="toolbar" class="toolbar">
|
||||||
|
{:build_toolbar('refresh')}
|
||||||
|
<a class="btn btn-info btn-multi btn-disabled disabled {:$auth->check('{%controllerUrl%}/restore')?'':'hide'}" href="javascript:;" data-url="{%controllerUrl%}/restore" data-action="restore"><i class="fa fa-rotate-left"></i> {:__('Restore')}</a>
|
||||||
|
<a class="btn btn-danger btn-multi btn-disabled disabled {:$auth->check('{%controllerUrl%}/destroy')?'':'hide'}" href="javascript:;" data-url="{%controllerUrl%}/destroy" data-action="destroy"><i class="fa fa-times"></i> {:__('Destroy')}</a>
|
||||||
|
<a class="btn btn-success btn-restoreall {:$auth->check('{%controllerUrl%}/restore')?'':'hide'}" href="javascript:;" data-url="{%controllerUrl%}/restore" title="{:__('Restore all')}"><i class="fa fa-rotate-left"></i> {:__('Restore all')}</a>
|
||||||
|
<a class="btn btn-danger btn-destroyall {:$auth->check('{%controllerUrl%}/destroy')?'':'hide'}" href="javascript:;" data-url="{%controllerUrl%}/destroy" title="{:__('Destroy all')}"><i class="fa fa-times"></i> {:__('Destroy all')}</a>
|
||||||
|
</div>
|
||||||
|
<table id="table" class="table table-striped table-bordered table-hover"
|
||||||
|
data-operate-restore="{:$auth->check('{%controllerUrl%}/restore')}"
|
||||||
|
data-operate-destroy="{:$auth->check('{%controllerUrl%}/destroy')}"
|
||||||
|
width="100%">
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace {%modelNamespace%};
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
class {%relationName%} extends Model
|
||||||
|
{
|
||||||
|
// 表名
|
||||||
|
protected ${%relationTableType%} = '{%relationTableTypeName%}';
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace {%validateNamespace%};
|
||||||
|
|
||||||
|
use think\Validate;
|
||||||
|
|
||||||
|
class {%validateName%} extends Validate
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 验证规则
|
||||||
|
*/
|
||||||
|
protected $rule = [
|
||||||
|
];
|
||||||
|
/**
|
||||||
|
* 提示消息
|
||||||
|
*/
|
||||||
|
protected $message = [
|
||||||
|
];
|
||||||
|
/**
|
||||||
|
* 验证场景
|
||||||
|
*/
|
||||||
|
protected $scene = [
|
||||||
|
'add' => [],
|
||||||
|
'edit' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,341 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\command;
|
||||||
|
|
||||||
|
use fast\Random;
|
||||||
|
use PDO;
|
||||||
|
use think\Config;
|
||||||
|
use think\console\Command;
|
||||||
|
use think\console\Input;
|
||||||
|
use think\console\input\Option;
|
||||||
|
use think\console\Output;
|
||||||
|
use think\Db;
|
||||||
|
use think\Exception;
|
||||||
|
use think\Lang;
|
||||||
|
use think\Request;
|
||||||
|
use think\View;
|
||||||
|
|
||||||
|
class Install extends Command
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最低PHP版本
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $minPhpVersion = '7.4.0';
|
||||||
|
|
||||||
|
protected $model = null;
|
||||||
|
/**
|
||||||
|
* @var \think\View 视图类实例
|
||||||
|
*/
|
||||||
|
protected $view;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \think\Request Request 实例
|
||||||
|
*/
|
||||||
|
protected $request;
|
||||||
|
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$config = Config::get('database');
|
||||||
|
$this
|
||||||
|
->setName('install')
|
||||||
|
->addOption('hostname', 'a', Option::VALUE_OPTIONAL, 'mysql hostname', $config['hostname'])
|
||||||
|
->addOption('hostport', 'o', Option::VALUE_OPTIONAL, 'mysql hostport', $config['hostport'])
|
||||||
|
->addOption('database', 'd', Option::VALUE_OPTIONAL, 'mysql database', $config['database'])
|
||||||
|
->addOption('prefix', 'r', Option::VALUE_OPTIONAL, 'table prefix', $config['prefix'])
|
||||||
|
->addOption('username', 'u', Option::VALUE_OPTIONAL, 'mysql username', $config['username'])
|
||||||
|
->addOption('password', 'p', Option::VALUE_OPTIONAL, 'mysql password', $config['password'])
|
||||||
|
->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force override', false)
|
||||||
|
->setDescription('New installation of FastAdmin');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 命令行安装
|
||||||
|
*/
|
||||||
|
protected function execute(Input $input, Output $output)
|
||||||
|
{
|
||||||
|
define('INSTALL_PATH', APP_PATH . 'admin' . DS . 'command' . DS . 'Install' . DS);
|
||||||
|
// 覆盖安装
|
||||||
|
$force = $input->getOption('force');
|
||||||
|
$hostname = $input->getOption('hostname');
|
||||||
|
$hostport = $input->getOption('hostport');
|
||||||
|
$database = $input->getOption('database');
|
||||||
|
$prefix = $input->getOption('prefix');
|
||||||
|
$username = $input->getOption('username');
|
||||||
|
$password = $input->getOption('password');
|
||||||
|
|
||||||
|
$installLockFile = INSTALL_PATH . "install.lock";
|
||||||
|
if (is_file($installLockFile) && !$force) {
|
||||||
|
throw new Exception("\nFastAdmin already installed!\nIf you need to reinstall again, use the parameter --force=true ");
|
||||||
|
}
|
||||||
|
|
||||||
|
$adminUsername = 'admin';
|
||||||
|
$adminPassword = Random::alnum(10);
|
||||||
|
$adminEmail = 'admin@admin.com';
|
||||||
|
$siteName = __('My Website');
|
||||||
|
|
||||||
|
$adminName = $this->installation($hostname, $hostport, $database, $username, $password, $prefix, $adminUsername, $adminPassword, $adminEmail, $siteName);
|
||||||
|
if ($adminName) {
|
||||||
|
$output->highlight("Admin url:https://www.example.com/{$adminName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->highlight("Admin username:{$adminUsername}");
|
||||||
|
$output->highlight("Admin password:{$adminPassword}");
|
||||||
|
|
||||||
|
\think\Cache::rm('__menu__');
|
||||||
|
|
||||||
|
$output->info("Install Successed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PC端安装
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$this->view = View::instance(array_merge(Config::get('template'), ['tpl_cache' => false]));
|
||||||
|
$this->request = Request::instance();
|
||||||
|
|
||||||
|
define('INSTALL_PATH', APP_PATH . 'admin' . DS . 'command' . DS . 'Install' . DS);
|
||||||
|
|
||||||
|
$lang = $this->request->langset();
|
||||||
|
$lang = preg_match("/^([a-zA-Z\-_]{2,10})\$/i", $lang) ? $lang : 'zh-cn';
|
||||||
|
|
||||||
|
if (!$lang || in_array($lang, ['zh-cn', 'zh-hans-cn'])) {
|
||||||
|
Lang::load(INSTALL_PATH . 'zh-cn.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
$installLockFile = INSTALL_PATH . "install.lock";
|
||||||
|
|
||||||
|
if (is_file($installLockFile)) {
|
||||||
|
echo __('The system has been installed. If you need to reinstall, please remove %s first', 'install.lock');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$output = function ($code, $msg, $url = null, $data = null) {
|
||||||
|
return json(['code' => $code, 'msg' => $msg, 'url' => $url, 'data' => $data]);
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($this->request->isPost()) {
|
||||||
|
$mysqlHostname = $this->request->post('mysqlHostname', '127.0.0.1');
|
||||||
|
$mysqlHostport = $this->request->post('mysqlHostport', '3306');
|
||||||
|
$hostArr = explode(':', $mysqlHostname);
|
||||||
|
if (count($hostArr) > 1) {
|
||||||
|
$mysqlHostname = $hostArr[0];
|
||||||
|
$mysqlHostport = $hostArr[1];
|
||||||
|
}
|
||||||
|
$mysqlUsername = $this->request->post('mysqlUsername', 'root');
|
||||||
|
$mysqlPassword = $this->request->post('mysqlPassword', '');
|
||||||
|
$mysqlDatabase = $this->request->post('mysqlDatabase', '');
|
||||||
|
$mysqlPrefix = $this->request->post('mysqlPrefix', 'fa_');
|
||||||
|
$adminUsername = $this->request->post('adminUsername', 'admin');
|
||||||
|
$adminPassword = $this->request->post('adminPassword', '');
|
||||||
|
$adminPasswordConfirmation = $this->request->post('adminPasswordConfirmation', '');
|
||||||
|
$adminEmail = $this->request->post('adminEmail', 'admin@admin.com');
|
||||||
|
$siteName = $this->request->post('siteName', __('My Website'));
|
||||||
|
|
||||||
|
if ($adminPassword !== $adminPasswordConfirmation) {
|
||||||
|
return $output(0, __('The two passwords you entered did not match'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$adminName = '';
|
||||||
|
try {
|
||||||
|
$adminName = $this->installation($mysqlHostname, $mysqlHostport, $mysqlDatabase, $mysqlUsername, $mysqlPassword, $mysqlPrefix, $adminUsername, $adminPassword, $adminEmail, $siteName);
|
||||||
|
} catch (\PDOException $e) {
|
||||||
|
throw new Exception($e->getMessage());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $output(0, $e->getMessage());
|
||||||
|
}
|
||||||
|
return $output(1, __('Install Successed'), null, ['adminName' => $adminName]);
|
||||||
|
}
|
||||||
|
$errInfo = '';
|
||||||
|
try {
|
||||||
|
$this->checkenv();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$errInfo = $e->getMessage();
|
||||||
|
}
|
||||||
|
return $this->view->fetch(INSTALL_PATH . "install.html", ['errInfo' => $errInfo]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行安装
|
||||||
|
*/
|
||||||
|
protected function installation($mysqlHostname, $mysqlHostport, $mysqlDatabase, $mysqlUsername, $mysqlPassword, $mysqlPrefix, $adminUsername, $adminPassword, $adminEmail = null, $siteName = null)
|
||||||
|
{
|
||||||
|
$this->checkenv();
|
||||||
|
|
||||||
|
if ($mysqlDatabase == '') {
|
||||||
|
throw new Exception(__('Please input correct database'));
|
||||||
|
}
|
||||||
|
if (!preg_match("/^\w{3,12}$/", $adminUsername)) {
|
||||||
|
throw new Exception(__('Please input correct username'));
|
||||||
|
}
|
||||||
|
if (!preg_match("/^[\S]{6,16}$/", $adminPassword)) {
|
||||||
|
throw new Exception(__('Please input correct password'));
|
||||||
|
}
|
||||||
|
$weakPasswordArr = ['123456', '12345678', '123456789', '654321', '111111', '000000', 'password', 'qwerty', 'abc123', '1qaz2wsx'];
|
||||||
|
if (in_array($adminPassword, $weakPasswordArr)) {
|
||||||
|
throw new Exception(__('Password is too weak'));
|
||||||
|
}
|
||||||
|
if ($siteName == '' || preg_match("/fast" . "admin/i", $siteName)) {
|
||||||
|
throw new Exception(__('Please input correct website'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = file_get_contents(INSTALL_PATH . 'fastadmin.sql');
|
||||||
|
|
||||||
|
$sql = str_replace("`fa_", "`{$mysqlPrefix}", $sql);
|
||||||
|
|
||||||
|
// 先尝试能否自动创建数据库
|
||||||
|
$config = Config::get('database');
|
||||||
|
try {
|
||||||
|
$pdo = new PDO("{$config['type']}:host={$mysqlHostname}" . ($mysqlHostport ? ";port={$mysqlHostport}" : ''), $mysqlUsername, $mysqlPassword);
|
||||||
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
$pdo->query("CREATE DATABASE IF NOT EXISTS `{$mysqlDatabase}` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;");
|
||||||
|
|
||||||
|
// 连接install命令中指定的数据库
|
||||||
|
$instance = Db::connect([
|
||||||
|
'type' => "{$config['type']}",
|
||||||
|
'hostname' => "{$mysqlHostname}",
|
||||||
|
'hostport' => "{$mysqlHostport}",
|
||||||
|
'database' => "{$mysqlDatabase}",
|
||||||
|
'username' => "{$mysqlUsername}",
|
||||||
|
'password' => "{$mysqlPassword}",
|
||||||
|
'prefix' => "{$mysqlPrefix}",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 查询一次SQL,判断连接是否正常
|
||||||
|
$instance->execute("SELECT 1");
|
||||||
|
|
||||||
|
// 调用原生PDO对象进行批量查询
|
||||||
|
$instance->getPdo()->exec($sql);
|
||||||
|
} catch (\PDOException $e) {
|
||||||
|
throw new Exception($e->getMessage());
|
||||||
|
}
|
||||||
|
// 后台入口文件
|
||||||
|
$adminFile = ROOT_PATH . 'public' . DS . 'admin.php';
|
||||||
|
|
||||||
|
// 数据库配置文件
|
||||||
|
$envSampleFile = ROOT_PATH . '.env.sample';
|
||||||
|
$envFile = ROOT_PATH . '.env';
|
||||||
|
if (!file_exists($envFile)) {
|
||||||
|
if (!copy($envSampleFile, $envFile)) {
|
||||||
|
throw new Exception(__('Failed to copy %s to %s', '.env.sample', '.env'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$envText = @file_get_contents($envFile);
|
||||||
|
|
||||||
|
$callback = function ($matches) use ($mysqlHostname, $mysqlHostport, $mysqlUsername, $mysqlPassword, $mysqlDatabase, $mysqlPrefix) {
|
||||||
|
$field = "mysql" . ucfirst($matches[1]);
|
||||||
|
$replace = $$field;
|
||||||
|
return "{$matches[1]} = {$replace}";
|
||||||
|
};
|
||||||
|
$envText = preg_replace_callback("/(hostname|database|username|password|hostport|prefix)\s*=\s*(.*)/", $callback, $envText);
|
||||||
|
|
||||||
|
// 检测能否成功写入数据库配置
|
||||||
|
$result = @file_put_contents($envFile, $envText);
|
||||||
|
if (!$result) {
|
||||||
|
throw new Exception(__('The current permissions are insufficient to write the file %s', '.env'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置新的Token随机密钥key
|
||||||
|
$oldTokenKey = config('token.key');
|
||||||
|
$newTokenKey = \fast\Random::alnum(32);
|
||||||
|
$coreConfigFile = CONF_PATH . 'config.php';
|
||||||
|
$coreConfigText = @file_get_contents($coreConfigFile);
|
||||||
|
$coreConfigText = preg_replace("/'key'(\s+)=>(\s+)'{$oldTokenKey}'/", "'key'\$1=>\$2'{$newTokenKey}'", $coreConfigText);
|
||||||
|
|
||||||
|
$result = @file_put_contents($coreConfigFile, $coreConfigText);
|
||||||
|
if (!$result) {
|
||||||
|
throw new Exception(__('The current permissions are insufficient to write the file %s', 'application/config.php'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$avatar = '/assets/img/avatar.png';
|
||||||
|
// 变更默认管理员密码
|
||||||
|
$adminPassword = $adminPassword ? $adminPassword : Random::alnum(8);
|
||||||
|
$adminEmail = $adminEmail ? $adminEmail : "admin@admin.com";
|
||||||
|
$newSalt = substr(md5(uniqid(true)), 0, 6);
|
||||||
|
$newPassword = md5(md5($adminPassword) . $newSalt);
|
||||||
|
$data = ['username' => $adminUsername, 'email' => $adminEmail, 'avatar' => $avatar, 'password' => $newPassword, 'salt' => $newSalt];
|
||||||
|
$instance->name('admin')->where('username', 'admin')->update($data);
|
||||||
|
|
||||||
|
// 变更前台默认用户的密码,随机生成
|
||||||
|
$newSalt = substr(md5(uniqid(true)), 0, 6);
|
||||||
|
$newPassword = md5(md5(Random::alnum(8)) . $newSalt);
|
||||||
|
$instance->name('user')->where('username', 'admin')->update(['avatar' => $avatar, 'password' => $newPassword, 'salt' => $newSalt]);
|
||||||
|
|
||||||
|
// 修改后台入口
|
||||||
|
$adminName = '';
|
||||||
|
if (is_file($adminFile)) {
|
||||||
|
$adminName = Random::alpha(10) . '.php';
|
||||||
|
rename($adminFile, ROOT_PATH . 'public' . DS . $adminName);
|
||||||
|
}
|
||||||
|
|
||||||
|
//修改站点名称
|
||||||
|
if ($siteName != config('site.name')) {
|
||||||
|
$instance->name('config')->where('name', 'name')->update(['value' => $siteName]);
|
||||||
|
$siteConfigFile = CONF_PATH . 'extra' . DS . 'site.php';
|
||||||
|
$siteConfig = include $siteConfigFile;
|
||||||
|
$configList = $instance->name("config")->select();
|
||||||
|
foreach ($configList as $k => $value) {
|
||||||
|
if (in_array($value['type'], ['selects', 'checkbox', 'images', 'files'])) {
|
||||||
|
$value['value'] = is_array($value['value']) ? $value['value'] : explode(',', $value['value']);
|
||||||
|
}
|
||||||
|
if ($value['type'] == 'array') {
|
||||||
|
$value['value'] = (array)json_decode($value['value'], true);
|
||||||
|
}
|
||||||
|
$siteConfig[$value['name']] = $value['value'];
|
||||||
|
}
|
||||||
|
$siteConfig['name'] = $siteName;
|
||||||
|
file_put_contents($siteConfigFile, '<?php' . "\n\nreturn " . var_export_short($siteConfig) . ";\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$installLockFile = INSTALL_PATH . "install.lock";
|
||||||
|
//检测能否成功写入lock文件
|
||||||
|
$result = @file_put_contents($installLockFile, 1);
|
||||||
|
if (!$result) {
|
||||||
|
throw new Exception(__('The current permissions are insufficient to write the file %s', 'application/admin/command/Install/install.lock'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
//删除安装脚本
|
||||||
|
@unlink(ROOT_PATH . 'public' . DS . 'install.php');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return $adminName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测环境
|
||||||
|
*/
|
||||||
|
protected function checkenv()
|
||||||
|
{
|
||||||
|
// 检测目录是否存在
|
||||||
|
$checkDirs = [
|
||||||
|
'thinkphp',
|
||||||
|
'vendor',
|
||||||
|
'public' . DS . 'assets' . DS . 'libs'
|
||||||
|
];
|
||||||
|
|
||||||
|
//数据库配置文件
|
||||||
|
$dbConfigFile = APP_PATH . 'database.php';
|
||||||
|
|
||||||
|
if (version_compare(PHP_VERSION, $this->minPhpVersion, '<')) {
|
||||||
|
throw new Exception(__("The current PHP %s is too low, please use PHP %s or higher", PHP_VERSION, $this->minPhpVersion));
|
||||||
|
}
|
||||||
|
if (!extension_loaded("PDO")) {
|
||||||
|
throw new Exception(__("PDO is not currently installed and cannot be installed"));
|
||||||
|
}
|
||||||
|
if (!is_really_writable($dbConfigFile)) {
|
||||||
|
throw new Exception(__('The current permissions are insufficient to write the configuration file application/database.php'));
|
||||||
|
}
|
||||||
|
foreach ($checkDirs as $k => $v) {
|
||||||
|
if (!is_dir(ROOT_PATH . $v)) {
|
||||||
|
throw new Exception(__('Please go to the official website to download the full package or resource package and try to install'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,588 @@
|
|||||||
|
/*
|
||||||
|
FastAdmin Install SQL
|
||||||
|
Date: 2024-09-03 15:05:25
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for fa_admin
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE `fa_admin` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`username` varchar(20) DEFAULT '' COMMENT '用户名',
|
||||||
|
`nickname` varchar(50) DEFAULT '' COMMENT '昵称',
|
||||||
|
`password` varchar(32) DEFAULT '' COMMENT '密码',
|
||||||
|
`salt` varchar(30) DEFAULT '' COMMENT '密码盐',
|
||||||
|
`avatar` varchar(255) DEFAULT '' COMMENT '头像',
|
||||||
|
`email` varchar(100) DEFAULT '' COMMENT '电子邮箱',
|
||||||
|
`mobile` varchar(11) DEFAULT '' COMMENT '手机号码',
|
||||||
|
`loginfailure` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '失败次数',
|
||||||
|
`logintime` bigint(16) DEFAULT NULL COMMENT '登录时间',
|
||||||
|
`loginip` varchar(50) DEFAULT NULL COMMENT '登录IP',
|
||||||
|
`createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`token` varchar(59) DEFAULT '' COMMENT 'Session标识',
|
||||||
|
`status` varchar(30) NOT NULL DEFAULT 'normal' COMMENT '状态',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `username` (`username`) USING BTREE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='管理员表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of fa_admin
|
||||||
|
-- ----------------------------
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO `fa_admin` VALUES (1, 'admin', 'Admin', '', '', '/assets/img/avatar.png', 'admin@example.com', '', 0, 1491635035, '127.0.0.1',1491635035, 1491635035, '', 'normal');
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for fa_admin_log
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE `fa_admin_log` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`admin_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '管理员ID',
|
||||||
|
`username` varchar(30) DEFAULT '' COMMENT '管理员名字',
|
||||||
|
`url` varchar(1500) DEFAULT '' COMMENT '操作页面',
|
||||||
|
`title` varchar(100) DEFAULT '' COMMENT '日志标题',
|
||||||
|
`content` longtext NOT NULL COMMENT '内容',
|
||||||
|
`ip` varchar(50) DEFAULT '' COMMENT 'IP',
|
||||||
|
`useragent` varchar(255) DEFAULT '' COMMENT 'User-Agent',
|
||||||
|
`createtime` bigint(16) DEFAULT NULL COMMENT '操作时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `name` (`username`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='管理员日志表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for fa_area
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE `fa_area` (
|
||||||
|
`id` int(10) NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`pid` int(10) DEFAULT NULL COMMENT '父id',
|
||||||
|
`shortname` varchar(100) DEFAULT NULL COMMENT '简称',
|
||||||
|
`name` varchar(100) DEFAULT NULL COMMENT '名称',
|
||||||
|
`mergename` varchar(255) DEFAULT NULL COMMENT '全称',
|
||||||
|
`level` tinyint(4) DEFAULT NULL COMMENT '层级:1=省,2=市,3=区/县',
|
||||||
|
`pinyin` varchar(100) DEFAULT NULL COMMENT '拼音',
|
||||||
|
`code` varchar(100) DEFAULT NULL COMMENT '长途区号',
|
||||||
|
`zip` varchar(100) DEFAULT NULL COMMENT '邮编',
|
||||||
|
`first` varchar(50) DEFAULT NULL COMMENT '首字母',
|
||||||
|
`lng` varchar(100) DEFAULT NULL COMMENT '经度',
|
||||||
|
`lat` varchar(100) DEFAULT NULL COMMENT '纬度',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `pid` (`pid`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='地区表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for fa_attachment
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE `fa_attachment` (
|
||||||
|
`id` int(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`category` varchar(50) DEFAULT '' COMMENT '类别',
|
||||||
|
`admin_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '管理员ID',
|
||||||
|
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '会员ID',
|
||||||
|
`url` varchar(255) DEFAULT '' COMMENT '物理路径',
|
||||||
|
`imagewidth` int(10) unsigned DEFAULT 0 COMMENT '宽度',
|
||||||
|
`imageheight` int(10) unsigned DEFAULT 0 COMMENT '高度',
|
||||||
|
`imagetype` varchar(30) DEFAULT '' COMMENT '图片类型',
|
||||||
|
`imageframes` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '图片帧数',
|
||||||
|
`filename` varchar(100) DEFAULT '' COMMENT '文件名称',
|
||||||
|
`filesize` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '文件大小',
|
||||||
|
`mimetype` varchar(100) DEFAULT '' COMMENT 'mime类型',
|
||||||
|
`extparam` varchar(255) DEFAULT '' COMMENT '透传数据',
|
||||||
|
`createtime` bigint(16) DEFAULT NULL COMMENT '创建日期',
|
||||||
|
`updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`uploadtime` bigint(16) DEFAULT NULL COMMENT '上传时间',
|
||||||
|
`storage` varchar(100) NOT NULL DEFAULT 'local' COMMENT '存储位置',
|
||||||
|
`sha1` varchar(40) DEFAULT '' COMMENT '文件 sha1编码',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='附件表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of fa_attachment
|
||||||
|
-- ----------------------------
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO `fa_attachment` VALUES (1, '', 1, 0, '/assets/img/qrcode.png', '150', '150', 'png', 0, 'qrcode.png', 21859, 'image/png', '', 1491635035, 1491635035, 1491635035, 'local', '17163603d0263e4838b9387ff2cd4877e8b018f6');
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for fa_auth_group
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE `fa_auth_group` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`pid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '父组别',
|
||||||
|
`name` varchar(100) DEFAULT '' COMMENT '组名',
|
||||||
|
`rules` text NOT NULL COMMENT '规则ID',
|
||||||
|
`createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`status` varchar(30) DEFAULT '' COMMENT '状态',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='分组表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of fa_auth_group
|
||||||
|
-- ----------------------------
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO `fa_auth_group` VALUES (1, 0, 'Admin group', '*', 1491635035, 1491635035, 'normal');
|
||||||
|
INSERT INTO `fa_auth_group` VALUES (2, 1, 'Second group', '13,14,16,15,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,40,41,42,43,44,45,46,47,48,49,50,55,56,57,58,59,60,61,62,63,64,65,1,9,10,11,7,6,8,2,4,5', 1491635035, 1491635035, 'normal');
|
||||||
|
INSERT INTO `fa_auth_group` VALUES (3, 2, 'Third group', '1,4,9,10,11,13,14,15,16,17,40,41,42,43,44,45,46,47,48,49,50,55,56,57,58,59,60,61,62,63,64,65,5', 1491635035, 1491635035, 'normal');
|
||||||
|
INSERT INTO `fa_auth_group` VALUES (4, 1, 'Second group 2', '1,4,13,14,15,16,17,55,56,57,58,59,60,61,62,63,64,65', 1491635035, 1491635035, 'normal');
|
||||||
|
INSERT INTO `fa_auth_group` VALUES (5, 2, 'Third group 2', '1,2,6,7,8,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34', 1491635035, 1491635035, 'normal');
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for fa_auth_group_access
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE `fa_auth_group_access` (
|
||||||
|
`uid` int(10) unsigned NOT NULL COMMENT '会员ID',
|
||||||
|
`group_id` int(10) unsigned NOT NULL COMMENT '级别ID',
|
||||||
|
UNIQUE KEY `uid_group_id` (`uid`,`group_id`),
|
||||||
|
KEY `uid` (`uid`),
|
||||||
|
KEY `group_id` (`group_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='权限分组表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of fa_auth_group_access
|
||||||
|
-- ----------------------------
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO `fa_auth_group_access` VALUES (1, 1);
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for fa_auth_rule
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE `fa_auth_rule` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`type` enum('menu','file') NOT NULL DEFAULT 'file' COMMENT 'menu为菜单,file为权限节点',
|
||||||
|
`pid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '父ID',
|
||||||
|
`name` varchar(100) DEFAULT '' COMMENT '规则名称',
|
||||||
|
`title` varchar(50) DEFAULT '' COMMENT '规则名称',
|
||||||
|
`icon` varchar(50) DEFAULT '' COMMENT '图标',
|
||||||
|
`url` varchar(255) DEFAULT '' COMMENT '规则URL',
|
||||||
|
`condition` varchar(255) DEFAULT '' COMMENT '条件',
|
||||||
|
`remark` varchar(255) DEFAULT '' COMMENT '备注',
|
||||||
|
`ismenu` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否为菜单',
|
||||||
|
`menutype` enum('addtabs','blank','dialog','ajax') DEFAULT NULL COMMENT '菜单类型',
|
||||||
|
`extend` varchar(255) DEFAULT '' COMMENT '扩展属性',
|
||||||
|
`py` varchar(30) DEFAULT '' COMMENT '拼音首字母',
|
||||||
|
`pinyin` varchar(100) DEFAULT '' COMMENT '拼音',
|
||||||
|
`createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重',
|
||||||
|
`status` varchar(30) DEFAULT '' COMMENT '状态',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `name` (`name`) USING BTREE,
|
||||||
|
KEY `pid` (`pid`),
|
||||||
|
KEY `weigh` (`weigh`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=66 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='节点表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of fa_auth_rule
|
||||||
|
-- ----------------------------
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (1, 'file', 0, 'dashboard', 'Dashboard', 'fa fa-dashboard', '', '', 'Dashboard tips', 1, NULL, '', 'kzt', 'kongzhitai', 1491635035, 1491635035, 143, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (2, 'file', 0, 'general', 'General', 'fa fa-cogs', '', '', '', 1, NULL, '', 'cggl', 'changguiguanli', 1491635035, 1491635035, 137, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (3, 'file', 0, 'category', 'Category', 'fa fa-leaf', '', '', 'Category tips', 0, NULL, '', 'flgl', 'fenleiguanli', 1491635035, 1491635035, 119, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (4, 'file', 0, 'addon', 'Addon', 'fa fa-rocket', '', '', 'Addon tips', 1, NULL, '', 'cjgl', 'chajianguanli', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (5, 'file', 0, 'auth', 'Auth', 'fa fa-group', '', '', '', 1, NULL, '', 'qxgl', 'quanxianguanli', 1491635035, 1491635035, 99, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (6, 'file', 2, 'general/config', 'Config', 'fa fa-cog', '', '', 'Config tips', 1, NULL, '', 'xtpz', 'xitongpeizhi', 1491635035, 1491635035, 60, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (7, 'file', 2, 'general/attachment', 'Attachment', 'fa fa-file-image-o', '', '', 'Attachment tips', 1, NULL, '', 'fjgl', 'fujianguanli', 1491635035, 1491635035, 53, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (8, 'file', 2, 'general/profile', 'Profile', 'fa fa-user', '', '', '', 1, NULL, '', 'grzl', 'gerenziliao', 1491635035, 1491635035, 34, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (9, 'file', 5, 'auth/admin', 'Admin', 'fa fa-user', '', '', 'Admin tips', 1, NULL, '', 'glygl', 'guanliyuanguanli', 1491635035, 1491635035, 118, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (10, 'file', 5, 'auth/adminlog', 'Admin log', 'fa fa-list-alt', '', '', 'Admin log tips', 1, NULL, '', 'glyrz', 'guanliyuanrizhi', 1491635035, 1491635035, 113, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (11, 'file', 5, 'auth/group', 'Group', 'fa fa-group', '', '', 'Group tips', 1, NULL, '', 'jsz', 'juesezu', 1491635035, 1491635035, 109, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (12, 'file', 5, 'auth/rule', 'Rule', 'fa fa-bars', '', '', 'Rule tips', 1, NULL, '', 'cdgz', 'caidanguize', 1491635035, 1491635035, 104, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (13, 'file', 1, 'dashboard/index', 'View', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 136, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (14, 'file', 1, 'dashboard/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 135, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (15, 'file', 1, 'dashboard/del', 'Delete', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 133, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (16, 'file', 1, 'dashboard/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 134, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (17, 'file', 1, 'dashboard/multi', 'Multi', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 132, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (18, 'file', 6, 'general/config/index', 'View', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 52, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (19, 'file', 6, 'general/config/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 51, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (20, 'file', 6, 'general/config/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 50, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (21, 'file', 6, 'general/config/del', 'Delete', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 49, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (22, 'file', 6, 'general/config/multi', 'Multi', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 48, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (23, 'file', 7, 'general/attachment/index', 'View', 'fa fa-circle-o', '', '', 'Attachment tips', 0, NULL, '', '', '', 1491635035, 1491635035, 59, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (24, 'file', 7, 'general/attachment/select', 'Select attachment', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 58, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (25, 'file', 7, 'general/attachment/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 57, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (26, 'file', 7, 'general/attachment/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 56, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (27, 'file', 7, 'general/attachment/del', 'Delete', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 55, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (28, 'file', 7, 'general/attachment/multi', 'Multi', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 54, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (29, 'file', 8, 'general/profile/index', 'View', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 33, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (30, 'file', 8, 'general/profile/update', 'Update profile', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 32, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (31, 'file', 8, 'general/profile/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 31, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (32, 'file', 8, 'general/profile/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 30, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (33, 'file', 8, 'general/profile/del', 'Delete', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 29, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (34, 'file', 8, 'general/profile/multi', 'Multi', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 28, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (35, 'file', 3, 'category/index', 'View', 'fa fa-circle-o', '', '', 'Category tips', 0, NULL, '', '', '', 1491635035, 1491635035, 142, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (36, 'file', 3, 'category/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 141, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (37, 'file', 3, 'category/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 140, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (38, 'file', 3, 'category/del', 'Delete', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 139, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (39, 'file', 3, 'category/multi', 'Multi', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 138, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (40, 'file', 9, 'auth/admin/index', 'View', 'fa fa-circle-o', '', '', 'Admin tips', 0, NULL, '', '', '', 1491635035, 1491635035, 117, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (41, 'file', 9, 'auth/admin/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 116, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (42, 'file', 9, 'auth/admin/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 115, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (43, 'file', 9, 'auth/admin/del', 'Delete', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 114, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (44, 'file', 10, 'auth/adminlog/index', 'View', 'fa fa-circle-o', '', '', 'Admin log tips', 0, NULL, '', '', '', 1491635035, 1491635035, 112, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (45, 'file', 10, 'auth/adminlog/detail', 'Detail', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 111, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (46, 'file', 10, 'auth/adminlog/del', 'Delete', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 110, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (47, 'file', 11, 'auth/group/index', 'View', 'fa fa-circle-o', '', '', 'Group tips', 0, NULL, '', '', '', 1491635035, 1491635035, 108, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (48, 'file', 11, 'auth/group/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 107, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (49, 'file', 11, 'auth/group/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 106, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (50, 'file', 11, 'auth/group/del', 'Delete', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 105, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (51, 'file', 12, 'auth/rule/index', 'View', 'fa fa-circle-o', '', '', 'Rule tips', 0, NULL, '', '', '', 1491635035, 1491635035, 103, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (52, 'file', 12, 'auth/rule/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 102, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (53, 'file', 12, 'auth/rule/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 101, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (54, 'file', 12, 'auth/rule/del', 'Delete', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 100, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (55, 'file', 4, 'addon/index', 'View', 'fa fa-circle-o', '', '', 'Addon tips', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (56, 'file', 4, 'addon/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (57, 'file', 4, 'addon/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (58, 'file', 4, 'addon/del', 'Delete', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (59, 'file', 4, 'addon/downloaded', 'Local addon', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (60, 'file', 4, 'addon/state', 'Update state', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (63, 'file', 4, 'addon/config', 'Setting', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (64, 'file', 4, 'addon/refresh', 'Refresh', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (65, 'file', 4, 'addon/multi', 'Multi', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (66, 'file', 0, 'user', 'User', 'fa fa-user-circle', '', '', '', 1, NULL, '', 'hygl', 'huiyuanguanli', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (67, 'file', 66, 'user/user', 'User', 'fa fa-user', '', '', '', 1, NULL, '', 'hygl', 'huiyuanguanli', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (68, 'file', 67, 'user/user/index', 'View', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (69, 'file', 67, 'user/user/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (70, 'file', 67, 'user/user/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (71, 'file', 67, 'user/user/del', 'Del', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (72, 'file', 67, 'user/user/multi', 'Multi', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (73, 'file', 66, 'user/group', 'User group', 'fa fa-users', '', '', '', 1, NULL, '', 'hyfz', 'huiyuanfenzu', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (74, 'file', 73, 'user/group/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (75, 'file', 73, 'user/group/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (76, 'file', 73, 'user/group/index', 'View', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (77, 'file', 73, 'user/group/del', 'Del', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (78, 'file', 73, 'user/group/multi', 'Multi', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (79, 'file', 66, 'user/rule', 'User rule', 'fa fa-circle-o', '', '', '', 1, NULL, '', 'hygz', 'huiyuanguize', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (80, 'file', 79, 'user/rule/index', 'View', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (81, 'file', 79, 'user/rule/del', 'Del', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (82, 'file', 79, 'user/rule/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (83, 'file', 79, 'user/rule/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
INSERT INTO `fa_auth_rule` VALUES (84, 'file', 79, 'user/rule/multi', 'Multi', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for fa_category
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE `fa_category` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`pid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '父ID',
|
||||||
|
`type` varchar(30) DEFAULT '' COMMENT '栏目类型',
|
||||||
|
`name` varchar(30) DEFAULT '',
|
||||||
|
`nickname` varchar(50) DEFAULT '',
|
||||||
|
`flag` set('hot','index','recommend') DEFAULT '',
|
||||||
|
`image` varchar(100) DEFAULT '' COMMENT '图片',
|
||||||
|
`keywords` varchar(255) DEFAULT '' COMMENT '关键字',
|
||||||
|
`description` varchar(255) DEFAULT '' COMMENT '描述',
|
||||||
|
`diyname` varchar(30) DEFAULT '' COMMENT '自定义名称',
|
||||||
|
`createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重',
|
||||||
|
`status` varchar(30) DEFAULT '' COMMENT '状态',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `weigh` (`weigh`,`id`),
|
||||||
|
KEY `pid` (`pid`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='分类表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of fa_category
|
||||||
|
-- ----------------------------
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO `fa_category` VALUES (1, 0, 'page', '官方新闻', 'news', 'recommend', '/assets/img/qrcode.png', '', '', 'news', 1491635035, 1491635035, 1, 'normal');
|
||||||
|
INSERT INTO `fa_category` VALUES (2, 0, 'page', '移动应用', 'mobileapp', 'hot', '/assets/img/qrcode.png', '', '', 'mobileapp', 1491635035, 1491635035, 2, 'normal');
|
||||||
|
INSERT INTO `fa_category` VALUES (3, 2, 'page', '微信公众号', 'wechatpublic', 'index', '/assets/img/qrcode.png', '', '', 'wechatpublic', 1491635035, 1491635035, 3, 'normal');
|
||||||
|
INSERT INTO `fa_category` VALUES (4, 2, 'page', 'Android开发', 'android', 'recommend', '/assets/img/qrcode.png', '', '', 'android', 1491635035, 1491635035, 4, 'normal');
|
||||||
|
INSERT INTO `fa_category` VALUES (5, 0, 'page', '软件产品', 'software', 'recommend', '/assets/img/qrcode.png', '', '', 'software', 1491635035, 1491635035, 5, 'normal');
|
||||||
|
INSERT INTO `fa_category` VALUES (6, 5, 'page', '网站建站', 'website', 'recommend', '/assets/img/qrcode.png', '', '', 'website', 1491635035, 1491635035, 6, 'normal');
|
||||||
|
INSERT INTO `fa_category` VALUES (7, 5, 'page', '企业管理软件', 'company', 'index', '/assets/img/qrcode.png', '', '', 'company', 1491635035, 1491635035, 7, 'normal');
|
||||||
|
INSERT INTO `fa_category` VALUES (8, 6, 'page', 'PC端', 'website-pc', 'recommend', '/assets/img/qrcode.png', '', '', 'website-pc', 1491635035, 1491635035, 8, 'normal');
|
||||||
|
INSERT INTO `fa_category` VALUES (9, 6, 'page', '移动端', 'website-mobile', 'recommend', '/assets/img/qrcode.png', '', '', 'website-mobile', 1491635035, 1491635035, 9, 'normal');
|
||||||
|
INSERT INTO `fa_category` VALUES (10, 7, 'page', 'CRM系统 ', 'company-crm', 'recommend', '/assets/img/qrcode.png', '', '', 'company-crm', 1491635035, 1491635035, 10, 'normal');
|
||||||
|
INSERT INTO `fa_category` VALUES (11, 7, 'page', 'SASS平台软件', 'company-sass', 'recommend', '/assets/img/qrcode.png', '', '', 'company-sass', 1491635035, 1491635035, 11, 'normal');
|
||||||
|
INSERT INTO `fa_category` VALUES (12, 0, 'test', '测试1', 'test1', 'recommend', '/assets/img/qrcode.png', '', '', 'test1', 1491635035, 1491635035, 12, 'normal');
|
||||||
|
INSERT INTO `fa_category` VALUES (13, 0, 'test', '测试2', 'test2', 'recommend', '/assets/img/qrcode.png', '', '', 'test2', 1491635035, 1491635035, 13, 'normal');
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for fa_config
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE `fa_config` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(30) DEFAULT '' COMMENT '变量名',
|
||||||
|
`group` varchar(30) DEFAULT '' COMMENT '分组',
|
||||||
|
`title` varchar(100) DEFAULT '' COMMENT '变量标题',
|
||||||
|
`tip` varchar(100) DEFAULT '' COMMENT '变量描述',
|
||||||
|
`type` varchar(30) DEFAULT '' COMMENT '类型:string,text,int,bool,array,datetime,date,file',
|
||||||
|
`visible` varchar(255) DEFAULT '' COMMENT '可见条件',
|
||||||
|
`value` text COMMENT '变量值',
|
||||||
|
`content` text COMMENT '变量字典数据',
|
||||||
|
`rule` varchar(100) DEFAULT '' COMMENT '验证规则',
|
||||||
|
`extend` varchar(255) DEFAULT '' COMMENT '扩展属性',
|
||||||
|
`setting` varchar(255) DEFAULT '' COMMENT '配置',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `name` (`name`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='系统配置';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of fa_config
|
||||||
|
-- ----------------------------
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO `fa_config` VALUES (1, 'name', 'basic', 'Site name', '请填写站点名称', 'string', '', '我的网站', '', 'required', '', '');
|
||||||
|
INSERT INTO `fa_config` VALUES (2, 'beian', 'basic', 'Beian', '粤ICP备15000000号-1', 'string', '', '', '', '', '', '');
|
||||||
|
INSERT INTO `fa_config` VALUES (3, 'cdnurl', 'basic', 'Cdn url', '如果全站静态资源使用第三方云储存请配置该值', 'string', '', '', '', '', '', '');
|
||||||
|
INSERT INTO `fa_config` VALUES (4, 'version', 'basic', 'Version', '如果静态资源有变动请重新配置该值', 'string', '', '1.0.1', '', 'required', '', '');
|
||||||
|
INSERT INTO `fa_config` VALUES (5, 'timezone', 'basic', 'Timezone', '', 'string', '', 'Asia/Shanghai', '', 'required', '', '');
|
||||||
|
INSERT INTO `fa_config` VALUES (6, 'forbiddenip', 'basic', 'Forbidden ip', '一行一条记录', 'text', '', '', '', '', '', '');
|
||||||
|
INSERT INTO `fa_config` VALUES (7, 'languages', 'basic', 'Languages', '', 'array', '', '{\"backend\":\"zh-cn\",\"frontend\":\"zh-cn\"}', '', 'required', '', '');
|
||||||
|
INSERT INTO `fa_config` VALUES (8, 'fixedpage', 'basic', 'Fixed page', '请输入左侧菜单栏存在的链接', 'string', '', 'dashboard', '', 'required', '', '');
|
||||||
|
INSERT INTO `fa_config` VALUES (9, 'categorytype', 'dictionary', 'Category type', '', 'array', '', '{\"default\":\"Default\",\"page\":\"Page\",\"article\":\"Article\",\"test\":\"Test\"}', '', '', '', '');
|
||||||
|
INSERT INTO `fa_config` VALUES (10, 'configgroup', 'dictionary', 'Config group', '', 'array', '', '{\"basic\":\"Basic\",\"email\":\"Email\",\"dictionary\":\"Dictionary\",\"user\":\"User\",\"example\":\"Example\"}', '', '', '', '');
|
||||||
|
INSERT INTO `fa_config` VALUES (11, 'mail_type', 'email', 'Mail type', '选择邮件发送方式', 'select', '', '1', '[\"请选择\",\"SMTP\"]', '', '', '');
|
||||||
|
INSERT INTO `fa_config` VALUES (12, 'mail_smtp_host', 'email', 'Mail smtp host', '错误的配置发送邮件会导致服务器超时', 'string', '', 'smtp.qq.com', '', '', '', '');
|
||||||
|
INSERT INTO `fa_config` VALUES (13, 'mail_smtp_port', 'email', 'Mail smtp port', '(不加密默认25,SSL默认465,TLS默认587)', 'string', '', '465', '', '', '', '');
|
||||||
|
INSERT INTO `fa_config` VALUES (14, 'mail_smtp_user', 'email', 'Mail smtp user', '(填写完整用户名)', 'string', '', '', '', '', '', '');
|
||||||
|
INSERT INTO `fa_config` VALUES (15, 'mail_smtp_pass', 'email', 'Mail smtp password', '(填写您的密码或授权码)', 'password', '', '', '', '', '', '');
|
||||||
|
INSERT INTO `fa_config` VALUES (16, 'mail_verify_type', 'email', 'Mail vertify type', '(SMTP验证方式[推荐SSL])', 'select', '', '2', '[\"无\",\"TLS\",\"SSL\"]', '', '', '');
|
||||||
|
INSERT INTO `fa_config` VALUES (17, 'mail_from', 'email', 'Mail from', '', 'string', '', '', '', '', '', '');
|
||||||
|
INSERT INTO `fa_config` VALUES (18, 'attachmentcategory', 'dictionary', 'Attachment category', '', 'array', '', '{\"category1\":\"Category1\",\"category2\":\"Category2\",\"custom\":\"Custom\"}', '', '', '', '');
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for fa_ems
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE `fa_ems` (
|
||||||
|
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`event` varchar(30) DEFAULT '' COMMENT '事件',
|
||||||
|
`email` varchar(100) DEFAULT '' COMMENT '邮箱',
|
||||||
|
`code` varchar(10) DEFAULT '' COMMENT '验证码',
|
||||||
|
`times` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '验证次数',
|
||||||
|
`ip` varchar(30) DEFAULT '' COMMENT 'IP',
|
||||||
|
`createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='邮箱验证码表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for fa_sms
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE `fa_sms` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`event` varchar(30) DEFAULT '' COMMENT '事件',
|
||||||
|
`mobile` varchar(20) DEFAULT '' COMMENT '手机号',
|
||||||
|
`code` varchar(10) DEFAULT '' COMMENT '验证码',
|
||||||
|
`times` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '验证次数',
|
||||||
|
`ip` varchar(30) DEFAULT '' COMMENT 'IP',
|
||||||
|
`createtime` bigint(16) unsigned DEFAULT '0' COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='短信验证码表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for fa_test
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE `fa_test` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`user_id` int(10) DEFAULT '0' COMMENT '会员ID',
|
||||||
|
`admin_id` int(10) DEFAULT '0' COMMENT '管理员ID',
|
||||||
|
`category_id` int(10) unsigned DEFAULT '0' COMMENT '分类ID(单选)',
|
||||||
|
`category_ids` varchar(100) COMMENT '分类ID(多选)',
|
||||||
|
`tags` varchar(255) DEFAULT '' COMMENT '标签',
|
||||||
|
`week` enum('monday','tuesday','wednesday') COMMENT '星期(单选):monday=星期一,tuesday=星期二,wednesday=星期三',
|
||||||
|
`flag` set('hot','index','recommend') DEFAULT '' COMMENT '标志(多选):hot=热门,index=首页,recommend=推荐',
|
||||||
|
`genderdata` enum('male','female') DEFAULT 'male' COMMENT '性别(单选):male=男,female=女',
|
||||||
|
`hobbydata` set('music','reading','swimming') COMMENT '爱好(多选):music=音乐,reading=读书,swimming=游泳',
|
||||||
|
`title` varchar(100) DEFAULT '' COMMENT '标题',
|
||||||
|
`content` text COMMENT '内容',
|
||||||
|
`image` varchar(100) DEFAULT '' COMMENT '图片',
|
||||||
|
`images` varchar(1500) DEFAULT '' COMMENT '图片组',
|
||||||
|
`attachfile` varchar(100) DEFAULT '' COMMENT '附件',
|
||||||
|
`keywords` varchar(255) DEFAULT '' COMMENT '关键字',
|
||||||
|
`description` varchar(255) DEFAULT '' COMMENT '描述',
|
||||||
|
`city` varchar(100) DEFAULT '' COMMENT '省市',
|
||||||
|
`array` varchar(255) DEFAULT '' COMMENT '数组:value=值',
|
||||||
|
`json` varchar(255) DEFAULT '' COMMENT '配置:key=名称,value=值',
|
||||||
|
`multiplejson` varchar(1500) DEFAULT '' COMMENT '二维数组:title=标题,intro=介绍,author=作者,age=年龄',
|
||||||
|
`price` decimal(10,2) unsigned DEFAULT '0.00' COMMENT '价格',
|
||||||
|
`views` int(10) unsigned DEFAULT '0' COMMENT '点击',
|
||||||
|
`workrange` varchar(100) DEFAULT '' COMMENT '时间区间',
|
||||||
|
`startdate` date DEFAULT NULL COMMENT '开始日期',
|
||||||
|
`activitytime` datetime DEFAULT NULL COMMENT '活动时间(datetime)',
|
||||||
|
`year` year(4) DEFAULT NULL COMMENT '年',
|
||||||
|
`times` time DEFAULT NULL COMMENT '时间',
|
||||||
|
`refreshtime` bigint(16) DEFAULT NULL COMMENT '刷新时间',
|
||||||
|
`createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`deletetime` bigint(16) DEFAULT NULL COMMENT '删除时间',
|
||||||
|
`weigh` int(10) DEFAULT '0' COMMENT '权重',
|
||||||
|
`switch` tinyint(1) DEFAULT '0' COMMENT '开关',
|
||||||
|
`status` enum('normal','hidden') DEFAULT 'normal' COMMENT '状态',
|
||||||
|
`state` enum('0','1','2') DEFAULT '1' COMMENT '状态值:0=禁用,1=正常,2=推荐',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='测试表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of fa_test
|
||||||
|
-- ----------------------------
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO `fa_test` VALUES (1, 1, 1, 12, '12,13', '互联网,计算机', 'monday', 'hot,index', 'male', 'music,reading', '我是一篇测试文章', '<p>我是测试内容</p>', '/assets/img/avatar.png', '/assets/img/avatar.png,/assets/img/qrcode.png', '/assets/img/avatar.png', '关键字', '我是一篇测试文章描述,内容过多时将自动隐藏', '广西壮族自治区/百色市/平果县', '[\"a\",\"b\"]', '{\"a\":\"1\",\"b\":\"2\"}', '[{\"title\":\"标题一\",\"intro\":\"介绍一\",\"author\":\"小明\",\"age\":\"21\"}]', 0.00, 0, '2020-10-01 00:00:00 - 2021-10-31 23:59:59', '2017-07-10', '2017-07-10 18:24:45', 2017, '18:24:45', 1491635035, 1491635035, 1491635035, NULL, 0, 1, 'normal', '1');
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for fa_user
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE `fa_user` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`group_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '组别ID',
|
||||||
|
`username` varchar(32) DEFAULT '' COMMENT '用户名',
|
||||||
|
`nickname` varchar(50) DEFAULT '' COMMENT '昵称',
|
||||||
|
`password` varchar(32) DEFAULT '' COMMENT '密码',
|
||||||
|
`salt` varchar(30) DEFAULT '' COMMENT '密码盐',
|
||||||
|
`email` varchar(100) DEFAULT '' COMMENT '电子邮箱',
|
||||||
|
`mobile` varchar(11) DEFAULT '' COMMENT '手机号',
|
||||||
|
`avatar` varchar(255) DEFAULT '' COMMENT '头像',
|
||||||
|
`level` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '等级',
|
||||||
|
`gender` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '性别',
|
||||||
|
`birthday` date DEFAULT NULL COMMENT '生日',
|
||||||
|
`bio` varchar(100) DEFAULT '' COMMENT '格言',
|
||||||
|
`money` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '余额',
|
||||||
|
`score` int(10) NOT NULL DEFAULT '0' COMMENT '积分',
|
||||||
|
`successions` int(10) unsigned NOT NULL DEFAULT '1' COMMENT '连续登录天数',
|
||||||
|
`maxsuccessions` int(10) unsigned NOT NULL DEFAULT '1' COMMENT '最大连续登录天数',
|
||||||
|
`prevtime` bigint(16) DEFAULT NULL COMMENT '上次登录时间',
|
||||||
|
`logintime` bigint(16) DEFAULT NULL COMMENT '登录时间',
|
||||||
|
`loginip` varchar(50) DEFAULT '' COMMENT '登录IP',
|
||||||
|
`loginfailure` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '失败次数',
|
||||||
|
`loginfailuretime` bigint(16) DEFAULT NULL COMMENT '最后登录失败时间',
|
||||||
|
`joinip` varchar(50) DEFAULT '' COMMENT '加入IP',
|
||||||
|
`jointime` bigint(16) DEFAULT NULL COMMENT '加入时间',
|
||||||
|
`createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`token` varchar(50) DEFAULT '' COMMENT 'Token',
|
||||||
|
`status` varchar(30) DEFAULT '' COMMENT '状态',
|
||||||
|
`verification` varchar(255) DEFAULT '' COMMENT '验证',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `username` (`username`),
|
||||||
|
KEY `email` (`email`),
|
||||||
|
KEY `mobile` (`mobile`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='会员表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of fa_user
|
||||||
|
-- ----------------------------
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO `fa_user` VALUES (1, 1, 'admin', 'admin', '', '', 'admin@163.com', '13000000000', '', 0, 0, '2017-04-08', '', 0, 0, 1, 1, 1491635035, 1491635035, '127.0.0.1', 0, 1491635035,'127.0.0.1', 1491635035, 0, 1491635035, '', 'normal','');
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for fa_user_group
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE `fa_user_group` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(50) DEFAULT '' COMMENT '组名',
|
||||||
|
`rules` text COMMENT '权限节点',
|
||||||
|
`createtime` bigint(16) DEFAULT NULL COMMENT '添加时间',
|
||||||
|
`updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`status` enum('normal','hidden') DEFAULT NULL COMMENT '状态',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='会员组表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of fa_user_group
|
||||||
|
-- ----------------------------
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO `fa_user_group` VALUES (1, '默认组', '1,2,3,4,5,6,7,8,9,10,11,12', 1491635035, 1491635035, 'normal');
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for fa_user_money_log
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE `fa_user_money_log` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '会员ID',
|
||||||
|
`money` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '变更余额',
|
||||||
|
`before` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '变更前余额',
|
||||||
|
`after` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '变更后余额',
|
||||||
|
`memo` varchar(255) DEFAULT '' COMMENT '备注',
|
||||||
|
`createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='会员余额变动表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for fa_user_rule
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE `fa_user_rule` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`pid` int(10) DEFAULT NULL COMMENT '父ID',
|
||||||
|
`name` varchar(50) DEFAULT NULL COMMENT '名称',
|
||||||
|
`title` varchar(50) DEFAULT '' COMMENT '标题',
|
||||||
|
`remark` varchar(100) DEFAULT NULL COMMENT '备注',
|
||||||
|
`ismenu` tinyint(1) DEFAULT NULL COMMENT '是否菜单',
|
||||||
|
`createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`weigh` int(10) DEFAULT '0' COMMENT '权重',
|
||||||
|
`status` enum('normal','hidden') DEFAULT NULL COMMENT '状态',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='会员规则表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of fa_user_rule
|
||||||
|
-- ----------------------------
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO `fa_user_rule` VALUES (1, 0, 'index', 'Frontend', '', 1, 1491635035, 1491635035, 1, 'normal');
|
||||||
|
INSERT INTO `fa_user_rule` VALUES (2, 0, 'api', 'API Interface', '', 1, 1491635035, 1491635035, 2, 'normal');
|
||||||
|
INSERT INTO `fa_user_rule` VALUES (3, 1, 'user', 'User Module', '', 1, 1491635035, 1491635035, 12, 'normal');
|
||||||
|
INSERT INTO `fa_user_rule` VALUES (4, 2, 'user', 'User Module', '', 1, 1491635035, 1491635035, 11, 'normal');
|
||||||
|
INSERT INTO `fa_user_rule` VALUES (5, 3, 'index/user/login', 'Login', '', 0, 1491635035, 1491635035, 5, 'normal');
|
||||||
|
INSERT INTO `fa_user_rule` VALUES (6, 3, 'index/user/register', 'Register', '', 0, 1491635035, 1491635035, 7, 'normal');
|
||||||
|
INSERT INTO `fa_user_rule` VALUES (7, 3, 'index/user/index', 'User Center', '', 0, 1491635035, 1491635035, 9, 'normal');
|
||||||
|
INSERT INTO `fa_user_rule` VALUES (8, 3, 'index/user/profile', 'Profile', '', 0, 1491635035, 1491635035, 4, 'normal');
|
||||||
|
INSERT INTO `fa_user_rule` VALUES (9, 4, 'api/user/login', 'Login', '', 0, 1491635035, 1491635035, 6, 'normal');
|
||||||
|
INSERT INTO `fa_user_rule` VALUES (10, 4, 'api/user/register', 'Register', '', 0, 1491635035, 1491635035, 8, 'normal');
|
||||||
|
INSERT INTO `fa_user_rule` VALUES (11, 4, 'api/user/index', 'User Center', '', 0, 1491635035, 1491635035, 10, 'normal');
|
||||||
|
INSERT INTO `fa_user_rule` VALUES (12, 4, 'api/user/profile', 'Profile', '', 0, 1491635035, 1491635035, 3, 'normal');
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for fa_user_score_log
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE `fa_user_score_log` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '会员ID',
|
||||||
|
`score` int(10) NOT NULL DEFAULT '0' COMMENT '变更积分',
|
||||||
|
`before` int(10) NOT NULL DEFAULT '0' COMMENT '变更前积分',
|
||||||
|
`after` int(10) NOT NULL DEFAULT '0' COMMENT '变更后积分',
|
||||||
|
`memo` varchar(255) DEFAULT '' COMMENT '备注',
|
||||||
|
`createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='会员积分变动表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for fa_user_token
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE `fa_user_token` (
|
||||||
|
`token` varchar(50) NOT NULL COMMENT 'Token',
|
||||||
|
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '会员ID',
|
||||||
|
`createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`expiretime` bigint(16) DEFAULT NULL COMMENT '过期时间',
|
||||||
|
PRIMARY KEY (`token`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='会员Token表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for fa_version
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE `fa_version` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`oldversion` varchar(30) DEFAULT '' COMMENT '旧版本号',
|
||||||
|
`newversion` varchar(30) DEFAULT '' COMMENT '新版本号',
|
||||||
|
`packagesize` varchar(30) DEFAULT '' COMMENT '包大小',
|
||||||
|
`content` varchar(500) DEFAULT '' COMMENT '升级内容',
|
||||||
|
`downloadurl` varchar(255) DEFAULT '' COMMENT '下载地址',
|
||||||
|
`enforce` tinyint(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT '强制更新',
|
||||||
|
`createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`weigh` int(10) NOT NULL DEFAULT 0 COMMENT '权重',
|
||||||
|
`status` varchar(30) DEFAULT '' COMMENT '状态',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='版本表';
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>{:__('Installing FastAdmin')}</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1">
|
||||||
|
<meta name="renderer" content="webkit">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: #f1f6fd;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, input, button {
|
||||||
|
font-family: 'Source Sans Pro', 'Helvetica Neue', Helvetica, 'Microsoft Yahei', Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #7E96B3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 480px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #4e73df;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #3C5675;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group .form-field:first-child input {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group .form-field:last-child input {
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field input {
|
||||||
|
background: #fff;
|
||||||
|
margin: 0 0 2px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px 15px 15px 180px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field input:focus {
|
||||||
|
border-color: #4e73df;
|
||||||
|
background: #fff;
|
||||||
|
color: #444;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field label {
|
||||||
|
float: left;
|
||||||
|
width: 160px;
|
||||||
|
text-align: right;
|
||||||
|
margin-right: -160px;
|
||||||
|
position: relative;
|
||||||
|
margin-top: 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, .btn {
|
||||||
|
background: #3C5675;
|
||||||
|
color: #fff;
|
||||||
|
border: 0;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 15px 30px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button[disabled] {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-buttons {
|
||||||
|
height: 52px;
|
||||||
|
line-height: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-buttons .btn {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#error, .error, #success, .success, #warmtips, .warmtips {
|
||||||
|
background: #D83E3E;
|
||||||
|
color: #fff;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#success {
|
||||||
|
background: #3C5675;
|
||||||
|
}
|
||||||
|
|
||||||
|
#error a, .error a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#warmtips {
|
||||||
|
background: #ffcdcd;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
#warmtips a {
|
||||||
|
background: #ffffff7a;
|
||||||
|
display: block;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #e21a1a;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>
|
||||||
|
<svg width="80px" height="96px" viewBox="0 0 768 830" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="logo" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<path d="M64.433651,605.899968 C20.067302,536.265612 0,469.698785 0,389.731348 C0,174.488668 171.922656,0 384,0 C596.077344,0 768,174.488668 768,389.731348 C768,469.698785 747.932698,536.265612 703.566349,605.899968 C614.4,753.480595 441.6,870.4 384,870.4 C326.4,870.4 153.6,753.480595 64.433651,605.899968 L64.433651,605.899968 Z"
|
||||||
|
id="body" fill="#4e73df"></path>
|
||||||
|
<path d="M429.648991,190.816 L430.160991,190.816 L429.648991,190.816 L429.648991,190.816 Z M429.648991,156 L427.088991,156 C419.408991,157.024 411.728991,160.608 404.560991,168.8 L403.024991,170.848 L206.928991,429.92 C198.736991,441.184 197.712991,453.984 204.368991,466.784 C210.512991,478.048 222.288991,485.728 235.600991,485.728 L336.464991,486.24 L304.208991,673.632 C301.648991,689.504 310.352991,705.376 325.200991,712.032 C329.808991,714.08 334.416991,714.592 339.536991,714.592 C349.776991,714.592 358.992991,709.472 366.160991,700.256 L561.744991,419.168 C569.936991,407.904 570.960991,395.104 564.304991,382.304 C557.648991,369.504 547.408991,363.36 533.072991,363.36 L432.208991,363.36 L463.952991,199.008 C464.464991,196.448 464.976991,193.376 464.976991,190.816 C464.976991,171.872 449.104991,156 431.184991,156 L429.648991,156 L429.648991,156 Z"
|
||||||
|
id="flash" fill="#FFFFFF"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</h1>
|
||||||
|
<h2>{:__('Installing FastAdmin')}</h2>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{if $errInfo}
|
||||||
|
<div class="error">
|
||||||
|
{$errInfo}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div id="error" style="display:none"></div>
|
||||||
|
<div id="success" style="display:none"></div>
|
||||||
|
<div id="warmtips" style="display:none"></div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-field">
|
||||||
|
<label>{:__('Mysql Hostname')}</label>
|
||||||
|
<input type="text" name="mysqlHostname" value="127.0.0.1" required="">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label>{:__('Mysql Database')}</label>
|
||||||
|
<input type="text" name="mysqlDatabase" value="" required="">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label>{:__('Mysql Username')}</label>
|
||||||
|
<input type="text" name="mysqlUsername" value="root" required="">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label>{:__('Mysql Password')}</label>
|
||||||
|
<input type="password" name="mysqlPassword">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label>{:__('Mysql Prefix')}</label>
|
||||||
|
<input type="text" name="mysqlPrefix" value="fa_">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label>{:__('Mysql Hostport')}</label>
|
||||||
|
<input type="number" name="mysqlHostport" value="3306">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-field">
|
||||||
|
<label>{:__('Admin Username')}</label>
|
||||||
|
<input name="adminUsername" value="admin" required=""/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label>{:__('Admin Email')}</label>
|
||||||
|
<input name="adminEmail" value="admin@admin.com" required="">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label>{:__('Admin Password')}</label>
|
||||||
|
<input type="password" name="adminPassword" required="">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label>{:__('Repeat Password')}</label>
|
||||||
|
<input type="password" name="adminPasswordConfirmation" required="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-field">
|
||||||
|
<label>{:__('Website')}</label>
|
||||||
|
<input type="text" name="siteName" value="{:__('My Website')}" required=""/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-buttons">
|
||||||
|
<!--@formatter:off-->
|
||||||
|
<button type="submit" {:$errInfo?'disabled':''}>{:__('Install now')}</button>
|
||||||
|
<!--@formatter:on-->
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- jQuery -->
|
||||||
|
<script src="__ROOT__/assets/libs/jquery/dist/jquery.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(function () {
|
||||||
|
$('form :input:first').select();
|
||||||
|
|
||||||
|
$('form').on('submit', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var form = this;
|
||||||
|
var $error = $("#error");
|
||||||
|
var $success = $("#success");
|
||||||
|
var $button = $(this).find('button')
|
||||||
|
.text("{:__('Installing')}")
|
||||||
|
.prop('disabled', true);
|
||||||
|
$.ajax({
|
||||||
|
url: "",
|
||||||
|
type: "POST",
|
||||||
|
dataType: "json",
|
||||||
|
data: $(this).serialize(),
|
||||||
|
success: function (ret) {
|
||||||
|
if (ret.code == 1) {
|
||||||
|
var data = ret.data;
|
||||||
|
$error.hide();
|
||||||
|
$(".form-group", form).remove();
|
||||||
|
$button.remove();
|
||||||
|
$("#success").text(ret.msg).show();
|
||||||
|
|
||||||
|
$buttons = $(".form-buttons", form);
|
||||||
|
$("<a class='btn' href='./'>{:__('Home')}</a>").appendTo($buttons);
|
||||||
|
|
||||||
|
if (typeof data.adminName !== 'undefined') {
|
||||||
|
var url = location.href.replace(/install\.php/, data.adminName);
|
||||||
|
$("#warmtips").html("{:__('Security tips')}" + '<a href="' + url + '">' + url + '</a>').show();
|
||||||
|
$('<a class="btn" href="' + url + '" id="btn-admin" style="background:#4e73df">' + "{:__('Dashboard')}" + '</a>').appendTo($buttons);
|
||||||
|
}
|
||||||
|
localStorage.setItem("fastep", "installed");
|
||||||
|
} else {
|
||||||
|
$error.show().text(ret.msg);
|
||||||
|
$button.prop('disabled', false).text("{:__('Install now')}");
|
||||||
|
$("html,body").animate({
|
||||||
|
scrollTop: 0
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
$error.show().text(xhr.responseText);
|
||||||
|
$button.prop('disabled', false).text("{:__('Install now')}");
|
||||||
|
$("html,body").animate({
|
||||||
|
scrollTop: 0
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
return [
|
||||||
|
'Warning' => '温馨提示',
|
||||||
|
'Installing FastAdmin' => '安装FastAdmin',
|
||||||
|
'Mysql Hostname' => 'MySQL 数据库地址',
|
||||||
|
'Mysql Database' => 'MySQL 数据库名',
|
||||||
|
'Mysql Username' => 'MySQL 用户名',
|
||||||
|
'Mysql Password' => 'MySQL 密码',
|
||||||
|
'Mysql Prefix' => 'MySQL 数据表前缀',
|
||||||
|
'Mysql Hostport' => 'MySQL 端口号',
|
||||||
|
'Admin Username' => '管理员用户名',
|
||||||
|
'Admin Email' => '管理员Email',
|
||||||
|
'Admin Password' => '管理员密码',
|
||||||
|
'Repeat Password' => '重复管理员密码',
|
||||||
|
'Website' => '网站名称',
|
||||||
|
'My Website' => '我的网站',
|
||||||
|
'Install now' => '点击安装',
|
||||||
|
'Installing' => '安装中...',
|
||||||
|
'Home' => '访问首页',
|
||||||
|
'Dashboard' => '进入后台',
|
||||||
|
'Go back' => '返回上一页',
|
||||||
|
'Install Successed' => '安装成功!',
|
||||||
|
'Security tips' => '温馨提示:请将以下后台登录入口添加到你的收藏夹,为了你的站点安全,不要泄漏或发送给他人!如有泄漏请及时修改!',
|
||||||
|
'Please input correct database' => '请输入正确的数据库名',
|
||||||
|
'Please input correct username' => '用户名只能由3-30位数字、字母、下划线组合',
|
||||||
|
'Please input correct password' => '密码长度必须在6-30位之间,不能包含空格',
|
||||||
|
'Password is too weak' => '密码太简单,请重新输入',
|
||||||
|
'The two passwords you entered did not match' => '两次输入的密码不一致',
|
||||||
|
'Please input correct website' => '网站名称输入不正确',
|
||||||
|
'The current PHP %s is too low, please use PHP %s or higher' => '当前版本PHP %s过低,请使用PHP %s及以上版本',
|
||||||
|
'PDO is not currently installed and cannot be installed' => '当前未开启PDO,无法进行安装',
|
||||||
|
'The current permissions are insufficient to write the file %s' => '当前权限不足,无法写入文件%s',
|
||||||
|
'Please go to the official website to download the full package or resource package and try to install' => '当前代码仅包含核心代码,请前往官网下载完整包或资源包覆盖后再尝试安装',
|
||||||
|
'The system has been installed. If you need to reinstall, please remove %s first' => '当前已经安装成功,如果需要重新安装,请手动移除%s文件',
|
||||||
|
];
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\command;
|
||||||
|
|
||||||
|
use app\admin\model\AuthRule;
|
||||||
|
use ReflectionClass;
|
||||||
|
use ReflectionMethod;
|
||||||
|
use think\Cache;
|
||||||
|
use think\Config;
|
||||||
|
use think\console\Command;
|
||||||
|
use think\console\Input;
|
||||||
|
use think\console\input\Option;
|
||||||
|
use think\console\Output;
|
||||||
|
use think\Exception;
|
||||||
|
use think\Loader;
|
||||||
|
|
||||||
|
class Menu extends Command
|
||||||
|
{
|
||||||
|
protected $model = null;
|
||||||
|
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setName('menu')
|
||||||
|
->addOption('controller', 'c', Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, 'controller name,use \'all-controller\' when build all menu', null)
|
||||||
|
->addOption('delete', 'd', Option::VALUE_OPTIONAL, 'delete the specified menu', '')
|
||||||
|
->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force delete menu,without tips', null)
|
||||||
|
->addOption('equal', 'e', Option::VALUE_OPTIONAL, 'the controller must be equal', null)
|
||||||
|
->setDescription('Build auth menu from controller');
|
||||||
|
//要执行的controller必须一样,不适用模糊查询
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(Input $input, Output $output)
|
||||||
|
{
|
||||||
|
$this->model = new AuthRule();
|
||||||
|
$adminPath = dirname(__DIR__) . DS;
|
||||||
|
//控制器名
|
||||||
|
$controller = $input->getOption('controller') ?: '';
|
||||||
|
if (!$controller) {
|
||||||
|
throw new Exception("please input controller name");
|
||||||
|
}
|
||||||
|
$force = $input->getOption('force');
|
||||||
|
//是否为删除模式
|
||||||
|
$delete = $input->getOption('delete');
|
||||||
|
//是否控制器完全匹配
|
||||||
|
$equal = $input->getOption('equal');
|
||||||
|
|
||||||
|
|
||||||
|
if ($delete) {
|
||||||
|
if (in_array('all-controller', $controller)) {
|
||||||
|
throw new Exception("could not delete all menu");
|
||||||
|
}
|
||||||
|
$ids = [];
|
||||||
|
$list = $this->model->where(function ($query) use ($controller, $equal) {
|
||||||
|
foreach ($controller as $index => $item) {
|
||||||
|
if (stripos($item, '_') !== false) {
|
||||||
|
$item = Loader::parseName($item, 1);
|
||||||
|
}
|
||||||
|
if (stripos($item, '/') !== false) {
|
||||||
|
$controllerArr = explode('/', $item);
|
||||||
|
end($controllerArr);
|
||||||
|
$key = key($controllerArr);
|
||||||
|
$controllerArr[$key] = Loader::parseName($controllerArr[$key]);
|
||||||
|
} else {
|
||||||
|
$controllerArr = [Loader::parseName($item)];
|
||||||
|
}
|
||||||
|
$item = str_replace('_', '\_', implode('/', $controllerArr));
|
||||||
|
if ($equal) {
|
||||||
|
$query->whereOr('name', 'eq', $item);
|
||||||
|
} else {
|
||||||
|
$query->whereOr('name', 'like', strtolower($item) . "%");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})->select();
|
||||||
|
foreach ($list as $k => $v) {
|
||||||
|
$output->warning($v->name);
|
||||||
|
$ids[] = $v->id;
|
||||||
|
}
|
||||||
|
if (!$ids) {
|
||||||
|
throw new Exception("There is no menu to delete");
|
||||||
|
}
|
||||||
|
if (!$force) {
|
||||||
|
$output->info("Are you sure you want to delete all those menu? Type 'yes' to continue: ");
|
||||||
|
$line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r'));
|
||||||
|
if (trim($line) != 'yes') {
|
||||||
|
throw new Exception("Operation is aborted!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AuthRule::destroy($ids);
|
||||||
|
|
||||||
|
Cache::rm("__menu__");
|
||||||
|
$output->info("Delete Successed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array('all-controller', $controller)) {
|
||||||
|
foreach ($controller as $index => $item) {
|
||||||
|
if (stripos($item, '_') !== false) {
|
||||||
|
$item = Loader::parseName($item, 1);
|
||||||
|
}
|
||||||
|
if (stripos($item, '/') !== false) {
|
||||||
|
$controllerArr = explode('/', $item);
|
||||||
|
end($controllerArr);
|
||||||
|
$key = key($controllerArr);
|
||||||
|
$controllerArr[$key] = ucfirst($controllerArr[$key]);
|
||||||
|
} else {
|
||||||
|
$controllerArr = [ucfirst($item)];
|
||||||
|
}
|
||||||
|
$adminPath = dirname(__DIR__) . DS . 'controller' . DS . implode(DS, $controllerArr) . '.php';
|
||||||
|
if (!is_file($adminPath)) {
|
||||||
|
$output->error("controller not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->importRule($item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$authRuleList = AuthRule::select();
|
||||||
|
//生成权限规则备份文件
|
||||||
|
file_put_contents(RUNTIME_PATH . 'authrule.json', json_encode(collection($authRuleList)->toArray()));
|
||||||
|
|
||||||
|
$this->model->where('id', '>', 0)->delete();
|
||||||
|
$controllerDir = $adminPath . 'controller' . DS;
|
||||||
|
// 扫描新的节点信息并导入
|
||||||
|
$treelist = $this->import($this->scandir($controllerDir));
|
||||||
|
}
|
||||||
|
Cache::rm("__menu__");
|
||||||
|
$output->info("Build Successed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归扫描文件夹
|
||||||
|
* @param string $dir
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function scandir($dir)
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
$cdir = scandir($dir);
|
||||||
|
foreach ($cdir as $value) {
|
||||||
|
if (!in_array($value, array(".", ".."))) {
|
||||||
|
if (is_dir($dir . DS . $value)) {
|
||||||
|
$result[$value] = $this->scandir($dir . DS . $value);
|
||||||
|
} else {
|
||||||
|
$result[] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入规则节点
|
||||||
|
* @param array $dirarr
|
||||||
|
* @param array $parentdir
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function import($dirarr, $parentdir = [])
|
||||||
|
{
|
||||||
|
$menuarr = [];
|
||||||
|
foreach ($dirarr as $k => $v) {
|
||||||
|
if (is_array($v)) {
|
||||||
|
//当前是文件夹
|
||||||
|
$nowparentdir = array_merge($parentdir, [$k]);
|
||||||
|
$this->import($v, $nowparentdir);
|
||||||
|
} else {
|
||||||
|
//只匹配PHP文件
|
||||||
|
if (!preg_match('/^(\w+)\.php$/', $v, $matchone)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
//导入文件
|
||||||
|
$controller = ($parentdir ? implode('/', $parentdir) . '/' : '') . $matchone[1];
|
||||||
|
$this->importRule($controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $menuarr;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function importRule($controller)
|
||||||
|
{
|
||||||
|
$controller = str_replace('\\', '/', $controller);
|
||||||
|
if (stripos($controller, '/') !== false) {
|
||||||
|
$controllerArr = explode('/', $controller);
|
||||||
|
end($controllerArr);
|
||||||
|
$key = key($controllerArr);
|
||||||
|
$controllerArr[$key] = ucfirst($controllerArr[$key]);
|
||||||
|
} else {
|
||||||
|
$key = 0;
|
||||||
|
$controllerArr = [ucfirst($controller)];
|
||||||
|
}
|
||||||
|
$classSuffix = Config::get('controller_suffix') ? ucfirst(Config::get('url_controller_layer')) : '';
|
||||||
|
$className = "\\app\\admin\\controller\\" . implode("\\", $controllerArr) . $classSuffix;
|
||||||
|
|
||||||
|
$pathArr = $controllerArr;
|
||||||
|
array_unshift($pathArr, '', 'application', 'admin', 'controller');
|
||||||
|
$classFile = ROOT_PATH . implode(DS, $pathArr) . $classSuffix . ".php";
|
||||||
|
$classContent = file_get_contents($classFile);
|
||||||
|
$uniqueName = uniqid("FastAdmin") . $classSuffix;
|
||||||
|
$classContent = str_replace("class " . $controllerArr[$key] . $classSuffix . " ", 'class ' . $uniqueName . ' ', $classContent);
|
||||||
|
$classContent = preg_replace("/namespace\s(.*);/", 'namespace ' . __NAMESPACE__ . ";", $classContent);
|
||||||
|
|
||||||
|
//临时的类文件
|
||||||
|
$tempClassFile = __DIR__ . DS . $uniqueName . ".php";
|
||||||
|
file_put_contents($tempClassFile, $classContent);
|
||||||
|
$className = "\\app\\admin\\command\\" . $uniqueName;
|
||||||
|
|
||||||
|
//删除临时文件
|
||||||
|
register_shutdown_function(function () use ($tempClassFile) {
|
||||||
|
if ($tempClassFile) {
|
||||||
|
//删除临时文件
|
||||||
|
@unlink($tempClassFile);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//反射机制调用类的注释和方法名
|
||||||
|
$reflector = new ReflectionClass($className);
|
||||||
|
|
||||||
|
//只匹配公共的方法
|
||||||
|
$methods = $reflector->getMethods(ReflectionMethod::IS_PUBLIC);
|
||||||
|
$classComment = $reflector->getDocComment();
|
||||||
|
//判断是否有启用软删除
|
||||||
|
$softDeleteMethods = ['destroy', 'restore', 'recyclebin'];
|
||||||
|
$withSofeDelete = false;
|
||||||
|
$modelRegexArr = ["/\\\$this\->model\s*=\s*model\(['|\"](\w+)['|\"]\);/", "/\\\$this\->model\s*=\s*new\s+([a-zA-Z\\\]+);/"];
|
||||||
|
$modelRegex = preg_match($modelRegexArr[0], $classContent) ? $modelRegexArr[0] : $modelRegexArr[1];
|
||||||
|
preg_match_all($modelRegex, $classContent, $matches);
|
||||||
|
if (isset($matches[1]) && isset($matches[1][0]) && $matches[1][0]) {
|
||||||
|
\think\Request::instance()->module('admin');
|
||||||
|
$model = model($matches[1][0]);
|
||||||
|
if (in_array('trashed', get_class_methods($model))) {
|
||||||
|
$withSofeDelete = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//忽略的类
|
||||||
|
if (stripos($classComment, "@internal") !== false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
preg_match_all('#(@.*?)\n#s', $classComment, $annotations);
|
||||||
|
$controllerIcon = 'fa fa-circle-o';
|
||||||
|
$controllerRemark = '';
|
||||||
|
//判断注释中是否设置了icon值
|
||||||
|
if (isset($annotations[1])) {
|
||||||
|
foreach ($annotations[1] as $tag) {
|
||||||
|
if (stripos($tag, '@icon') !== false) {
|
||||||
|
$controllerIcon = substr($tag, stripos($tag, ' ') + 1);
|
||||||
|
}
|
||||||
|
if (stripos($tag, '@remark') !== false) {
|
||||||
|
$controllerRemark = substr($tag, stripos($tag, ' ') + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//过滤掉其它字符
|
||||||
|
$controllerTitle = trim(preg_replace(array('/^\/\*\*(.*)[\n\r\t]/u', '/[\s]+\*\//u', '/\*\s@(.*)/u', '/[\s|\*]+/u'), '', $classComment));
|
||||||
|
|
||||||
|
//导入中文语言包
|
||||||
|
\think\Lang::load(dirname(__DIR__) . DS . 'lang/zh-cn.php');
|
||||||
|
|
||||||
|
//先导入菜单的数据
|
||||||
|
$pid = 0;
|
||||||
|
foreach ($controllerArr as $k => $v) {
|
||||||
|
$key = $k + 1;
|
||||||
|
//驼峰转下划线
|
||||||
|
$controllerNameArr = array_slice($controllerArr, 0, $key);
|
||||||
|
foreach ($controllerNameArr as &$val) {
|
||||||
|
$val = strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $val), "_"));
|
||||||
|
}
|
||||||
|
unset($val);
|
||||||
|
$name = implode('/', $controllerNameArr);
|
||||||
|
$title = (!isset($controllerArr[$key]) ? $controllerTitle : '');
|
||||||
|
$icon = (!isset($controllerArr[$key]) ? $controllerIcon : 'fa fa-list');
|
||||||
|
$remark = (!isset($controllerArr[$key]) ? $controllerRemark : '');
|
||||||
|
$title = $title ? $title : $v;
|
||||||
|
$rulemodel = $this->model->get(['name' => $name]);
|
||||||
|
if (!$rulemodel) {
|
||||||
|
$this->model
|
||||||
|
->data(['pid' => $pid, 'name' => $name, 'title' => $title, 'icon' => $icon, 'remark' => $remark, 'ismenu' => 1, 'status' => 'normal'])
|
||||||
|
->isUpdate(false)
|
||||||
|
->save();
|
||||||
|
$pid = $this->model->id;
|
||||||
|
} else {
|
||||||
|
$pid = $rulemodel->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$ruleArr = [];
|
||||||
|
foreach ($methods as $m => $n) {
|
||||||
|
//过滤特殊的类
|
||||||
|
if (substr($n->name, 0, 2) == '__' || $n->name == '_initialize') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
//未启用软删除时过滤相关方法
|
||||||
|
if (!$withSofeDelete && in_array($n->name, $softDeleteMethods)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
//只匹配符合的方法
|
||||||
|
if (!preg_match('/^(\w+)' . Config::get('action_suffix') . '/', $n->name, $matchtwo)) {
|
||||||
|
unset($methods[$m]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$comment = $reflector->getMethod($n->name)->getDocComment();
|
||||||
|
//忽略的方法
|
||||||
|
if (stripos($comment, "@internal") !== false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
//过滤掉其它字符
|
||||||
|
$comment = preg_replace(array('/^\/\*\*(.*)[\n\r\t]/u', '/[\s]+\*\//u', '/\*\s@(.*)/u', '/[\s|\*]+/u'), '', $comment);
|
||||||
|
|
||||||
|
$title = $comment ? $comment : ucfirst($n->name);
|
||||||
|
|
||||||
|
//获取主键,作为AuthRule更新依据
|
||||||
|
$id = $this->getAuthRulePK($name . "/" . strtolower($n->name));
|
||||||
|
|
||||||
|
$ruleArr[] = array('id' => $id, 'pid' => $pid, 'name' => $name . "/" . strtolower($n->name), 'icon' => 'fa fa-circle-o', 'title' => $title, 'ismenu' => 0, 'status' => 'normal');
|
||||||
|
}
|
||||||
|
$this->model->isUpdate(false)->saveAll($ruleArr);
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取主键
|
||||||
|
protected function getAuthRulePK($name)
|
||||||
|
{
|
||||||
|
if (!empty($name)) {
|
||||||
|
$id = $this->model
|
||||||
|
->where('name', $name)
|
||||||
|
->value('id');
|
||||||
|
return $id ? $id : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\command;
|
||||||
|
|
||||||
|
use think\console\Command;
|
||||||
|
use think\console\Input;
|
||||||
|
use think\console\input\Option;
|
||||||
|
use think\console\Output;
|
||||||
|
use think\Exception;
|
||||||
|
|
||||||
|
class Min extends Command
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路径和文件名配置
|
||||||
|
*/
|
||||||
|
protected $options = [
|
||||||
|
'cssBaseUrl' => 'public/assets/css/',
|
||||||
|
'cssBaseName' => '{module}',
|
||||||
|
'jsBaseUrl' => 'public/assets/js/',
|
||||||
|
'jsBaseName' => 'require-{module}',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setName('min')
|
||||||
|
->addOption('module', 'm', Option::VALUE_REQUIRED, 'module name(frontend or backend),use \'all\' when build all modules', null)
|
||||||
|
->addOption('resource', 'r', Option::VALUE_REQUIRED, 'resource name(js or css),use \'all\' when build all resources', null)
|
||||||
|
->addOption('optimize', 'o', Option::VALUE_OPTIONAL, 'optimize type(uglify|closure|none)', 'none')
|
||||||
|
->setDescription('Compress js and css file');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(Input $input, Output $output)
|
||||||
|
{
|
||||||
|
$module = $input->getOption('module') ?: '';
|
||||||
|
$resource = $input->getOption('resource') ?: '';
|
||||||
|
$optimize = $input->getOption('optimize') ?: 'none';
|
||||||
|
|
||||||
|
if (!$module || !in_array($module, ['frontend', 'backend', 'all'])) {
|
||||||
|
throw new Exception('Please input correct module name');
|
||||||
|
}
|
||||||
|
if (!$resource || !in_array($resource, ['js', 'css', 'all'])) {
|
||||||
|
throw new Exception('Please input correct resource name');
|
||||||
|
}
|
||||||
|
|
||||||
|
$moduleArr = $module == 'all' ? ['frontend', 'backend'] : [$module];
|
||||||
|
$resourceArr = $resource == 'all' ? ['js', 'css'] : [$resource];
|
||||||
|
|
||||||
|
$minPath = __DIR__ . DS . 'Min' . DS;
|
||||||
|
$publicPath = ROOT_PATH . 'public' . DS;
|
||||||
|
$tempFile = $minPath . 'temp.js';
|
||||||
|
|
||||||
|
$nodeExec = '';
|
||||||
|
|
||||||
|
if (!$nodeExec) {
|
||||||
|
if (IS_WIN) {
|
||||||
|
// Winsows下请手动配置配置该值,一般将该值配置为 '"C:\Program Files\nodejs\node.exe"',除非你的Node安装路径有变更
|
||||||
|
$nodeExec = 'C:\Program Files\nodejs\node.exe';
|
||||||
|
if (file_exists($nodeExec)) {
|
||||||
|
$nodeExec = '"' . $nodeExec . '"';
|
||||||
|
} else {
|
||||||
|
// 如果 '"C:\Program Files\nodejs\node.exe"' 不存在,可能是node安装路径有变更
|
||||||
|
// 但安装node会自动配置环境变量,直接执行 '"node.exe"' 提高第一次使用压缩打包的成功率
|
||||||
|
$nodeExec = '"node.exe"';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$nodeExec = exec("which node");
|
||||||
|
if (!$nodeExec) {
|
||||||
|
throw new Exception("node environment not found!please install node first!");
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
throw new Exception($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($moduleArr as $mod) {
|
||||||
|
foreach ($resourceArr as $res) {
|
||||||
|
$data = [
|
||||||
|
'publicPath' => $publicPath,
|
||||||
|
'jsBaseName' => str_replace('{module}', $mod, $this->options['jsBaseName']),
|
||||||
|
'jsBaseUrl' => $this->options['jsBaseUrl'],
|
||||||
|
'cssBaseName' => str_replace('{module}', $mod, $this->options['cssBaseName']),
|
||||||
|
'cssBaseUrl' => $this->options['cssBaseUrl'],
|
||||||
|
'jsBasePath' => str_replace(DS, '/', ROOT_PATH . $this->options['jsBaseUrl']),
|
||||||
|
'cssBasePath' => str_replace(DS, '/', ROOT_PATH . $this->options['cssBaseUrl']),
|
||||||
|
'optimize' => $optimize,
|
||||||
|
'ds' => DS,
|
||||||
|
];
|
||||||
|
|
||||||
|
//源文件
|
||||||
|
$from = $data["{$res}BasePath"] . $data["{$res}BaseName"] . '.' . $res;
|
||||||
|
if (!is_file($from)) {
|
||||||
|
$output->error("{$res} source file not found!file:{$from}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($res == "js") {
|
||||||
|
$content = file_get_contents($from);
|
||||||
|
preg_match("/require\.config\(\{[\r\n]?[\n]?+(.*?)[\r\n]?[\n]?}\);/is", $content, $matches);
|
||||||
|
if (!isset($matches[1])) {
|
||||||
|
$output->error("js config not found!");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$config = preg_replace("/(urlArgs|baseUrl):(.*)\n/", '', $matches[1]);
|
||||||
|
$config = preg_replace("/('tableexport'):(.*)\,\n/", "'tableexport': 'empty:',\n", $config);
|
||||||
|
$data['config'] = $config;
|
||||||
|
}
|
||||||
|
// 生成压缩文件
|
||||||
|
$this->writeToFile($res, $data, $tempFile);
|
||||||
|
|
||||||
|
$output->info("Compress " . $data["{$res}BaseName"] . ".{$res}");
|
||||||
|
|
||||||
|
// 执行压缩
|
||||||
|
$command = "{$nodeExec} \"{$minPath}r.js\" -o \"{$tempFile}\" >> \"{$minPath}node.log\"";
|
||||||
|
if ($output->isDebug()) {
|
||||||
|
$output->warning($command);
|
||||||
|
}
|
||||||
|
echo exec($command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$output->isDebug()) {
|
||||||
|
@unlink($tempFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->info("Build Successed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入到文件
|
||||||
|
* @param string $name
|
||||||
|
* @param array $data
|
||||||
|
* @param string $pathname
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
protected function writeToFile($name, $data, $pathname)
|
||||||
|
{
|
||||||
|
$search = $replace = [];
|
||||||
|
foreach ($data as $k => $v) {
|
||||||
|
$search[] = "{%{$k}%}";
|
||||||
|
$replace[] = $v;
|
||||||
|
}
|
||||||
|
$stub = file_get_contents($this->getStub($name));
|
||||||
|
$content = str_replace($search, $replace, $stub);
|
||||||
|
|
||||||
|
if (!is_dir(dirname($pathname))) {
|
||||||
|
mkdir(strtolower(dirname($pathname)), 0755, true);
|
||||||
|
}
|
||||||
|
return file_put_contents($pathname, $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取基础模板
|
||||||
|
* @param string $name
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getStub($name)
|
||||||
|
{
|
||||||
|
return __DIR__ . DS . 'Min' . DS . 'stubs' . DS . $name . '.stub';
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,6 @@
|
|||||||
|
({
|
||||||
|
cssIn: "{%cssBasePath%}{%cssBaseName%}.css",
|
||||||
|
out: "{%cssBasePath%}{%cssBaseName%}.min.css",
|
||||||
|
optimizeCss: "default",
|
||||||
|
optimize: "{%optimize%}"
|
||||||
|
})
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
({
|
||||||
|
{%config%}
|
||||||
|
,
|
||||||
|
optimizeCss: "standard",
|
||||||
|
optimize: "{%optimize%}", //可使用uglify|closure|none
|
||||||
|
preserveLicenseComments: false,
|
||||||
|
removeCombined: false,
|
||||||
|
baseUrl: "{%jsBasePath%}", //JS文件所在的基础目录
|
||||||
|
name: "{%jsBaseName%}", //来源文件,不包含后缀
|
||||||
|
out: "{%jsBasePath%}{%jsBaseName%}.min.js" //目标文件
|
||||||
|
});
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use app\common\model\Category;
|
||||||
|
use fast\Form;
|
||||||
|
use fast\Tree;
|
||||||
|
use think\Db;
|
||||||
|
use think\Loader;
|
||||||
|
|
||||||
|
if (!function_exists('build_select')) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成下拉列表
|
||||||
|
* @param string $name
|
||||||
|
* @param mixed $options
|
||||||
|
* @param mixed $selected
|
||||||
|
* @param mixed $attr
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function build_select($name, $options, $selected = [], $attr = [])
|
||||||
|
{
|
||||||
|
$options = is_array($options) ? $options : explode(',', $options ?? '');
|
||||||
|
$selected = is_array($selected) ? $selected : explode(',', $selected ?? '');
|
||||||
|
return Form::select($name, $options, $selected, $attr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('build_radios')) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成单选按钮组
|
||||||
|
* @param string $name
|
||||||
|
* @param array $list
|
||||||
|
* @param mixed $selected
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function build_radios($name, $list = [], $selected = null)
|
||||||
|
{
|
||||||
|
$html = [];
|
||||||
|
$selected = is_null($selected) ? key($list) : $selected;
|
||||||
|
$selected = is_array($selected) ? $selected : explode(',', $selected);
|
||||||
|
foreach ($list as $k => $v) {
|
||||||
|
$html[] = sprintf(Form::label("{$name}-{$k}", "%s " . str_replace('%', '%%', $v)), Form::radio($name, $k, in_array($k, $selected), ['id' => "{$name}-{$k}"]));
|
||||||
|
}
|
||||||
|
return '<div class="radio">' . implode(' ', $html) . '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('build_checkboxs')) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成复选按钮组
|
||||||
|
* @param string $name
|
||||||
|
* @param array $list
|
||||||
|
* @param mixed $selected
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function build_checkboxs($name, $list = [], $selected = null)
|
||||||
|
{
|
||||||
|
$html = [];
|
||||||
|
$selected = is_null($selected) ? [] : $selected;
|
||||||
|
$selected = is_array($selected) ? $selected : explode(',', $selected);
|
||||||
|
foreach ($list as $k => $v) {
|
||||||
|
$html[] = sprintf(Form::label("{$name}-{$k}", "%s " . str_replace('%', '%%', $v)), Form::checkbox($name, $k, in_array($k, $selected), ['id' => "{$name}-{$k}"]));
|
||||||
|
}
|
||||||
|
return '<div class="checkbox">' . implode(' ', $html) . '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!function_exists('build_category_select')) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成分类下拉列表框
|
||||||
|
* @param string $name
|
||||||
|
* @param string $type
|
||||||
|
* @param mixed $selected
|
||||||
|
* @param array $attr
|
||||||
|
* @param array $header
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function build_category_select($name, $type, $selected = null, $attr = [], $header = [])
|
||||||
|
{
|
||||||
|
$tree = Tree::instance();
|
||||||
|
$tree->init(Category::getCategoryArray($type), 'pid');
|
||||||
|
$categorylist = $tree->getTreeList($tree->getTreeArray(0), 'name');
|
||||||
|
$categorydata = $header ? $header : [];
|
||||||
|
foreach ($categorylist as $k => $v) {
|
||||||
|
$categorydata[$v['id']] = $v['name'];
|
||||||
|
}
|
||||||
|
$attr = array_merge(['id' => "c-{$name}", 'class' => 'form-control selectpicker'], $attr);
|
||||||
|
return build_select($name, $categorydata, $selected, $attr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('build_toolbar')) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成表格操作按钮栏
|
||||||
|
* @param array $btns 按钮组
|
||||||
|
* @param array $attr 按钮属性值
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function build_toolbar($btns = null, $attr = [])
|
||||||
|
{
|
||||||
|
$auth = \app\admin\library\Auth::instance();
|
||||||
|
$controller = str_replace('.', '/', Loader::parseName(request()->controller()));
|
||||||
|
$btns = $btns ? $btns : ['refresh', 'add', 'edit', 'del', 'import'];
|
||||||
|
$btns = is_array($btns) ? $btns : explode(',', $btns);
|
||||||
|
$index = array_search('delete', $btns);
|
||||||
|
if ($index !== false) {
|
||||||
|
$btns[$index] = 'del';
|
||||||
|
}
|
||||||
|
$btnAttr = [
|
||||||
|
'refresh' => ['javascript:;', 'btn btn-primary btn-refresh', 'fa fa-refresh', '', __('Refresh')],
|
||||||
|
'add' => ['javascript:;', 'btn btn-success btn-add', 'fa fa-plus', __('Add'), __('Add')],
|
||||||
|
'edit' => ['javascript:;', 'btn btn-success btn-edit btn-disabled disabled', 'fa fa-pencil', __('Edit'), __('Edit')],
|
||||||
|
'del' => ['javascript:;', 'btn btn-danger btn-del btn-disabled disabled', 'fa fa-trash', __('Delete'), __('Delete')],
|
||||||
|
'import' => ['javascript:;', 'btn btn-info btn-import', 'fa fa-upload', __('Import'), __('Import')],
|
||||||
|
];
|
||||||
|
$btnAttr = array_merge($btnAttr, $attr);
|
||||||
|
$html = [];
|
||||||
|
foreach ($btns as $k => $v) {
|
||||||
|
//如果未定义或没有权限
|
||||||
|
if (!isset($btnAttr[$v]) || ($v !== 'refresh' && !$auth->check("{$controller}/{$v}", $auth->id))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
list($href, $class, $icon, $text, $title) = $btnAttr[$v];
|
||||||
|
//$extend = $v == 'import' ? 'id="btn-import-file" data-url="ajax/upload" data-mimetype="csv,xls,xlsx" data-multiple="false"' : '';
|
||||||
|
//$html[] = '<a href="' . $href . '" class="' . $class . '" title="' . $title . '" ' . $extend . '><i class="' . $icon . '"></i> ' . $text . '</a>';
|
||||||
|
if ($v == 'import') {
|
||||||
|
$template = str_replace('/', '_', $controller);
|
||||||
|
$download = '';
|
||||||
|
if (file_exists("./template/{$template}.xlsx")) {
|
||||||
|
$download .= "<li><a href=\"/template/{$template}.xlsx\" target=\"_blank\">XLSX模版</a></li>";
|
||||||
|
}
|
||||||
|
if (file_exists("./template/{$template}.xls")) {
|
||||||
|
$download .= "<li><a href=\"/template/{$template}.xls\" target=\"_blank\">XLS模版</a></li>";
|
||||||
|
}
|
||||||
|
if (file_exists("./template/{$template}.csv")) {
|
||||||
|
$download .= empty($download) ? '' : "<li class=\"divider\"></li>";
|
||||||
|
$download .= "<li><a href=\"/template/{$template}.csv\" target=\"_blank\">CSV模版</a></li>";
|
||||||
|
}
|
||||||
|
$download .= empty($download) ? '' : "\n ";
|
||||||
|
if (!empty($download)) {
|
||||||
|
$html[] = <<<EOT
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" href="{$href}" class="btn btn-info btn-import" title="{$title}" id="btn-import-file" data-url="ajax/upload" data-mimetype="csv,xls,xlsx" data-multiple="false"><i class="{$icon}"></i> {$text}</button>
|
||||||
|
<button type="button" class="btn btn-info dropdown-toggle" data-toggle="dropdown" title="下载批量导入模版">
|
||||||
|
<span class="caret"></span>
|
||||||
|
<span class="sr-only">Toggle Dropdown</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu" role="menu">{$download}</ul>
|
||||||
|
</div>
|
||||||
|
EOT;
|
||||||
|
} else {
|
||||||
|
$html[] = '<a href="' . $href . '" class="' . $class . '" title="' . $title . '" id="btn-import-file" data-url="ajax/upload" data-mimetype="csv,xls,xlsx" data-multiple="false"><i class="' . $icon . '"></i> ' . $text . '</a>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$html[] = '<a href="' . $href . '" class="' . $class . '" title="' . $title . '"><i class="' . $icon . '"></i> ' . $text . '</a>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return implode(' ', $html);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('build_heading')) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成页面Heading
|
||||||
|
*
|
||||||
|
* @param string $path 指定的path
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function build_heading($path = null, $container = true)
|
||||||
|
{
|
||||||
|
$title = $content = '';
|
||||||
|
if (is_null($path)) {
|
||||||
|
$action = request()->action();
|
||||||
|
$controller = str_replace('.', '/', Loader::parseName(request()->controller()));
|
||||||
|
$path = strtolower($controller . ($action && $action != 'index' ? '/' . $action : ''));
|
||||||
|
}
|
||||||
|
// 根据当前的URI自动匹配父节点的标题和备注
|
||||||
|
$data = Db::name('auth_rule')->where('name', $path)->field('title,remark')->find();
|
||||||
|
if ($data) {
|
||||||
|
$title = __($data['title']);
|
||||||
|
$content = __($data['remark']);
|
||||||
|
}
|
||||||
|
if (!$content) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$result = '<div class="panel-lead"><em>' . $title . '</em>' . $content . '</div>';
|
||||||
|
if ($container) {
|
||||||
|
$result = '<div class="panel-heading">' . $result . '</div>';
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
//配置文件
|
||||||
|
return [
|
||||||
|
'url_common_param' => true,
|
||||||
|
'url_html_suffix' => '',
|
||||||
|
'controller_auto_search' => true,
|
||||||
|
];
|
||||||
@@ -0,0 +1,462 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\controller;
|
||||||
|
|
||||||
|
use app\common\controller\Backend;
|
||||||
|
use fast\Http;
|
||||||
|
use think\addons\AddonException;
|
||||||
|
use think\addons\Service;
|
||||||
|
use think\Cache;
|
||||||
|
use think\Config;
|
||||||
|
use think\Db;
|
||||||
|
use think\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件管理
|
||||||
|
*
|
||||||
|
* @icon fa fa-cube
|
||||||
|
* @remark 可在线安装、卸载、禁用、启用、配置、升级插件,插件升级前请做好备份。
|
||||||
|
*/
|
||||||
|
class Addon extends Backend
|
||||||
|
{
|
||||||
|
protected $model = null;
|
||||||
|
protected $noNeedRight = ['get_table_list'];
|
||||||
|
|
||||||
|
public function _initialize()
|
||||||
|
{
|
||||||
|
parent::_initialize();
|
||||||
|
if (!$this->auth->isSuperAdmin() && in_array($this->request->action(), ['install', 'uninstall', 'local', 'upgrade', 'authorization', 'testdata'])) {
|
||||||
|
$this->error(__('Access is allowed only to the super management group'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件列表
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$addons = get_addon_list();
|
||||||
|
foreach ($addons as $k => &$v) {
|
||||||
|
$config = get_addon_config($v['name']);
|
||||||
|
$v['config'] = $config ? 1 : 0;
|
||||||
|
$v['url'] = str_replace($this->request->server('SCRIPT_NAME'), '', $v['url']);
|
||||||
|
}
|
||||||
|
$this->assignconfig(['addons' => $addons, 'api_url' => config('fastadmin.api_url'), 'faversion' => config('fastadmin.version'), 'domain' => request()->host(true)]);
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置
|
||||||
|
*/
|
||||||
|
public function config($name = null)
|
||||||
|
{
|
||||||
|
$name = $name ? $name : $this->request->get("name");
|
||||||
|
if (!$name) {
|
||||||
|
$this->error(__('Parameter %s can not be empty', 'name'));
|
||||||
|
}
|
||||||
|
if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
|
||||||
|
$this->error(__('Addon name incorrect'));
|
||||||
|
}
|
||||||
|
$info = get_addon_info($name);
|
||||||
|
$config = get_addon_fullconfig($name);
|
||||||
|
if (!$info) {
|
||||||
|
$this->error(__('Addon not exists'));
|
||||||
|
}
|
||||||
|
if ($this->request->isPost()) {
|
||||||
|
$params = $this->request->post("row/a", [], 'trim');
|
||||||
|
if ($params) {
|
||||||
|
foreach ($config as $k => &$v) {
|
||||||
|
if (isset($params[$v['name']])) {
|
||||||
|
if ($v['type'] == 'array') {
|
||||||
|
$params[$v['name']] = is_array($params[$v['name']]) ? $params[$v['name']] : (array)json_decode($params[$v['name']], true);
|
||||||
|
$value = $params[$v['name']];
|
||||||
|
} else {
|
||||||
|
$value = is_array($params[$v['name']]) ? implode(',', $params[$v['name']]) : $params[$v['name']];
|
||||||
|
}
|
||||||
|
$v['value'] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$addon = get_addon_instance($name);
|
||||||
|
//插件自定义配置实现逻辑
|
||||||
|
if (method_exists($addon, 'config')) {
|
||||||
|
$addon->config($name, $config);
|
||||||
|
} else {
|
||||||
|
//更新配置文件
|
||||||
|
set_addon_fullconfig($name, $config);
|
||||||
|
Service::refresh();
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->error(__($e->getMessage()));
|
||||||
|
}
|
||||||
|
$this->success();
|
||||||
|
}
|
||||||
|
$this->error(__('Parameter %s can not be empty', ''));
|
||||||
|
}
|
||||||
|
$tips = [];
|
||||||
|
$groupList = [];
|
||||||
|
$ungroupList = [];
|
||||||
|
foreach ($config as $index => &$item) {
|
||||||
|
//如果有设置分组
|
||||||
|
if (isset($item['group']) && $item['group']) {
|
||||||
|
if (!in_array($item['group'], $groupList)) {
|
||||||
|
$groupList["custom" . (count($groupList) + 1)] = $item['group'];
|
||||||
|
}
|
||||||
|
} elseif ($item['name'] != '__tips__') {
|
||||||
|
$ungroupList[] = $item['name'];
|
||||||
|
}
|
||||||
|
if ($item['name'] == '__tips__') {
|
||||||
|
$tips = $item;
|
||||||
|
unset($config[$index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($ungroupList) {
|
||||||
|
$groupList['other'] = '其它';
|
||||||
|
}
|
||||||
|
$this->view->assign("groupList", $groupList);
|
||||||
|
$this->view->assign("addon", ['info' => $info, 'config' => $config, 'tips' => $tips]);
|
||||||
|
$configFile = ADDON_PATH . $name . DS . 'config.html';
|
||||||
|
$viewFile = is_file($configFile) ? $configFile : '';
|
||||||
|
return $this->view->fetch($viewFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安装
|
||||||
|
*/
|
||||||
|
public function install()
|
||||||
|
{
|
||||||
|
$name = $this->request->post("name");
|
||||||
|
$force = (int)$this->request->post("force");
|
||||||
|
if (!$name) {
|
||||||
|
$this->error(__('Parameter %s can not be empty', 'name'));
|
||||||
|
}
|
||||||
|
if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
|
||||||
|
$this->error(__('Addon name incorrect'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$info = [];
|
||||||
|
try {
|
||||||
|
$uid = $this->request->post("uid");
|
||||||
|
$token = $this->request->post("token");
|
||||||
|
$version = $this->request->post("version");
|
||||||
|
$faversion = $this->request->post("faversion");
|
||||||
|
$extend = [
|
||||||
|
'uid' => $uid,
|
||||||
|
'token' => $token,
|
||||||
|
'version' => $version,
|
||||||
|
'faversion' => $faversion
|
||||||
|
];
|
||||||
|
$info = Service::install($name, $force, $extend);
|
||||||
|
} catch (AddonException $e) {
|
||||||
|
$this->result($e->getData(), $e->getCode(), __($e->getMessage()));
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->error(__($e->getMessage()), $e->getCode());
|
||||||
|
}
|
||||||
|
$this->success(__('Install successful'), '', ['addon' => $info]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卸载
|
||||||
|
*/
|
||||||
|
public function uninstall()
|
||||||
|
{
|
||||||
|
$name = $this->request->post("name");
|
||||||
|
$force = (int)$this->request->post("force");
|
||||||
|
$droptables = (int)$this->request->post("droptables");
|
||||||
|
if (!$name) {
|
||||||
|
$this->error(__('Parameter %s can not be empty', 'name'));
|
||||||
|
}
|
||||||
|
if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
|
||||||
|
$this->error(__('Addon name incorrect'));
|
||||||
|
}
|
||||||
|
//只有开启调试且为超级管理员才允许删除相关数据库
|
||||||
|
$tables = [];
|
||||||
|
if ($droptables && Config::get("app_debug") && $this->auth->isSuperAdmin()) {
|
||||||
|
$tables = get_addon_tables($name);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Service::uninstall($name, $force);
|
||||||
|
if ($tables) {
|
||||||
|
$prefix = Config::get('database.prefix');
|
||||||
|
//删除插件关联表
|
||||||
|
foreach ($tables as $index => $table) {
|
||||||
|
//忽略非插件标识的表名
|
||||||
|
if (!preg_match("/^{$prefix}{$name}/", $table)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Db::execute("DROP TABLE IF EXISTS `{$table}`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (AddonException $e) {
|
||||||
|
$this->result($e->getData(), $e->getCode(), __($e->getMessage()));
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->error(__($e->getMessage()));
|
||||||
|
}
|
||||||
|
$this->success(__('Uninstall successful'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用启用
|
||||||
|
*/
|
||||||
|
public function state()
|
||||||
|
{
|
||||||
|
$name = $this->request->post("name");
|
||||||
|
$action = $this->request->post("action");
|
||||||
|
$force = (int)$this->request->post("force");
|
||||||
|
if (!$name) {
|
||||||
|
$this->error(__('Parameter %s can not be empty', 'name'));
|
||||||
|
}
|
||||||
|
if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
|
||||||
|
$this->error(__('Addon name incorrect'));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$action = $action == 'enable' ? $action : 'disable';
|
||||||
|
//调用启用、禁用的方法
|
||||||
|
Service::$action($name, $force);
|
||||||
|
Cache::rm('__menu__');
|
||||||
|
} catch (AddonException $e) {
|
||||||
|
$this->result($e->getData(), $e->getCode(), __($e->getMessage()));
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->error(__($e->getMessage()));
|
||||||
|
}
|
||||||
|
$this->success(__('Operate successful'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本地上传
|
||||||
|
*/
|
||||||
|
public function local()
|
||||||
|
{
|
||||||
|
Config::set('default_return_type', 'json');
|
||||||
|
|
||||||
|
$info = [];
|
||||||
|
$file = $this->request->file('file');
|
||||||
|
try {
|
||||||
|
$uid = $this->request->post("uid");
|
||||||
|
$token = $this->request->post("token");
|
||||||
|
$faversion = $this->request->post("faversion");
|
||||||
|
$force = $this->request->post("force");
|
||||||
|
if (!$uid || !$token) {
|
||||||
|
throw new Exception(__('Please login and try to install'));
|
||||||
|
}
|
||||||
|
$extend = [
|
||||||
|
'uid' => $uid,
|
||||||
|
'token' => $token,
|
||||||
|
'faversion' => $faversion
|
||||||
|
];
|
||||||
|
$info = Service::local($file, $extend, $force);
|
||||||
|
} catch (AddonException $e) {
|
||||||
|
$this->result($e->getData(), $e->getCode(), __($e->getMessage()));
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->error(__($e->getMessage()));
|
||||||
|
}
|
||||||
|
$this->success(__('Offline installed tips'), '', ['addon' => $info]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新插件
|
||||||
|
*/
|
||||||
|
public function upgrade()
|
||||||
|
{
|
||||||
|
$name = $this->request->post("name");
|
||||||
|
$addonTmpDir = RUNTIME_PATH . 'addons' . DS;
|
||||||
|
if (!$name) {
|
||||||
|
$this->error(__('Parameter %s can not be empty', 'name'));
|
||||||
|
}
|
||||||
|
if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
|
||||||
|
$this->error(__('Addon name incorrect'));
|
||||||
|
}
|
||||||
|
if (!is_dir($addonTmpDir)) {
|
||||||
|
@mkdir($addonTmpDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$info = [];
|
||||||
|
try {
|
||||||
|
$info = get_addon_info($name);
|
||||||
|
$uid = $this->request->post("uid");
|
||||||
|
$token = $this->request->post("token");
|
||||||
|
$version = $this->request->post("version");
|
||||||
|
$faversion = $this->request->post("faversion");
|
||||||
|
$extend = [
|
||||||
|
'uid' => $uid,
|
||||||
|
'token' => $token,
|
||||||
|
'version' => $version,
|
||||||
|
'oldversion' => $info['version'] ?? '',
|
||||||
|
'faversion' => $faversion
|
||||||
|
];
|
||||||
|
//调用更新的方法
|
||||||
|
$info = Service::upgrade($name, $extend);
|
||||||
|
Cache::rm('__menu__');
|
||||||
|
} catch (AddonException $e) {
|
||||||
|
$this->result($e->getData(), $e->getCode(), __($e->getMessage()));
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->error(__($e->getMessage()));
|
||||||
|
}
|
||||||
|
$this->success(__('Operate successful'), '', ['addon' => $info]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试数据
|
||||||
|
*/
|
||||||
|
public function testdata()
|
||||||
|
{
|
||||||
|
$name = $this->request->post("name");
|
||||||
|
if (!$name) {
|
||||||
|
$this->error(__('Parameter %s can not be empty', 'name'));
|
||||||
|
}
|
||||||
|
if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
|
||||||
|
$this->error(__('Addon name incorrect'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Service::importsql($name, 'testdata.sql');
|
||||||
|
} catch (AddonException $e) {
|
||||||
|
$this->result($e->getData(), $e->getCode(), __($e->getMessage()));
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->error(__($e->getMessage()), $e->getCode());
|
||||||
|
}
|
||||||
|
$this->success(__('Import successful'), '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已装插件
|
||||||
|
*/
|
||||||
|
public function downloaded()
|
||||||
|
{
|
||||||
|
$offset = (int)$this->request->get("offset");
|
||||||
|
$limit = (int)$this->request->get("limit");
|
||||||
|
$filter = $this->request->get("filter", '');
|
||||||
|
$search = $this->request->get("search", '', 'strip_tags,htmlspecialchars');
|
||||||
|
$onlineaddons = $this->getAddonList();
|
||||||
|
$filter = (array)json_decode($filter, true);
|
||||||
|
$addons = get_addon_list();
|
||||||
|
$list = [];
|
||||||
|
foreach ($addons as $k => $v) {
|
||||||
|
if ($search && stripos($v['name'], $search) === false && stripos($v['title'], $search) === false && stripos($v['intro'], $search) === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($onlineaddons[$v['name']])) {
|
||||||
|
$v = array_merge($v, $onlineaddons[$v['name']]);
|
||||||
|
$v['price'] = '-';
|
||||||
|
} else {
|
||||||
|
$v['category_id'] = 0;
|
||||||
|
$v['flag'] = '';
|
||||||
|
$v['banner'] = '';
|
||||||
|
$v['image'] = '';
|
||||||
|
$v['demourl'] = '';
|
||||||
|
$v['price'] = __('None');
|
||||||
|
$v['screenshots'] = [];
|
||||||
|
$v['releaselist'] = [];
|
||||||
|
$v['url'] = addon_url($v['name']);
|
||||||
|
$v['url'] = str_replace($this->request->server('SCRIPT_NAME'), '', $v['url']);
|
||||||
|
}
|
||||||
|
$v['createtime'] = filemtime(ADDON_PATH . $v['name']);
|
||||||
|
if ($filter && isset($filter['category_id']) && is_numeric($filter['category_id']) && $filter['category_id'] != $v['category_id']) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$list[] = $v;
|
||||||
|
}
|
||||||
|
$total = count($list);
|
||||||
|
if ($limit) {
|
||||||
|
$list = array_slice($list, $offset, $limit);
|
||||||
|
}
|
||||||
|
$result = array("total" => $total, "rows" => $list);
|
||||||
|
|
||||||
|
$callback = $this->request->get('callback') ? "jsonp" : "json";
|
||||||
|
return $callback($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测
|
||||||
|
*/
|
||||||
|
public function isbuy()
|
||||||
|
{
|
||||||
|
$name = $this->request->post("name");
|
||||||
|
$uid = $this->request->post("uid");
|
||||||
|
$token = $this->request->post("token");
|
||||||
|
$version = $this->request->post("version");
|
||||||
|
$faversion = $this->request->post("faversion");
|
||||||
|
$extend = [
|
||||||
|
'uid' => $uid,
|
||||||
|
'token' => $token,
|
||||||
|
'version' => $version,
|
||||||
|
'faversion' => $faversion
|
||||||
|
];
|
||||||
|
try {
|
||||||
|
$result = Service::isBuy($name, $extend);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->error(__($e->getMessage()));
|
||||||
|
}
|
||||||
|
return json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新授权
|
||||||
|
*/
|
||||||
|
public function authorization()
|
||||||
|
{
|
||||||
|
$params = [
|
||||||
|
'uid' => $this->request->post('uid'),
|
||||||
|
'token' => $this->request->post('token'),
|
||||||
|
'faversion' => $this->request->post('faversion'),
|
||||||
|
];
|
||||||
|
try {
|
||||||
|
Service::authorization($params);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->error(__($e->getMessage()));
|
||||||
|
}
|
||||||
|
$this->success(__('Operate successful'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件相关表
|
||||||
|
*/
|
||||||
|
public function get_table_list()
|
||||||
|
{
|
||||||
|
$name = $this->request->post("name");
|
||||||
|
if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
|
||||||
|
$this->error(__('Addon name incorrect'));
|
||||||
|
}
|
||||||
|
$tables = get_addon_tables($name);
|
||||||
|
$prefix = Config::get('database.prefix');
|
||||||
|
foreach ($tables as $index => $table) {
|
||||||
|
//忽略非插件标识的表名
|
||||||
|
if (!preg_match("/^{$prefix}{$name}/", $table)) {
|
||||||
|
unset($tables[$index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$tables = array_values($tables);
|
||||||
|
$this->success('', null, ['tables' => $tables]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getAddonList()
|
||||||
|
{
|
||||||
|
$onlineaddons = Cache::get("onlineaddons");
|
||||||
|
if (!is_array($onlineaddons) && config('fastadmin.api_url')) {
|
||||||
|
$onlineaddons = [];
|
||||||
|
$params = [
|
||||||
|
'uid' => $this->request->post('uid'),
|
||||||
|
'token' => $this->request->post('token'),
|
||||||
|
'version' => config('fastadmin.version'),
|
||||||
|
'faversion' => config('fastadmin.version'),
|
||||||
|
];
|
||||||
|
$json = [];
|
||||||
|
try {
|
||||||
|
$json = Service::addons($params);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
$rows = $json['rows'] ?? [];
|
||||||
|
foreach ($rows as $index => $row) {
|
||||||
|
if (!isset($row['name'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$onlineaddons[$row['name']] = $row;
|
||||||
|
}
|
||||||
|
Cache::set("onlineaddons", $onlineaddons, 600);
|
||||||
|
}
|
||||||
|
return $onlineaddons;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\controller;
|
||||||
|
|
||||||
|
use app\common\controller\Backend;
|
||||||
|
use app\common\exception\UploadException;
|
||||||
|
use app\common\library\Upload;
|
||||||
|
use fast\Random;
|
||||||
|
use think\addons\Service;
|
||||||
|
use think\Cache;
|
||||||
|
use think\Config;
|
||||||
|
use think\Db;
|
||||||
|
use think\Lang;
|
||||||
|
use think\Loader;
|
||||||
|
use think\Response;
|
||||||
|
use think\Validate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajax异步请求接口
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class Ajax extends Backend
|
||||||
|
{
|
||||||
|
protected $noNeedLogin = ['lang'];
|
||||||
|
protected $noNeedRight = ['*'];
|
||||||
|
protected $layout = '';
|
||||||
|
|
||||||
|
public function _initialize()
|
||||||
|
{
|
||||||
|
parent::_initialize();
|
||||||
|
|
||||||
|
//设置过滤方法
|
||||||
|
$this->request->filter(['trim', 'strip_tags', 'htmlspecialchars']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载语言包
|
||||||
|
*/
|
||||||
|
public function lang()
|
||||||
|
{
|
||||||
|
$this->request->get(['callback' => 'define']);
|
||||||
|
$header = ['Content-Type' => 'application/javascript'];
|
||||||
|
if (!config('app_debug')) {
|
||||||
|
$offset = 30 * 60 * 60 * 24; // 缓存一个月
|
||||||
|
$header['Cache-Control'] = 'public';
|
||||||
|
$header['Pragma'] = 'cache';
|
||||||
|
$header['Expires'] = gmdate("D, d M Y H:i:s", time() + $offset) . " GMT";
|
||||||
|
}
|
||||||
|
|
||||||
|
$controllername = $this->request->get('controllername');
|
||||||
|
$lang = $this->request->get('lang');
|
||||||
|
if (!$lang || !in_array($lang, config('allow_lang_list')) || !$controllername || !preg_match("/^[a-z0-9_\.]+$/i", $controllername)) {
|
||||||
|
return jsonp(['errmsg' => '参数错误'], 200, [], ['json_encode_param' => JSON_FORCE_OBJECT | JSON_UNESCAPED_UNICODE]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$controllername = input("controllername");
|
||||||
|
$className = Loader::parseClass($this->request->module(), 'controller', $controllername, false);
|
||||||
|
|
||||||
|
//存在对应的类才加载
|
||||||
|
if (class_exists($className)) {
|
||||||
|
$this->loadlang($controllername);
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonp(Lang::get(), 200, $header, ['json_encode_param' => JSON_FORCE_OBJECT | JSON_UNESCAPED_UNICODE]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件
|
||||||
|
*/
|
||||||
|
public function upload()
|
||||||
|
{
|
||||||
|
Config::set('default_return_type', 'json');
|
||||||
|
|
||||||
|
//必须还原upload配置,否则分片及cdnurl函数计算错误
|
||||||
|
Config::load(APP_PATH . 'extra/upload.php', 'upload');
|
||||||
|
|
||||||
|
$chunkid = $this->request->post("chunkid");
|
||||||
|
if ($chunkid) {
|
||||||
|
if (!Config::get('upload.chunking')) {
|
||||||
|
$this->error(__('Chunk file disabled'));
|
||||||
|
}
|
||||||
|
$action = $this->request->post("action");
|
||||||
|
$chunkindex = $this->request->post("chunkindex/d");
|
||||||
|
$chunkcount = $this->request->post("chunkcount/d");
|
||||||
|
$filename = $this->request->post("filename");
|
||||||
|
$method = $this->request->method(true);
|
||||||
|
if ($action == 'merge') {
|
||||||
|
$attachment = null;
|
||||||
|
//合并分片文件
|
||||||
|
try {
|
||||||
|
$upload = new Upload();
|
||||||
|
$attachment = $upload->merge($chunkid, $chunkcount, $filename);
|
||||||
|
} catch (UploadException $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
$this->success(__('Uploaded successful'), '', ['url' => $attachment->url, 'fullurl' => cdnurl($attachment->url, true)]);
|
||||||
|
} elseif ($method == 'clean') {
|
||||||
|
//删除冗余的分片文件
|
||||||
|
try {
|
||||||
|
$upload = new Upload();
|
||||||
|
$upload->clean($chunkid);
|
||||||
|
} catch (UploadException $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
$this->success();
|
||||||
|
} else {
|
||||||
|
//上传分片文件
|
||||||
|
//默认普通上传文件
|
||||||
|
$file = $this->request->file('file');
|
||||||
|
try {
|
||||||
|
$upload = new Upload($file);
|
||||||
|
$upload->chunk($chunkid, $chunkindex, $chunkcount);
|
||||||
|
} catch (UploadException $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
$this->success();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$attachment = null;
|
||||||
|
//默认普通上传文件
|
||||||
|
$file = $this->request->file('file');
|
||||||
|
try {
|
||||||
|
$upload = new Upload($file);
|
||||||
|
$attachment = $upload->upload();
|
||||||
|
} catch (UploadException $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->success(__('Uploaded successful'), '', ['url' => $attachment->url, 'fullurl' => cdnurl($attachment->url, true)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用排序
|
||||||
|
*/
|
||||||
|
public function weigh()
|
||||||
|
{
|
||||||
|
//排序的数组
|
||||||
|
$ids = $this->request->post("ids");
|
||||||
|
//拖动的记录ID
|
||||||
|
$changeid = $this->request->post("changeid");
|
||||||
|
//操作字段
|
||||||
|
$field = $this->request->post("field");
|
||||||
|
//操作的数据表
|
||||||
|
$table = $this->request->post("table");
|
||||||
|
if (!Validate::is($table, "alphaDash")) {
|
||||||
|
$this->error();
|
||||||
|
}
|
||||||
|
//主键
|
||||||
|
$pk = $this->request->post("pk");
|
||||||
|
//排序的方式
|
||||||
|
$orderway = strtolower($this->request->post("orderway", ""));
|
||||||
|
$orderway = $orderway == 'asc' ? 'ASC' : 'DESC';
|
||||||
|
$sour = $weighdata = [];
|
||||||
|
$ids = explode(',', $ids);
|
||||||
|
$prikey = $pk && preg_match("/^[a-z0-9\-_]+$/i", $pk) ? $pk : (Db::name($table)->getPk() ?: 'id');
|
||||||
|
$pid = $this->request->post("pid", "");
|
||||||
|
//限制更新的字段
|
||||||
|
$field = in_array($field, ['weigh']) ? $field : 'weigh';
|
||||||
|
|
||||||
|
// 如果设定了pid的值,此时只匹配满足条件的ID,其它忽略
|
||||||
|
if ($pid !== '') {
|
||||||
|
$hasids = [];
|
||||||
|
$list = Db::name($table)->where($prikey, 'in', $ids)->where('pid', 'in', $pid)->field("{$prikey},pid")->select();
|
||||||
|
foreach ($list as $k => $v) {
|
||||||
|
$hasids[] = $v[$prikey];
|
||||||
|
}
|
||||||
|
$ids = array_values(array_intersect($ids, $hasids));
|
||||||
|
}
|
||||||
|
|
||||||
|
$list = Db::name($table)->field("$prikey,$field")->where($prikey, 'in', $ids)->order($field, $orderway)->select();
|
||||||
|
foreach ($list as $k => $v) {
|
||||||
|
$sour[] = $v[$prikey];
|
||||||
|
$weighdata[$v[$prikey]] = $v[$field];
|
||||||
|
}
|
||||||
|
$position = array_search($changeid, $ids);
|
||||||
|
$desc_id = $sour[$position] ?? end($sour); //移动到目标的ID值,取出所处改变前位置的值
|
||||||
|
$sour_id = $changeid;
|
||||||
|
$weighids = [];
|
||||||
|
$temp = array_values(array_diff_assoc($ids, $sour));
|
||||||
|
foreach ($temp as $m => $n) {
|
||||||
|
if ($n == $sour_id) {
|
||||||
|
$offset = $desc_id;
|
||||||
|
} else {
|
||||||
|
if ($sour_id == $temp[0]) {
|
||||||
|
$offset = $temp[$m + 1] ?? $sour_id;
|
||||||
|
} else {
|
||||||
|
$offset = $temp[$m - 1] ?? $sour_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isset($weighdata[$offset])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$weighids[$n] = $weighdata[$offset];
|
||||||
|
Db::name($table)->where($prikey, $n)->update([$field => $weighdata[$offset]]);
|
||||||
|
}
|
||||||
|
$this->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空系统缓存
|
||||||
|
*/
|
||||||
|
public function wipecache()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$type = $this->request->request("type");
|
||||||
|
switch ($type) {
|
||||||
|
case 'all':
|
||||||
|
case 'content':
|
||||||
|
//内容缓存
|
||||||
|
rmdirs(CACHE_PATH, false);
|
||||||
|
Cache::clear();
|
||||||
|
if ($type == 'content') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// no break
|
||||||
|
case 'template':
|
||||||
|
// 模板缓存
|
||||||
|
rmdirs(TEMP_PATH, false);
|
||||||
|
if ($type == 'template') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// no break
|
||||||
|
case 'addons':
|
||||||
|
// 插件缓存
|
||||||
|
Service::refresh();
|
||||||
|
if ($type == 'addons') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// no break
|
||||||
|
case 'browser':
|
||||||
|
// 浏览器缓存
|
||||||
|
// 只有生产环境下才修改
|
||||||
|
if (!config('app_debug')) {
|
||||||
|
$version = config('site.version');
|
||||||
|
$newversion = preg_replace_callback("/(.*)\.([0-9]+)\$/", function ($match) {
|
||||||
|
return $match[1] . '.' . ($match[2] + 1);
|
||||||
|
}, $version);
|
||||||
|
if ($newversion && $newversion != $version) {
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
\app\common\model\Config::where('name', 'version')->update(['value' => $newversion]);
|
||||||
|
\app\common\model\Config::refreshFile();
|
||||||
|
Db::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
exception($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($type == 'browser') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
\think\Hook::listen("wipecache_after");
|
||||||
|
$this->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取分类数据,联动列表
|
||||||
|
*/
|
||||||
|
public function category()
|
||||||
|
{
|
||||||
|
$type = $this->request->get('type', '');
|
||||||
|
$pid = $this->request->get('pid', '');
|
||||||
|
$where = ['status' => 'normal'];
|
||||||
|
$categorylist = null;
|
||||||
|
if ($pid || $pid === '0') {
|
||||||
|
$where['pid'] = $pid;
|
||||||
|
}
|
||||||
|
if ($type) {
|
||||||
|
$where['type'] = $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
$categorylist = Db::name('category')->where($where)->field('id as value,name')->order('weigh desc,id desc')->select();
|
||||||
|
|
||||||
|
$this->success('', '', $categorylist);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取省市区数据,联动列表
|
||||||
|
*/
|
||||||
|
public function area()
|
||||||
|
{
|
||||||
|
$params = $this->request->get("row/a");
|
||||||
|
if (!empty($params)) {
|
||||||
|
$province = isset($params['province']) ? $params['province'] : null;
|
||||||
|
$city = isset($params['city']) ? $params['city'] : null;
|
||||||
|
} else {
|
||||||
|
$province = $this->request->get('province');
|
||||||
|
$city = $this->request->get('city');
|
||||||
|
}
|
||||||
|
$where = ['pid' => 0, 'level' => 1];
|
||||||
|
$provincelist = null;
|
||||||
|
if ($province !== null) {
|
||||||
|
$where['pid'] = $province;
|
||||||
|
$where['level'] = 2;
|
||||||
|
if ($city !== null) {
|
||||||
|
$where['pid'] = $city;
|
||||||
|
$where['level'] = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$provincelist = Db::name('area')->where($where)->field('id as value,name')->select();
|
||||||
|
$this->success('', '', $provincelist);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成后缀图标
|
||||||
|
*/
|
||||||
|
public function icon()
|
||||||
|
{
|
||||||
|
$suffix = $this->request->request("suffix");
|
||||||
|
$suffix = $suffix ? $suffix : "FILE";
|
||||||
|
$data = build_suffix_image($suffix);
|
||||||
|
$header = ['Content-Type' => 'image/svg+xml'];
|
||||||
|
$offset = 30 * 60 * 60 * 24; // 缓存一个月
|
||||||
|
$header['Cache-Control'] = 'public';
|
||||||
|
$header['Pragma'] = 'cache';
|
||||||
|
$header['Expires'] = gmdate("D, d M Y H:i:s", time() + $offset) . " GMT";
|
||||||
|
$response = Response::create($data, '', 200, $header);
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\controller;
|
||||||
|
|
||||||
|
use app\common\controller\Backend;
|
||||||
|
use app\common\model\Category as CategoryModel;
|
||||||
|
use fast\Tree;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类管理
|
||||||
|
*
|
||||||
|
* @icon fa fa-list
|
||||||
|
* @remark 用于管理网站的所有分类,分类可进行无限级分类,分类类型请在常规管理->系统配置->字典配置中添加
|
||||||
|
*/
|
||||||
|
class Category extends Backend
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \app\common\model\Category
|
||||||
|
*/
|
||||||
|
protected $model = null;
|
||||||
|
protected $categorylist = [];
|
||||||
|
protected $noNeedRight = ['selectpage'];
|
||||||
|
|
||||||
|
public function _initialize()
|
||||||
|
{
|
||||||
|
parent::_initialize();
|
||||||
|
$this->model = model('app\common\model\Category');
|
||||||
|
|
||||||
|
$tree = Tree::instance();
|
||||||
|
$tree->init(collection($this->model->order('weigh desc,id desc')->select())->toArray(), 'pid');
|
||||||
|
$this->categorylist = $tree->getTreeList($tree->getTreeArray(0), 'name');
|
||||||
|
$categorydata = [0 => ['type' => 'all', 'name' => __('None')]];
|
||||||
|
foreach ($this->categorylist as $k => $v) {
|
||||||
|
$categorydata[$v['id']] = $v;
|
||||||
|
}
|
||||||
|
$typeList = CategoryModel::getTypeList();
|
||||||
|
$this->view->assign("flagList", $this->model->getFlagList());
|
||||||
|
$this->view->assign("typeList", $typeList);
|
||||||
|
$this->view->assign("parentList", $categorydata);
|
||||||
|
$this->assignconfig('typeList', $typeList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
//设置过滤方法
|
||||||
|
$this->request->filter(['strip_tags']);
|
||||||
|
if ($this->request->isAjax()) {
|
||||||
|
$search = $this->request->request("search");
|
||||||
|
$type = $this->request->request("type");
|
||||||
|
|
||||||
|
//构造父类select列表选项数据
|
||||||
|
$list = [];
|
||||||
|
|
||||||
|
foreach ($this->categorylist as $k => $v) {
|
||||||
|
if ($search) {
|
||||||
|
if ($v['type'] == $type && stripos($v['name'], $search) !== false || stripos($v['nickname'], $search) !== false) {
|
||||||
|
if ($type == "all" || $type == null) {
|
||||||
|
$list = $this->categorylist;
|
||||||
|
} else {
|
||||||
|
$list[] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($type == "all" || $type == null) {
|
||||||
|
$list = $this->categorylist;
|
||||||
|
} elseif ($v['type'] == $type) {
|
||||||
|
$list[] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = count($list);
|
||||||
|
$result = array("total" => $total, "rows" => $list);
|
||||||
|
|
||||||
|
return json($result);
|
||||||
|
}
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加
|
||||||
|
*/
|
||||||
|
public function add()
|
||||||
|
{
|
||||||
|
if ($this->request->isPost()) {
|
||||||
|
$this->token();
|
||||||
|
}
|
||||||
|
return parent::add();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑
|
||||||
|
*/
|
||||||
|
public function edit($ids = null)
|
||||||
|
{
|
||||||
|
$row = $this->model->get($ids);
|
||||||
|
if (!$row) {
|
||||||
|
$this->error(__('No Results were found'));
|
||||||
|
}
|
||||||
|
$adminIds = $this->getDataLimitAdminIds();
|
||||||
|
if (is_array($adminIds)) {
|
||||||
|
if (!in_array($row[$this->dataLimitField], $adminIds)) {
|
||||||
|
$this->error(__('You have no permission'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($this->request->isPost()) {
|
||||||
|
$this->token();
|
||||||
|
$params = $this->request->post("row/a");
|
||||||
|
if ($params) {
|
||||||
|
$params = $this->preExcludeFields($params);
|
||||||
|
|
||||||
|
if ($params['pid'] != $row['pid']) {
|
||||||
|
$childrenIds = Tree::instance()->init(collection(\app\common\model\Category::select())->toArray())->getChildrenIds($row['id'], true);
|
||||||
|
if (in_array($params['pid'], $childrenIds)) {
|
||||||
|
$this->error(__('Can not change the parent to child or itself'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
//是否采用模型验证
|
||||||
|
if ($this->modelValidate) {
|
||||||
|
$name = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||||
|
$validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.edit' : $name) : $this->modelValidate;
|
||||||
|
$row->validate($validate);
|
||||||
|
}
|
||||||
|
$result = $row->allowField(true)->save($params);
|
||||||
|
if ($result !== false) {
|
||||||
|
$this->success();
|
||||||
|
} else {
|
||||||
|
$this->error($row->getError());
|
||||||
|
}
|
||||||
|
} catch (\think\exception\PDOException $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
} catch (\think\Exception $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->error(__('Parameter %s can not be empty', ''));
|
||||||
|
}
|
||||||
|
$this->view->assign("row", $row);
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selectpage搜索
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function selectpage()
|
||||||
|
{
|
||||||
|
return parent::selectpage();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\controller;
|
||||||
|
|
||||||
|
use app\common\controller\Backend;
|
||||||
|
use think\Config;
|
||||||
|
use think\console\Input;
|
||||||
|
use think\Db;
|
||||||
|
use think\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线命令管理
|
||||||
|
*
|
||||||
|
* @icon fa fa-circle-o
|
||||||
|
*/
|
||||||
|
class Command extends Backend
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command模型对象
|
||||||
|
*/
|
||||||
|
protected $model = null;
|
||||||
|
protected $noNeedRight = ['get_controller_list', 'get_field_list'];
|
||||||
|
|
||||||
|
public function _initialize()
|
||||||
|
{
|
||||||
|
parent::_initialize();
|
||||||
|
$this->model = new \app\admin\model\Command;
|
||||||
|
$this->view->assign("statusList", $this->model->getStatusList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加
|
||||||
|
*/
|
||||||
|
public function add()
|
||||||
|
{
|
||||||
|
|
||||||
|
$tableList = [];
|
||||||
|
$list = \think\Db::query("SHOW TABLES");
|
||||||
|
foreach ($list as $key => $row) {
|
||||||
|
$tableList[reset($row)] = reset($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->view->assign("tableList", $tableList);
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取字段列表
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function get_field_list()
|
||||||
|
{
|
||||||
|
if (!$this->request->isPost()) {
|
||||||
|
$this->error(__('请求方式不正确'));
|
||||||
|
}
|
||||||
|
$dbname = Config::get('database.database');
|
||||||
|
$prefix = Config::get('database.prefix');
|
||||||
|
$table = $this->request->request('table');
|
||||||
|
//从数据库中获取表字段信息
|
||||||
|
$sql = "SELECT * FROM `information_schema`.`columns` "
|
||||||
|
. "WHERE TABLE_SCHEMA = ? AND table_name = ? "
|
||||||
|
. "ORDER BY ORDINAL_POSITION";
|
||||||
|
//加载主表的列
|
||||||
|
$columnList = Db::query($sql, [$dbname, $table]);
|
||||||
|
$fieldlist = [];
|
||||||
|
foreach ($columnList as $index => $item) {
|
||||||
|
$fieldlist[] = $item['COLUMN_NAME'];
|
||||||
|
}
|
||||||
|
$this->success("", null, ['fieldlist' => $fieldlist]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取控制器列表
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function get_controller_list()
|
||||||
|
{
|
||||||
|
if (!$this->request->isPost()) {
|
||||||
|
$this->error(__('请求方式不正确'));
|
||||||
|
}
|
||||||
|
//搜索关键词,客户端输入以空格分开,这里接收为数组
|
||||||
|
$word = (array)$this->request->post("q_word/a");
|
||||||
|
$word = implode('', $word);
|
||||||
|
|
||||||
|
$adminPath = dirname(__DIR__) . DS;
|
||||||
|
$controllerDir = $adminPath . 'controller' . DS;
|
||||||
|
$files = new \RecursiveIteratorIterator(
|
||||||
|
new \RecursiveDirectoryIterator($controllerDir), \RecursiveIteratorIterator::LEAVES_ONLY
|
||||||
|
);
|
||||||
|
$list = [];
|
||||||
|
foreach ($files as $name => $file) {
|
||||||
|
if (!$file->isDir()) {
|
||||||
|
$filePath = $file->getRealPath();
|
||||||
|
$name = str_replace($controllerDir, '', $filePath);
|
||||||
|
$name = str_replace(DS, "/", $name);
|
||||||
|
if (!preg_match("/(.*)\.php\$/", $name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!$word || stripos($name, $word) !== false) {
|
||||||
|
$list[] = ['id' => $name, 'name' => $name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$pageNumber = $this->request->request("pageNumber");
|
||||||
|
$pageSize = $this->request->request("pageSize");
|
||||||
|
return json(['list' => array_slice($list, ($pageNumber - 1) * $pageSize, $pageSize), 'total' => count($list)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 详情
|
||||||
|
*/
|
||||||
|
public function detail($ids)
|
||||||
|
{
|
||||||
|
$row = $this->model->get($ids);
|
||||||
|
if (!$row) {
|
||||||
|
$this->error(__('No Results were found'));
|
||||||
|
}
|
||||||
|
$row['params'] = (array)json_decode($row['params'], true);
|
||||||
|
$this->view->assign("row", $row);
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行
|
||||||
|
*/
|
||||||
|
public function execute($ids)
|
||||||
|
{
|
||||||
|
if (!$this->request->isPost()) {
|
||||||
|
$this->error(__('请求方式不正确'));
|
||||||
|
}
|
||||||
|
$row = $this->model->get($ids);
|
||||||
|
if (!$row) {
|
||||||
|
$this->error(__('No Results were found'));
|
||||||
|
}
|
||||||
|
$params = (array)json_decode($row['params'], true);
|
||||||
|
if (!isset($params['commandtype'])) {
|
||||||
|
$this->error('不支持1.1.3之前版本的旧命令');
|
||||||
|
}
|
||||||
|
$this->request->post($params);
|
||||||
|
$this->command('execute');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成命令
|
||||||
|
*/
|
||||||
|
public function command($action = '')
|
||||||
|
{
|
||||||
|
if (!$this->request->isPost()) {
|
||||||
|
$this->error(__('请求方式不正确'));
|
||||||
|
}
|
||||||
|
$commandtype = $this->request->post("commandtype");
|
||||||
|
$params = $this->request->post();
|
||||||
|
$allowfields = [
|
||||||
|
'crud' => 'table,controller,model,fields,force,local,delete,menu',
|
||||||
|
'menu' => 'delete,force',
|
||||||
|
'min' => 'module,resource,optimize',
|
||||||
|
'api' => 'url,module,output,template,force,title,class,language,addon',
|
||||||
|
];
|
||||||
|
$argv = [];
|
||||||
|
$allowfields = isset($allowfields[$commandtype]) ? explode(',', $allowfields[$commandtype]) : [];
|
||||||
|
$allowfields = array_filter(array_intersect_key($params, array_flip($allowfields)));
|
||||||
|
if (isset($params['local']) && !$params['local']) {
|
||||||
|
$allowfields['local'] = $params['local'];
|
||||||
|
} else {
|
||||||
|
unset($allowfields['local']);
|
||||||
|
}
|
||||||
|
foreach ($allowfields as $key => $param) {
|
||||||
|
if (is_string($param) && in_array($param, ['force', 'delete', 'local'])) {
|
||||||
|
$param = (int)$param;
|
||||||
|
}
|
||||||
|
$argv[] = "--{$key}=" . (is_array($param) ? implode(',', $param) : $param);
|
||||||
|
}
|
||||||
|
if ($commandtype == 'crud') {
|
||||||
|
$extend = 'setcheckboxsuffix,enumradiosuffix,imagefield,filefield,intdatesuffix,switchsuffix,citysuffix,selectpagesuffix,selectpagessuffix,ignorefields,sortfield,editorsuffix,headingfilterfield,tagsuffix,jsonsuffix,fixedcolumns';
|
||||||
|
$extendArr = explode(',', $extend);
|
||||||
|
foreach ($params as $index => $item) {
|
||||||
|
if (in_array($index, $extendArr)) {
|
||||||
|
foreach (explode(',', $item) as $key => $value) {
|
||||||
|
if ($value) {
|
||||||
|
$argv[] = "--{$index}={$value}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$isrelation = (int)$this->request->request('isrelation');
|
||||||
|
if ($isrelation && isset($params['relation'])) {
|
||||||
|
foreach ($params['relation'] as $index => $relation) {
|
||||||
|
foreach ($relation as $key => $value) {
|
||||||
|
$argv[] = "--{$key}=" . (is_array($value) ? implode(',', $value) : $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($commandtype == 'menu') {
|
||||||
|
|
||||||
|
if (isset($params['allcontroller']) && $params['allcontroller']) {
|
||||||
|
$argv[] = "--controller=all-controller";
|
||||||
|
} else {
|
||||||
|
foreach (explode(',', $params['controllerfile']) as $index => $param) {
|
||||||
|
if ($param) {
|
||||||
|
if (!preg_match("/^([a-zA-Z0-9\/]+)\.php$/", $param)) {
|
||||||
|
$this->error("请输入正确的控制器名称");
|
||||||
|
}
|
||||||
|
$argv[] = "--controller=" . substr($param, 0, -4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($commandtype == 'min') {
|
||||||
|
$module = $params['module'] ?? '';
|
||||||
|
if ($module && !in_array($module, ['all', 'backend', 'frontend'])) {
|
||||||
|
$this->error("请选择压缩模块");
|
||||||
|
}
|
||||||
|
$resource = $params['resource'] ?? '';
|
||||||
|
if ($resource && !in_array($resource, ['all', 'js', 'css'])) {
|
||||||
|
$this->error("请选择压缩资源");
|
||||||
|
}
|
||||||
|
$optimize = $params['optimize'] ?? '';
|
||||||
|
if ($optimize && !in_array($optimize, ['uglify', 'closure'])) {
|
||||||
|
$this->error("请选择压缩模式");
|
||||||
|
}
|
||||||
|
} elseif ($commandtype == 'api') {
|
||||||
|
$url = $params['url'] ?? '';
|
||||||
|
if ($url && !filter_var($url, FILTER_VALIDATE_URL)) {
|
||||||
|
$this->error("接口地址格式错误");
|
||||||
|
}
|
||||||
|
$template = $params['template'] ?? '';
|
||||||
|
if ($template && !preg_match("/([a-zA-Z0-9\-_]+)\.html/", $template)) {
|
||||||
|
$this->error("模板文件错误");
|
||||||
|
}
|
||||||
|
$output = $params['output'] ?? '';
|
||||||
|
if ($output && !preg_match("/([a-zA-Z0-9\-_]+)\.html/", $output)) {
|
||||||
|
$this->error("接口生成文件只支持HTML后缀");
|
||||||
|
}
|
||||||
|
$title = $params['title'] ?? '';
|
||||||
|
$author = $params['author'] ?? '';
|
||||||
|
$language = $params['language'] ?? '';
|
||||||
|
$module = $params['module'] ?? '';
|
||||||
|
$addon = $params['addon'] ?? '';
|
||||||
|
if (($title && !$this->validateString($title))) {
|
||||||
|
$this->error("请填写正确的标题");
|
||||||
|
}
|
||||||
|
if (($author && !$this->validateString($author))) {
|
||||||
|
$this->error("请填写正确的作者");
|
||||||
|
}
|
||||||
|
if ($language && !in_array($language, ['zh-cn', 'en'])) {
|
||||||
|
$this->error("请选择正确的语言");
|
||||||
|
}
|
||||||
|
if ($module && !preg_match("/[a-zA-Z0-9]+/", $module)) {
|
||||||
|
$this->error("请填写正确的模块名称");
|
||||||
|
}
|
||||||
|
if ($addon && !preg_match("/[a-zA-Z0-9]+/", $addon)) {
|
||||||
|
$this->error("请填写正确的插件名称");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->error('参数类型错误');
|
||||||
|
}
|
||||||
|
if ($action == 'execute') {
|
||||||
|
if (stripos(implode(' ', $argv), '--controller=all-controller') !== false) {
|
||||||
|
$this->error("只允许在命令行执行该命令,执行前请做好菜单规则备份!!!");
|
||||||
|
}
|
||||||
|
if (config('app_debug')) {
|
||||||
|
$result = $this->doexecute($commandtype, $argv);
|
||||||
|
$this->success("", null, ['result' => $result]);
|
||||||
|
} else {
|
||||||
|
$this->error("只允许在开发环境下执行命令");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->success("", null, ['command' => "php think {$commandtype} " . implode(' ', $argv)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doexecute($commandtype, $argv)
|
||||||
|
{
|
||||||
|
if (!config('app_debug')) {
|
||||||
|
$this->error("只允许在开发环境下执行命令");
|
||||||
|
}
|
||||||
|
if (preg_match("/([;\|&]+)/", implode(' ', $argv))) {
|
||||||
|
$this->error("不支持的命令参数");
|
||||||
|
}
|
||||||
|
if (!$this->auth->isSuperAdmin()) {
|
||||||
|
$this->error("仅允许超级管理员执行命令");
|
||||||
|
}
|
||||||
|
$commandName = "\\app\\admin\\command\\" . ucfirst($commandtype);
|
||||||
|
$input = new Input($argv);
|
||||||
|
$output = new \addons\command\library\Output();
|
||||||
|
$command = new $commandName($commandtype);
|
||||||
|
$data = [
|
||||||
|
'type' => $commandtype,
|
||||||
|
'params' => json_encode($this->request->post()),
|
||||||
|
'command' => "php think {$commandtype} " . implode(' ', $argv),
|
||||||
|
'executetime' => time(),
|
||||||
|
];
|
||||||
|
$this->model->save($data);
|
||||||
|
try {
|
||||||
|
$command->run($input, $output);
|
||||||
|
$result = implode("\n", $output->getMessage());
|
||||||
|
$this->model->status = 'successed';
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$result = implode("\n", $output->getMessage()) . "\n";
|
||||||
|
$result .= $e->getMessage();
|
||||||
|
$this->model->status = 'failured';
|
||||||
|
}
|
||||||
|
$result = trim($result);
|
||||||
|
$this->model->content = $result;
|
||||||
|
$this->model->save();
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function validateString($string)
|
||||||
|
{
|
||||||
|
// 匹配中文、英文、数字、下划线、连字符、空格和感叹号
|
||||||
|
$pattern = '/^[a-zA-Z0-9_\-\.\s\x{4e00}-\x{9fa5}!]+$/u';
|
||||||
|
return preg_match($pattern, $string);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\controller;
|
||||||
|
|
||||||
|
use app\admin\model\Admin;
|
||||||
|
use app\admin\model\User;
|
||||||
|
use app\common\controller\Backend;
|
||||||
|
use app\common\model\Attachment;
|
||||||
|
use fast\Date;
|
||||||
|
use think\Db;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 控制台
|
||||||
|
*
|
||||||
|
* @icon fa fa-dashboard
|
||||||
|
* @remark 用于展示当前系统中的统计数据、统计报表及重要实时数据
|
||||||
|
*/
|
||||||
|
class Dashboard extends Backend
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
\think\Db::execute("SET @@sql_mode='';");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
$column = [];
|
||||||
|
$starttime = Date::unixtime('day', -6);
|
||||||
|
$endtime = Date::unixtime('day', 0, 'end');
|
||||||
|
$joinlist = Db("user")->where('jointime', 'between time', [$starttime, $endtime])
|
||||||
|
->field('jointime, status, COUNT(*) AS nums, DATE_FORMAT(FROM_UNIXTIME(jointime), "%Y-%m-%d") AS join_date')
|
||||||
|
->group('join_date')
|
||||||
|
->select();
|
||||||
|
for ($time = $starttime; $time <= $endtime;) {
|
||||||
|
$column[] = date("Y-m-d", $time);
|
||||||
|
$time += 86400;
|
||||||
|
}
|
||||||
|
$userlist = array_fill_keys($column, 0);
|
||||||
|
foreach ($joinlist as $k => $v) {
|
||||||
|
$userlist[$v['join_date']] = $v['nums'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbTableList = Db::query("SHOW TABLE STATUS");
|
||||||
|
$addonList = get_addon_list();
|
||||||
|
$totalworkingaddon = 0;
|
||||||
|
$totaladdon = count($addonList);
|
||||||
|
foreach ($addonList as $index => $item) {
|
||||||
|
if ($item['state']) {
|
||||||
|
$totalworkingaddon += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->view->assign([
|
||||||
|
'totaluser' => User::count(),
|
||||||
|
'totaladdon' => $totaladdon,
|
||||||
|
'totaladmin' => Admin::count(),
|
||||||
|
'totalcategory' => \app\common\model\Category::count(),
|
||||||
|
'todayusersignup' => User::whereTime('jointime', 'today')->count(),
|
||||||
|
'todayuserlogin' => User::whereTime('logintime', 'today')->count(),
|
||||||
|
'sevendau' => User::whereTime('jointime|logintime|prevtime', '-7 days')->count(),
|
||||||
|
'thirtydau' => User::whereTime('jointime|logintime|prevtime', '-30 days')->count(),
|
||||||
|
'threednu' => User::whereTime('jointime', '-3 days')->count(),
|
||||||
|
'sevendnu' => User::whereTime('jointime', '-7 days')->count(),
|
||||||
|
'dbtablenums' => count($dbTableList),
|
||||||
|
'dbsize' => array_sum(array_map(function ($item) {
|
||||||
|
return $item['Data_length'] + $item['Index_length'];
|
||||||
|
}, $dbTableList)),
|
||||||
|
'totalworkingaddon' => $totalworkingaddon,
|
||||||
|
'attachmentnums' => Attachment::count(),
|
||||||
|
'attachmentsize' => Attachment::sum('filesize'),
|
||||||
|
'picturenums' => Attachment::where('mimetype', 'like', 'image/%')->count(),
|
||||||
|
'picturesize' => Attachment::where('mimetype', 'like', 'image/%')->sum('filesize'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assignconfig('column', array_keys($userlist));
|
||||||
|
$this->assignconfig('userdata', array_values($userlist));
|
||||||
|
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\controller;
|
||||||
|
|
||||||
|
use app\common\controller\Backend;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @icon fa fa-circle-o
|
||||||
|
*/
|
||||||
|
class History extends Backend
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* History模型对象
|
||||||
|
* @var \app\admin\model\History
|
||||||
|
*/
|
||||||
|
protected $model = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 无需额外权限检查的方法(但仍在 admin 模块内,需要 admin 登录)
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $noNeedRight = ['missingNum'];
|
||||||
|
|
||||||
|
public function _initialize()
|
||||||
|
{
|
||||||
|
parent::_initialize();
|
||||||
|
$this->model = new \app\admin\model\History;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询遗漏号码
|
||||||
|
* @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 之间');
|
||||||
|
}
|
||||||
|
$type = $this->request->get('type', 'all');
|
||||||
|
if (!in_array($type, ['all', 'special'])) {
|
||||||
|
$this->error('查询类型不正确');
|
||||||
|
}
|
||||||
|
$result = $this->model->getMissingNumbers($periods, $type);
|
||||||
|
$this->success('查询成功', null, $result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\controller;
|
||||||
|
|
||||||
|
use app\admin\model\AdminLog;
|
||||||
|
use app\common\controller\Backend;
|
||||||
|
use think\Config;
|
||||||
|
use think\Hook;
|
||||||
|
use think\Session;
|
||||||
|
use think\Validate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台首页
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class Index extends Backend
|
||||||
|
{
|
||||||
|
|
||||||
|
protected $noNeedLogin = ['login'];
|
||||||
|
protected $noNeedRight = ['index', 'logout'];
|
||||||
|
protected $layout = '';
|
||||||
|
|
||||||
|
public function _initialize()
|
||||||
|
{
|
||||||
|
parent::_initialize();
|
||||||
|
//移除HTML标签
|
||||||
|
$this->request->filter('trim,strip_tags,htmlspecialchars');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台首页
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$cookieArr = ['adminskin' => "/^skin\-([a-z\-]+)\$/i", 'multiplenav' => "/^(0|1)\$/", 'multipletab' => "/^(0|1)\$/", 'show_submenu' => "/^(0|1)\$/"];
|
||||||
|
foreach ($cookieArr as $key => $regex) {
|
||||||
|
$cookieValue = $this->request->cookie($key);
|
||||||
|
if (!is_null($cookieValue) && preg_match($regex, $cookieValue)) {
|
||||||
|
config('fastadmin.' . $key, $cookieValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//左侧菜单
|
||||||
|
list($menulist, $navlist, $fixedmenu, $referermenu) = $this->auth->getSidebar([
|
||||||
|
'dashboard' => 'hot',
|
||||||
|
'addon' => ['new', 'red', 'badge'],
|
||||||
|
'auth/rule' => __('Menu'),
|
||||||
|
], $this->view->site['fixedpage']);
|
||||||
|
$action = $this->request->request('action');
|
||||||
|
if ($this->request->isPost()) {
|
||||||
|
if ($action == 'refreshmenu') {
|
||||||
|
$this->success('', null, ['menulist' => $menulist, 'navlist' => $navlist]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->assignconfig('cookie', ['prefix' => config('cookie.prefix')]);
|
||||||
|
$this->view->assign('menulist', $menulist);
|
||||||
|
$this->view->assign('navlist', $navlist);
|
||||||
|
$this->view->assign('fixedmenu', $fixedmenu);
|
||||||
|
$this->view->assign('referermenu', $referermenu);
|
||||||
|
$this->view->assign('title', __('Home'));
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员登录
|
||||||
|
*/
|
||||||
|
public function login()
|
||||||
|
{
|
||||||
|
$url = $this->request->get('url', '', 'url_clean');
|
||||||
|
$url = $url ?: 'index/index';
|
||||||
|
if ($this->auth->isLogin()) {
|
||||||
|
$this->success(__("You've logged in, do not login again"), $url);
|
||||||
|
}
|
||||||
|
//保持会话有效时长,单位:小时
|
||||||
|
$keeyloginhours = 24;
|
||||||
|
if ($this->request->isPost()) {
|
||||||
|
$username = $this->request->post('username');
|
||||||
|
$password = $this->request->post('password', '', null);
|
||||||
|
$keeplogin = $this->request->post('keeplogin');
|
||||||
|
$token = $this->request->post('__token__');
|
||||||
|
$rule = [
|
||||||
|
'username' => 'require|length:3,30',
|
||||||
|
'password' => 'require|length:3,30',
|
||||||
|
'__token__' => 'require|token',
|
||||||
|
];
|
||||||
|
$data = [
|
||||||
|
'username' => $username,
|
||||||
|
'password' => $password,
|
||||||
|
'__token__' => $token,
|
||||||
|
];
|
||||||
|
if (Config::get('fastadmin.login_captcha')) {
|
||||||
|
$rule['captcha'] = 'require|captcha';
|
||||||
|
$data['captcha'] = $this->request->post('captcha');
|
||||||
|
}
|
||||||
|
$validate = new Validate($rule, [], ['username' => __('Username'), 'password' => __('Password'), 'captcha' => __('Captcha')]);
|
||||||
|
$result = $validate->check($data);
|
||||||
|
if (!$result) {
|
||||||
|
$this->error($validate->getError(), $url, ['token' => $this->request->token()]);
|
||||||
|
}
|
||||||
|
AdminLog::setTitle(__('Login'));
|
||||||
|
$result = $this->auth->login($username, $password, $keeplogin ? $keeyloginhours * 3600 : 0);
|
||||||
|
if ($result === true) {
|
||||||
|
Hook::listen("admin_login_after", $this->request);
|
||||||
|
$this->success(__('Login successful'), $url, ['url' => $url, 'id' => $this->auth->id, 'username' => $username, 'avatar' => $this->auth->avatar]);
|
||||||
|
} else {
|
||||||
|
$msg = $this->auth->getError();
|
||||||
|
$msg = $msg ? $msg : __('Username or password is incorrect');
|
||||||
|
$this->error($msg, $url, ['token' => $this->request->token()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据客户端的cookie,判断是否可以自动登录
|
||||||
|
if ($this->auth->autologin()) {
|
||||||
|
Session::delete("referer");
|
||||||
|
$this->redirect($url);
|
||||||
|
}
|
||||||
|
$background = Config::get('fastadmin.login_background');
|
||||||
|
$background = $background ? (stripos($background, 'http') === 0 ? $background : config('site.cdnurl') . $background) : '';
|
||||||
|
$this->view->assign('keeyloginhours', $keeyloginhours);
|
||||||
|
$this->view->assign('background', $background);
|
||||||
|
$this->view->assign('title', __('Login'));
|
||||||
|
Hook::listen("admin_login_init", $this->request);
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*/
|
||||||
|
public function logout()
|
||||||
|
{
|
||||||
|
if ($this->request->isPost()) {
|
||||||
|
$this->auth->logout();
|
||||||
|
Hook::listen("admin_logout_after", $this->request);
|
||||||
|
$this->success(__('Logout successful'), 'index/login');
|
||||||
|
}
|
||||||
|
$html = "<form id='logout_submit' name='logout_submit' action='' method='post'>" . token() . "<input type='submit' value='ok' style='display:none;'></form>";
|
||||||
|
$html .= "<script>document.forms['logout_submit'].submit();</script>";
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\controller;
|
||||||
|
|
||||||
|
use app\common\controller\Backend;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数字波色查询
|
||||||
|
*/
|
||||||
|
class Num extends Backend
|
||||||
|
{
|
||||||
|
protected $model = null;
|
||||||
|
|
||||||
|
public function _initialize()
|
||||||
|
{
|
||||||
|
parent::_initialize();
|
||||||
|
$this->model = new \app\admin\model\Num;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回数字与波色的映射关系
|
||||||
|
*/
|
||||||
|
public function getColorMap()
|
||||||
|
{
|
||||||
|
$list = $this->model->field('num,color')->select();
|
||||||
|
$map = [];
|
||||||
|
foreach ($list as $item) {
|
||||||
|
$map[$item['num']] = $item['color'];
|
||||||
|
}
|
||||||
|
$this->success($map);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\controller\auth;
|
||||||
|
|
||||||
|
use app\admin\model\AuthGroup;
|
||||||
|
use app\admin\model\AuthGroupAccess;
|
||||||
|
use app\common\controller\Backend;
|
||||||
|
use fast\Random;
|
||||||
|
use fast\Tree;
|
||||||
|
use think\Db;
|
||||||
|
use think\Validate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员管理
|
||||||
|
*
|
||||||
|
* @icon fa fa-users
|
||||||
|
* @remark 一个管理员可以有多个角色组,左侧的菜单根据管理员所拥有的权限进行生成
|
||||||
|
*/
|
||||||
|
class Admin extends Backend
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \app\admin\model\Admin
|
||||||
|
*/
|
||||||
|
protected $model = null;
|
||||||
|
protected $selectpageFields = 'id,username,nickname,avatar';
|
||||||
|
protected $searchFields = 'id,username,nickname';
|
||||||
|
protected $childrenGroupIds = [];
|
||||||
|
protected $childrenAdminIds = [];
|
||||||
|
|
||||||
|
public function _initialize()
|
||||||
|
{
|
||||||
|
parent::_initialize();
|
||||||
|
$this->model = model('Admin');
|
||||||
|
|
||||||
|
$this->childrenAdminIds = $this->auth->getChildrenAdminIds($this->auth->isSuperAdmin());
|
||||||
|
$this->childrenGroupIds = $this->auth->getChildrenGroupIds($this->auth->isSuperAdmin());
|
||||||
|
|
||||||
|
$groupList = collection(AuthGroup::where('id', 'in', $this->childrenGroupIds)->select())->toArray();
|
||||||
|
|
||||||
|
Tree::instance()->init($groupList);
|
||||||
|
$groupdata = [];
|
||||||
|
if ($this->auth->isSuperAdmin()) {
|
||||||
|
$result = Tree::instance()->getTreeList(Tree::instance()->getTreeArray(0));
|
||||||
|
foreach ($result as $k => $v) {
|
||||||
|
$groupdata[$v['id']] = $v['name'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$result = [];
|
||||||
|
$groups = $this->auth->getGroups();
|
||||||
|
foreach ($groups as $m => $n) {
|
||||||
|
$childlist = Tree::instance()->getTreeList(Tree::instance()->getTreeArray($n['id']));
|
||||||
|
$temp = [];
|
||||||
|
foreach ($childlist as $k => $v) {
|
||||||
|
$temp[$v['id']] = $v['name'];
|
||||||
|
}
|
||||||
|
$result[__($n['name'])] = $temp;
|
||||||
|
}
|
||||||
|
$groupdata = $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->view->assign('groupdata', $groupdata);
|
||||||
|
$this->assignconfig("admin", ['id' => $this->auth->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
//设置过滤方法
|
||||||
|
$this->request->filter(['strip_tags', 'trim']);
|
||||||
|
if ($this->request->isAjax()) {
|
||||||
|
//如果发送的来源是Selectpage,则转发到Selectpage
|
||||||
|
if ($this->request->request('keyField')) {
|
||||||
|
return $this->selectpage();
|
||||||
|
}
|
||||||
|
$childrenGroupIds = $this->childrenGroupIds;
|
||||||
|
$groupName = AuthGroup::where('id', 'in', $childrenGroupIds)
|
||||||
|
->column('id,name');
|
||||||
|
$authGroupList = AuthGroupAccess::where('group_id', 'in', $childrenGroupIds)
|
||||||
|
->field('uid,group_id')
|
||||||
|
->select();
|
||||||
|
|
||||||
|
$adminGroupName = [];
|
||||||
|
foreach ($authGroupList as $k => $v) {
|
||||||
|
if (isset($groupName[$v['group_id']])) {
|
||||||
|
$adminGroupName[$v['uid']][$v['group_id']] = $groupName[$v['group_id']];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$groups = $this->auth->getGroups();
|
||||||
|
foreach ($groups as $m => $n) {
|
||||||
|
$adminGroupName[$this->auth->id][$n['id']] = $n['name'];
|
||||||
|
}
|
||||||
|
list($where, $sort, $order, $offset, $limit) = $this->buildparams();
|
||||||
|
|
||||||
|
$list = $this->model
|
||||||
|
->where($where)
|
||||||
|
->where('id', 'in', $this->childrenAdminIds)
|
||||||
|
->field(['password', 'salt', 'token'], true)
|
||||||
|
->order($sort, $order)
|
||||||
|
->paginate($limit);
|
||||||
|
|
||||||
|
foreach ($list as $k => &$v) {
|
||||||
|
$groups = isset($adminGroupName[$v['id']]) ? $adminGroupName[$v['id']] : [];
|
||||||
|
$v['groups'] = implode(',', array_keys($groups));
|
||||||
|
$v['groups_text'] = implode(',', array_values($groups));
|
||||||
|
}
|
||||||
|
unset($v);
|
||||||
|
$result = array("total" => $list->total(), "rows" => $list->items());
|
||||||
|
|
||||||
|
return json($result);
|
||||||
|
}
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加
|
||||||
|
*/
|
||||||
|
public function add()
|
||||||
|
{
|
||||||
|
if ($this->request->isPost()) {
|
||||||
|
$this->token();
|
||||||
|
$params = $this->request->post("row/a");
|
||||||
|
if ($params) {
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
if (!Validate::is($params['password'], '\S{6,30}')) {
|
||||||
|
exception(__("Please input correct password"));
|
||||||
|
}
|
||||||
|
$params['salt'] = Random::alnum();
|
||||||
|
$params['password'] = $this->auth->getEncryptPassword($params['password'], $params['salt']);
|
||||||
|
$params['avatar'] = '/assets/img/avatar.png'; //设置新管理员默认头像。
|
||||||
|
$result = $this->model->validate('Admin.add')->save($params);
|
||||||
|
if ($result === false) {
|
||||||
|
exception($this->model->getError());
|
||||||
|
}
|
||||||
|
$group = $this->request->post("group/a");
|
||||||
|
|
||||||
|
//过滤不允许的组别,避免越权
|
||||||
|
$group = array_intersect($this->childrenGroupIds, $group);
|
||||||
|
if (!$group) {
|
||||||
|
exception(__('The parent group exceeds permission limit'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$dataset = [];
|
||||||
|
foreach ($group as $value) {
|
||||||
|
$dataset[] = ['uid' => $this->model->id, 'group_id' => $value];
|
||||||
|
}
|
||||||
|
model('AuthGroupAccess')->saveAll($dataset);
|
||||||
|
Db::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
$this->success();
|
||||||
|
}
|
||||||
|
$this->error(__('Parameter %s can not be empty', ''));
|
||||||
|
}
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑
|
||||||
|
*/
|
||||||
|
public function edit($ids = null)
|
||||||
|
{
|
||||||
|
$row = $this->model->get(['id' => $ids]);
|
||||||
|
if (!$row) {
|
||||||
|
$this->error(__('No Results were found'));
|
||||||
|
}
|
||||||
|
if (!in_array($row->id, $this->childrenAdminIds)) {
|
||||||
|
$this->error(__('You have no permission'));
|
||||||
|
}
|
||||||
|
if ($this->request->isPost()) {
|
||||||
|
$this->token();
|
||||||
|
$params = $this->request->post("row/a");
|
||||||
|
if ($params) {
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
if ($params['password']) {
|
||||||
|
if (!Validate::is($params['password'], '\S{6,30}')) {
|
||||||
|
exception(__("Please input correct password"));
|
||||||
|
}
|
||||||
|
$params['salt'] = Random::alnum();
|
||||||
|
$params['password'] = $this->auth->getEncryptPassword($params['password'], $params['salt']);
|
||||||
|
} else {
|
||||||
|
unset($params['password'], $params['salt']);
|
||||||
|
}
|
||||||
|
//这里需要针对username和email做唯一验证
|
||||||
|
$adminValidate = \think\Loader::validate('Admin');
|
||||||
|
$adminValidate->rule([
|
||||||
|
'username' => 'require|regex:\w{3,30}|unique:admin,username,' . $row->id,
|
||||||
|
'email' => 'require|email|unique:admin,email,' . $row->id,
|
||||||
|
'mobile' => 'regex:1[3-9]\d{9}|unique:admin,mobile,' . $row->id,
|
||||||
|
'password' => 'regex:\S{32}',
|
||||||
|
]);
|
||||||
|
$result = $row->validate('Admin.edit')->save($params);
|
||||||
|
if ($result === false) {
|
||||||
|
exception($row->getError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先移除所有权限
|
||||||
|
model('AuthGroupAccess')->where('uid', $row->id)->delete();
|
||||||
|
|
||||||
|
$group = $this->request->post("group/a");
|
||||||
|
|
||||||
|
// 过滤不允许的组别,避免越权
|
||||||
|
$group = array_intersect($this->childrenGroupIds, $group);
|
||||||
|
if (!$group) {
|
||||||
|
exception(__('The parent group exceeds permission limit'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$dataset = [];
|
||||||
|
foreach ($group as $value) {
|
||||||
|
$dataset[] = ['uid' => $row->id, 'group_id' => $value];
|
||||||
|
}
|
||||||
|
model('AuthGroupAccess')->saveAll($dataset);
|
||||||
|
Db::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
$this->success();
|
||||||
|
}
|
||||||
|
$this->error(__('Parameter %s can not be empty', ''));
|
||||||
|
}
|
||||||
|
$grouplist = $this->auth->getGroups($row['id']);
|
||||||
|
$groupids = [];
|
||||||
|
foreach ($grouplist as $k => $v) {
|
||||||
|
$groupids[] = $v['id'];
|
||||||
|
}
|
||||||
|
$this->view->assign("row", $row);
|
||||||
|
$this->view->assign("groupids", $groupids);
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除
|
||||||
|
*/
|
||||||
|
public function del($ids = "")
|
||||||
|
{
|
||||||
|
if (!$this->request->isPost()) {
|
||||||
|
$this->error(__("Invalid parameters"));
|
||||||
|
}
|
||||||
|
$ids = $ids ? $ids : $this->request->post("ids");
|
||||||
|
if ($ids) {
|
||||||
|
$ids = array_intersect($this->childrenAdminIds, array_filter(explode(',', $ids)));
|
||||||
|
// 避免越权删除管理员
|
||||||
|
$childrenGroupIds = $this->childrenGroupIds;
|
||||||
|
$adminList = $this->model->where('id', 'in', $ids)->where('id', 'in', function ($query) use ($childrenGroupIds) {
|
||||||
|
$query->name('auth_group_access')->where('group_id', 'in', $childrenGroupIds)->field('uid');
|
||||||
|
})->select();
|
||||||
|
if ($adminList) {
|
||||||
|
$deleteIds = [];
|
||||||
|
foreach ($adminList as $k => $v) {
|
||||||
|
$deleteIds[] = $v->id;
|
||||||
|
}
|
||||||
|
$deleteIds = array_values(array_diff($deleteIds, [$this->auth->id]));
|
||||||
|
if ($deleteIds) {
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
$this->model->destroy($deleteIds);
|
||||||
|
model('AuthGroupAccess')->where('uid', 'in', $deleteIds)->delete();
|
||||||
|
Db::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
$this->success();
|
||||||
|
}
|
||||||
|
$this->error(__('No rows were deleted'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->error(__('You have no permission'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function multi($ids = "")
|
||||||
|
{
|
||||||
|
// 管理员禁止批量操作
|
||||||
|
$this->error();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下拉搜索
|
||||||
|
*/
|
||||||
|
public function selectpage()
|
||||||
|
{
|
||||||
|
$this->dataLimit = 'auth';
|
||||||
|
$this->dataLimitField = 'id';
|
||||||
|
return parent::selectpage();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\controller\auth;
|
||||||
|
|
||||||
|
use app\admin\model\AuthGroup;
|
||||||
|
use app\common\controller\Backend;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员日志
|
||||||
|
*
|
||||||
|
* @icon fa fa-users
|
||||||
|
* @remark 管理员可以查看自己所拥有的权限的管理员日志
|
||||||
|
*/
|
||||||
|
class Adminlog extends Backend
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \app\admin\model\AdminLog
|
||||||
|
*/
|
||||||
|
protected $model = null;
|
||||||
|
protected $childrenAdminIds = [];
|
||||||
|
|
||||||
|
public function _initialize()
|
||||||
|
{
|
||||||
|
parent::_initialize();
|
||||||
|
$this->model = model('AdminLog');
|
||||||
|
$this->childrenAdminIds = $this->auth->getChildrenAdminIds(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
//设置过滤方法
|
||||||
|
$this->request->filter(['strip_tags', 'trim']);
|
||||||
|
if ($this->request->isAjax()) {
|
||||||
|
list($where, $sort, $order, $offset, $limit) = $this->buildparams();
|
||||||
|
$isSuperAdmin = $this->auth->isSuperAdmin();
|
||||||
|
$childrenAdminIds = $this->childrenAdminIds;
|
||||||
|
$list = $this->model
|
||||||
|
->where($where)
|
||||||
|
->where(function ($query) use ($isSuperAdmin, $childrenAdminIds) {
|
||||||
|
if (!$isSuperAdmin) {
|
||||||
|
$query->where('admin_id', 'in', $childrenAdminIds);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->field('content,useragent', true)
|
||||||
|
->order($sort, $order)
|
||||||
|
->paginate($limit);
|
||||||
|
|
||||||
|
$result = array("total" => $list->total(), "rows" => $list->items());
|
||||||
|
|
||||||
|
return json($result);
|
||||||
|
}
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 详情
|
||||||
|
*/
|
||||||
|
public function detail($ids)
|
||||||
|
{
|
||||||
|
$row = $this->model->get(['id' => $ids]);
|
||||||
|
if (!$row) {
|
||||||
|
$this->error(__('No Results were found'));
|
||||||
|
}
|
||||||
|
if (!$this->auth->isSuperAdmin()) {
|
||||||
|
if (!$row['admin_id'] || !in_array($row['admin_id'], $this->childrenAdminIds)) {
|
||||||
|
$this->error(__('You have no permission'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->view->assign("row", $row->toArray());
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function add()
|
||||||
|
{
|
||||||
|
$this->error();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function edit($ids = null)
|
||||||
|
{
|
||||||
|
$this->error();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除
|
||||||
|
*/
|
||||||
|
public function del($ids = "")
|
||||||
|
{
|
||||||
|
if (!$this->request->isPost()) {
|
||||||
|
$this->error(__("Invalid parameters"));
|
||||||
|
}
|
||||||
|
$ids = $ids ? $ids : $this->request->post("ids");
|
||||||
|
if ($ids) {
|
||||||
|
$isSuperAdmin = $this->auth->isSuperAdmin();
|
||||||
|
$childrenAdminIds = $this->childrenAdminIds;
|
||||||
|
$adminList = $this->model->where('id', 'in', $ids)
|
||||||
|
->where(function ($query) use ($isSuperAdmin, $childrenAdminIds) {
|
||||||
|
if (!$isSuperAdmin) {
|
||||||
|
$query->where('admin_id', 'in', $childrenAdminIds);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->select();
|
||||||
|
if ($adminList) {
|
||||||
|
$deleteIds = [];
|
||||||
|
foreach ($adminList as $k => $v) {
|
||||||
|
$deleteIds[] = $v->id;
|
||||||
|
}
|
||||||
|
if ($deleteIds) {
|
||||||
|
$this->model->destroy($deleteIds);
|
||||||
|
$this->success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->error();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function multi($ids = "")
|
||||||
|
{
|
||||||
|
// 管理员禁止批量操作
|
||||||
|
$this->error();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\controller\auth;
|
||||||
|
|
||||||
|
use app\admin\model\AuthGroup;
|
||||||
|
use app\common\controller\Backend;
|
||||||
|
use fast\Tree;
|
||||||
|
use think\Db;
|
||||||
|
use think\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色组
|
||||||
|
*
|
||||||
|
* @icon fa fa-group
|
||||||
|
* @remark 角色组可以有多个,角色有上下级层级关系,如果子角色有角色组和管理员的权限则可以派生属于自己组别下级的角色组或管理员
|
||||||
|
*/
|
||||||
|
class Group extends Backend
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \app\admin\model\AuthGroup
|
||||||
|
*/
|
||||||
|
protected $model = null;
|
||||||
|
//当前登录管理员所有子组别
|
||||||
|
protected $childrenGroupIds = [];
|
||||||
|
//当前组别列表数据
|
||||||
|
protected $grouplist = [];
|
||||||
|
protected $groupdata = [];
|
||||||
|
//无需要权限判断的方法
|
||||||
|
protected $noNeedRight = ['roletree'];
|
||||||
|
|
||||||
|
public function _initialize()
|
||||||
|
{
|
||||||
|
parent::_initialize();
|
||||||
|
$this->model = model('AuthGroup');
|
||||||
|
|
||||||
|
$this->childrenGroupIds = $this->auth->getChildrenGroupIds(true);
|
||||||
|
|
||||||
|
$groupList = collection(AuthGroup::where('id', 'in', $this->childrenGroupIds)->select())->toArray();
|
||||||
|
|
||||||
|
Tree::instance()->init($groupList);
|
||||||
|
$groupList = [];
|
||||||
|
if ($this->auth->isSuperAdmin()) {
|
||||||
|
$groupList = Tree::instance()->getTreeList(Tree::instance()->getTreeArray(0));
|
||||||
|
} else {
|
||||||
|
$groups = $this->auth->getGroups();
|
||||||
|
$groupIds = [];
|
||||||
|
foreach ($groups as $m => $n) {
|
||||||
|
if (in_array($n['id'], $groupIds) || in_array($n['pid'], $groupIds)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$groupList = array_merge($groupList, Tree::instance()->getTreeList(Tree::instance()->getTreeArray($n['pid'])));
|
||||||
|
foreach ($groupList as $index => $item) {
|
||||||
|
$groupIds[] = $item['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$groupName = [];
|
||||||
|
foreach ($groupList as $k => $v) {
|
||||||
|
$groupName[$v['id']] = $v['name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->grouplist = $groupList;
|
||||||
|
$this->groupdata = $groupName;
|
||||||
|
$this->assignconfig("admin", ['id' => $this->auth->id, 'group_ids' => $this->auth->getGroupIds()]);
|
||||||
|
|
||||||
|
$this->view->assign('groupdata', $this->groupdata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
if ($this->request->isAjax()) {
|
||||||
|
$list = $this->grouplist;
|
||||||
|
$total = count($list);
|
||||||
|
$result = array("total" => $total, "rows" => $list);
|
||||||
|
|
||||||
|
return json($result);
|
||||||
|
}
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加
|
||||||
|
*/
|
||||||
|
public function add()
|
||||||
|
{
|
||||||
|
if ($this->request->isPost()) {
|
||||||
|
$this->token();
|
||||||
|
$params = $this->request->post("row/a", [], 'strip_tags');
|
||||||
|
$params['rules'] = explode(',', $params['rules']);
|
||||||
|
if (!in_array($params['pid'], $this->childrenGroupIds)) {
|
||||||
|
$this->error(__('The parent group exceeds permission limit'));
|
||||||
|
}
|
||||||
|
$parentmodel = model("AuthGroup")->get($params['pid']);
|
||||||
|
if (!$parentmodel) {
|
||||||
|
$this->error(__('The parent group can not found'));
|
||||||
|
}
|
||||||
|
// 父级别的规则节点
|
||||||
|
$parentrules = explode(',', $parentmodel->rules);
|
||||||
|
// 当前组别的规则节点
|
||||||
|
$currentrules = $this->auth->getRuleIds();
|
||||||
|
$rules = $params['rules'];
|
||||||
|
// 如果父组不是超级管理员则需要过滤规则节点,不能超过父组别的权限
|
||||||
|
$rules = in_array('*', $parentrules) ? $rules : array_intersect($parentrules, $rules);
|
||||||
|
// 如果当前组别不是超级管理员则需要过滤规则节点,不能超当前组别的权限
|
||||||
|
$rules = in_array('*', $currentrules) ? $rules : array_intersect($currentrules, $rules);
|
||||||
|
$params['rules'] = implode(',', $rules);
|
||||||
|
if ($params) {
|
||||||
|
$this->model->create($params);
|
||||||
|
$this->success();
|
||||||
|
}
|
||||||
|
$this->error();
|
||||||
|
}
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑
|
||||||
|
*/
|
||||||
|
public function edit($ids = null)
|
||||||
|
{
|
||||||
|
if (!in_array($ids, $this->childrenGroupIds)) {
|
||||||
|
$this->error(__('You have no permission'));
|
||||||
|
}
|
||||||
|
$row = $this->model->get(['id' => $ids]);
|
||||||
|
if (!$row) {
|
||||||
|
$this->error(__('No Results were found'));
|
||||||
|
}
|
||||||
|
if ($this->request->isPost()) {
|
||||||
|
$this->token();
|
||||||
|
$params = $this->request->post("row/a", [], 'strip_tags');
|
||||||
|
//父节点不能是非权限内节点
|
||||||
|
if (!in_array($params['pid'], $this->childrenGroupIds)) {
|
||||||
|
$this->error(__('The parent group exceeds permission limit'));
|
||||||
|
}
|
||||||
|
// 父节点不能是它自身的子节点或自己本身
|
||||||
|
if (in_array($params['pid'], Tree::instance()->getChildrenIds($row->id, true))) {
|
||||||
|
$this->error(__('The parent group can not be its own child or itself'));
|
||||||
|
}
|
||||||
|
$params['rules'] = explode(',', $params['rules']);
|
||||||
|
|
||||||
|
$parentmodel = model("AuthGroup")->get($params['pid']);
|
||||||
|
if (!$parentmodel) {
|
||||||
|
$this->error(__('The parent group can not found'));
|
||||||
|
}
|
||||||
|
// 父级别的规则节点
|
||||||
|
$parentrules = explode(',', $parentmodel->rules);
|
||||||
|
// 当前组别的规则节点
|
||||||
|
$currentrules = $this->auth->getRuleIds();
|
||||||
|
$rules = $params['rules'];
|
||||||
|
// 如果父组不是超级管理员则需要过滤规则节点,不能超过父组别的权限
|
||||||
|
$rules = in_array('*', $parentrules) ? $rules : array_intersect($parentrules, $rules);
|
||||||
|
// 如果当前组别不是超级管理员则需要过滤规则节点,不能超当前组别的权限
|
||||||
|
$rules = in_array('*', $currentrules) ? $rules : array_intersect($currentrules, $rules);
|
||||||
|
$params['rules'] = implode(',', $rules);
|
||||||
|
if ($params) {
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
$row->save($params);
|
||||||
|
$children_auth_groups = model("AuthGroup")->all(['id' => ['in', implode(',', (Tree::instance()->getChildrenIds($row->id)))]]);
|
||||||
|
$childparams = [];
|
||||||
|
foreach ($children_auth_groups as $key => $children_auth_group) {
|
||||||
|
$childparams[$key]['id'] = $children_auth_group->id;
|
||||||
|
$childparams[$key]['rules'] = implode(',', array_intersect(explode(',', $children_auth_group->rules), $rules));
|
||||||
|
}
|
||||||
|
model("AuthGroup")->saveAll($childparams);
|
||||||
|
Db::commit();
|
||||||
|
$this->success();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->error();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->view->assign("row", $row);
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除
|
||||||
|
*/
|
||||||
|
public function del($ids = "")
|
||||||
|
{
|
||||||
|
if (!$this->request->isPost()) {
|
||||||
|
$this->error(__("Invalid parameters"));
|
||||||
|
}
|
||||||
|
$ids = $ids ? $ids : $this->request->post("ids");
|
||||||
|
if ($ids) {
|
||||||
|
$ids = explode(',', $ids);
|
||||||
|
$grouplist = $this->auth->getGroups();
|
||||||
|
$group_ids = array_map(function ($group) {
|
||||||
|
return $group['id'];
|
||||||
|
}, $grouplist);
|
||||||
|
// 移除掉当前管理员所在组别
|
||||||
|
$ids = array_diff($ids, $group_ids);
|
||||||
|
|
||||||
|
// 循环判断每一个组别是否可删除
|
||||||
|
$grouplist = $this->model->where('id', 'in', $ids)->select();
|
||||||
|
$groupaccessmodel = model('AuthGroupAccess');
|
||||||
|
foreach ($grouplist as $k => $v) {
|
||||||
|
// 当前组别下有管理员
|
||||||
|
$groupone = $groupaccessmodel->get(['group_id' => $v['id']]);
|
||||||
|
if ($groupone) {
|
||||||
|
$ids = array_diff($ids, [$v['id']]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 当前组别下有子组别
|
||||||
|
$groupone = $this->model->get(['pid' => $v['id']]);
|
||||||
|
if ($groupone) {
|
||||||
|
$ids = array_diff($ids, [$v['id']]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$ids) {
|
||||||
|
$this->error(__('You can not delete group that contain child group and administrators'));
|
||||||
|
}
|
||||||
|
$count = $this->model->where('id', 'in', $ids)->delete();
|
||||||
|
if ($count) {
|
||||||
|
$this->success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->error();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function multi($ids = "")
|
||||||
|
{
|
||||||
|
// 组别禁止批量操作
|
||||||
|
$this->error();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取角色权限树
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function roletree()
|
||||||
|
{
|
||||||
|
$this->loadlang('auth/group');
|
||||||
|
|
||||||
|
$model = model('AuthGroup');
|
||||||
|
$id = $this->request->post("id");
|
||||||
|
$pid = $this->request->post("pid");
|
||||||
|
$parentGroupModel = $model->get($pid);
|
||||||
|
$currentGroupModel = null;
|
||||||
|
if ($id) {
|
||||||
|
$currentGroupModel = $model->get($id);
|
||||||
|
}
|
||||||
|
if (($pid || $parentGroupModel) && (!$id || $currentGroupModel)) {
|
||||||
|
$id = $id ? $id : null;
|
||||||
|
$ruleList = collection(model('AuthRule')->order('weigh', 'desc')->order('id', 'asc')->select())->toArray();
|
||||||
|
//读取父类角色所有节点列表
|
||||||
|
$parentRuleList = [];
|
||||||
|
if (in_array('*', explode(',', $parentGroupModel->rules))) {
|
||||||
|
$parentRuleList = $ruleList;
|
||||||
|
} else {
|
||||||
|
$parentRuleIds = explode(',', $parentGroupModel->rules);
|
||||||
|
foreach ($ruleList as $k => $v) {
|
||||||
|
if (in_array($v['id'], $parentRuleIds)) {
|
||||||
|
$parentRuleList[] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ruleTree = new Tree();
|
||||||
|
$groupTree = new Tree();
|
||||||
|
//当前所有正常规则列表
|
||||||
|
$ruleTree->init($parentRuleList);
|
||||||
|
//角色组列表
|
||||||
|
$groupTree->init(collection(model('AuthGroup')->where('id', 'in', $this->childrenGroupIds)->select())->toArray());
|
||||||
|
|
||||||
|
//读取当前角色下规则ID集合
|
||||||
|
$adminRuleIds = $this->auth->getRuleIds();
|
||||||
|
//是否是超级管理员
|
||||||
|
$superadmin = $this->auth->isSuperAdmin();
|
||||||
|
//当前拥有的规则ID集合
|
||||||
|
$currentRuleIds = $id ? explode(',', $currentGroupModel->rules) : [];
|
||||||
|
|
||||||
|
if (!$id || !in_array($pid, $this->childrenGroupIds) || !in_array($pid, $groupTree->getChildrenIds($id, true))) {
|
||||||
|
$parentRuleList = $ruleTree->getTreeList($ruleTree->getTreeArray(0), 'name');
|
||||||
|
$hasChildrens = [];
|
||||||
|
foreach ($parentRuleList as $k => $v) {
|
||||||
|
if ($v['haschild']) {
|
||||||
|
$hasChildrens[] = $v['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$parentRuleIds = array_map(function ($item) {
|
||||||
|
return $item['id'];
|
||||||
|
}, $parentRuleList);
|
||||||
|
$nodeList = [];
|
||||||
|
foreach ($parentRuleList as $k => $v) {
|
||||||
|
if (!$superadmin && !in_array($v['id'], $adminRuleIds)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($v['pid'] && !in_array($v['pid'], $parentRuleIds)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$state = array('selected' => in_array($v['id'], $currentRuleIds) && !in_array($v['id'], $hasChildrens));
|
||||||
|
$nodeList[] = array('id' => $v['id'], 'parent' => $v['pid'] ? $v['pid'] : '#', 'text' => __($v['title']), 'type' => 'menu', 'state' => $state);
|
||||||
|
}
|
||||||
|
$this->success('', null, $nodeList);
|
||||||
|
} else {
|
||||||
|
$this->error(__('Can not change the parent to child'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->error(__('Group not found'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\controller\auth;
|
||||||
|
|
||||||
|
use app\admin\model\AuthRule;
|
||||||
|
use app\common\controller\Backend;
|
||||||
|
use fast\Tree;
|
||||||
|
use think\Cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规则管理
|
||||||
|
*
|
||||||
|
* @icon fa fa-list
|
||||||
|
* @remark 规则通常对应一个控制器的方法,同时左侧的菜单栏数据也从规则中体现,通常建议通过控制台进行生成规则节点
|
||||||
|
*/
|
||||||
|
class Rule extends Backend
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \app\admin\model\AuthRule
|
||||||
|
*/
|
||||||
|
protected $model = null;
|
||||||
|
protected $rulelist = [];
|
||||||
|
protected $multiFields = 'ismenu,status';
|
||||||
|
|
||||||
|
public function _initialize()
|
||||||
|
{
|
||||||
|
parent::_initialize();
|
||||||
|
if (!$this->auth->isSuperAdmin()) {
|
||||||
|
$this->error(__('Access is allowed only to the super management group'));
|
||||||
|
}
|
||||||
|
$this->model = model('AuthRule');
|
||||||
|
// 必须将结果集转换为数组
|
||||||
|
$ruleList = \think\Db::name("auth_rule")->field('type,condition,remark,createtime,updatetime', true)->order('weigh DESC,id ASC')->select();
|
||||||
|
foreach ($ruleList as $k => &$v) {
|
||||||
|
$v['title'] = __($v['title']);
|
||||||
|
}
|
||||||
|
unset($v);
|
||||||
|
Tree::instance()->init($ruleList)->icon = [' ', ' ', ' '];
|
||||||
|
$this->rulelist = Tree::instance()->getTreeList(Tree::instance()->getTreeArray(0), 'title');
|
||||||
|
$ruledata = [0 => __('None')];
|
||||||
|
foreach ($this->rulelist as $k => &$v) {
|
||||||
|
if (!$v['ismenu']) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$ruledata[$v['id']] = $v['title'];
|
||||||
|
unset($v['spacer']);
|
||||||
|
}
|
||||||
|
unset($v);
|
||||||
|
$this->view->assign('ruledata', $ruledata);
|
||||||
|
$this->view->assign("menutypeList", $this->model->getMenutypeList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
if ($this->request->isAjax()) {
|
||||||
|
$list = $this->rulelist;
|
||||||
|
$total = count($this->rulelist);
|
||||||
|
$result = array("total" => $total, "rows" => $list);
|
||||||
|
|
||||||
|
return json($result);
|
||||||
|
}
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加
|
||||||
|
*/
|
||||||
|
public function add()
|
||||||
|
{
|
||||||
|
if ($this->request->isPost()) {
|
||||||
|
$this->token();
|
||||||
|
$params = $this->request->post("row/a", [], 'strip_tags');
|
||||||
|
if ($params) {
|
||||||
|
if (!$params['ismenu'] && !$params['pid']) {
|
||||||
|
$this->error(__('The non-menu rule must have parent'));
|
||||||
|
}
|
||||||
|
$result = $this->model->validate()->save($params);
|
||||||
|
if ($result === false) {
|
||||||
|
$this->error($this->model->getError());
|
||||||
|
}
|
||||||
|
$this->success();
|
||||||
|
}
|
||||||
|
$this->error();
|
||||||
|
}
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑
|
||||||
|
*/
|
||||||
|
public function edit($ids = null)
|
||||||
|
{
|
||||||
|
$row = $this->model->get(['id' => $ids]);
|
||||||
|
if (!$row) {
|
||||||
|
$this->error(__('No Results were found'));
|
||||||
|
}
|
||||||
|
if ($this->request->isPost()) {
|
||||||
|
$this->token();
|
||||||
|
$params = $this->request->post("row/a", [], 'strip_tags');
|
||||||
|
if ($params) {
|
||||||
|
if (!$params['ismenu'] && !$params['pid']) {
|
||||||
|
$this->error(__('The non-menu rule must have parent'));
|
||||||
|
}
|
||||||
|
if ($params['pid'] == $row['id']) {
|
||||||
|
$this->error(__('Can not change the parent to self'));
|
||||||
|
}
|
||||||
|
if ($params['pid'] != $row['pid']) {
|
||||||
|
$childrenIds = Tree::instance()->init(collection(AuthRule::select())->toArray())->getChildrenIds($row['id']);
|
||||||
|
if (in_array($params['pid'], $childrenIds)) {
|
||||||
|
$this->error(__('Can not change the parent to child'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//这里需要针对name做唯一验证
|
||||||
|
$ruleValidate = \think\Loader::validate('AuthRule');
|
||||||
|
$ruleValidate->rule([
|
||||||
|
'name' => 'require|unique:AuthRule,name,' . $row->id,
|
||||||
|
]);
|
||||||
|
$result = $row->validate()->save($params);
|
||||||
|
if ($result === false) {
|
||||||
|
$this->error($row->getError());
|
||||||
|
}
|
||||||
|
$this->success();
|
||||||
|
}
|
||||||
|
$this->error();
|
||||||
|
}
|
||||||
|
$this->view->assign("row", $row);
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除
|
||||||
|
*/
|
||||||
|
public function del($ids = "")
|
||||||
|
{
|
||||||
|
if (!$this->request->isPost()) {
|
||||||
|
$this->error(__("Invalid parameters"));
|
||||||
|
}
|
||||||
|
$ids = $ids ? $ids : $this->request->post("ids");
|
||||||
|
if ($ids) {
|
||||||
|
$delIds = [];
|
||||||
|
foreach (explode(',', $ids) as $k => $v) {
|
||||||
|
$delIds = array_merge($delIds, Tree::instance()->getChildrenIds($v, true));
|
||||||
|
}
|
||||||
|
$delIds = array_unique($delIds);
|
||||||
|
$count = $this->model->where('id', 'in', $delIds)->delete();
|
||||||
|
if ($count) {
|
||||||
|
Cache::rm('__menu__');
|
||||||
|
$this->success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->error();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\controller\general;
|
||||||
|
|
||||||
|
use app\common\controller\Backend;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附件管理
|
||||||
|
*
|
||||||
|
* @icon fa fa-circle-o
|
||||||
|
* @remark 主要用于管理上传到服务器或第三方存储的数据
|
||||||
|
*/
|
||||||
|
class Attachment extends Backend
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \app\common\model\Attachment
|
||||||
|
*/
|
||||||
|
protected $model = null;
|
||||||
|
|
||||||
|
protected $searchFields = 'id,filename,url';
|
||||||
|
protected $noNeedRight = ['classify'];
|
||||||
|
|
||||||
|
public function _initialize()
|
||||||
|
{
|
||||||
|
parent::_initialize();
|
||||||
|
$this->model = model('Attachment');
|
||||||
|
$this->view->assign("mimetypeList", \app\common\model\Attachment::getMimetypeList());
|
||||||
|
$this->view->assign("categoryList", \app\common\model\Attachment::getCategoryList());
|
||||||
|
$this->assignconfig("categoryList", \app\common\model\Attachment::getCategoryList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
//设置过滤方法
|
||||||
|
$this->request->filter(['strip_tags', 'trim']);
|
||||||
|
if ($this->request->isAjax()) {
|
||||||
|
$mimetypeQuery = [];
|
||||||
|
$filter = $this->request->request('filter');
|
||||||
|
$filterArr = (array)json_decode($filter, true);
|
||||||
|
if (isset($filterArr['category']) && $filterArr['category'] == 'unclassed') {
|
||||||
|
$filterArr['category'] = ',unclassed';
|
||||||
|
$this->request->get(['filter' => json_encode(array_diff_key($filterArr, ['category' => '']))]);
|
||||||
|
}
|
||||||
|
if (isset($filterArr['mimetype']) && preg_match("/(\/|\,|\*)/", $filterArr['mimetype'])) {
|
||||||
|
$mimetype = $filterArr['mimetype'];
|
||||||
|
$filterArr = array_diff_key($filterArr, ['mimetype' => '']);
|
||||||
|
$mimetypeQuery = function ($query) use ($mimetype) {
|
||||||
|
$mimetypeArr = array_filter(explode(',', $mimetype));
|
||||||
|
foreach ($mimetypeArr as $index => $item) {
|
||||||
|
$query->whereOr('mimetype', 'like', '%' . str_replace("/*", "/", $item) . '%');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
$this->request->get(['filter' => json_encode($filterArr)]);
|
||||||
|
|
||||||
|
list($where, $sort, $order, $offset, $limit) = $this->buildparams();
|
||||||
|
|
||||||
|
$list = $this->model
|
||||||
|
->where($mimetypeQuery)
|
||||||
|
->where($where)
|
||||||
|
->order($sort, $order)
|
||||||
|
->paginate($limit);
|
||||||
|
|
||||||
|
$cdnurl = preg_replace("/\/(\w+)\.php$/i", '', $this->request->root());
|
||||||
|
foreach ($list as $k => &$v) {
|
||||||
|
$v['fullurl'] = ($v['storage'] == 'local' ? $cdnurl : $this->view->config['upload']['cdnurl']) . $v['url'];
|
||||||
|
}
|
||||||
|
unset($v);
|
||||||
|
$result = array("total" => $list->total(), "rows" => $list->items());
|
||||||
|
|
||||||
|
return json($result);
|
||||||
|
}
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择附件
|
||||||
|
*/
|
||||||
|
public function select()
|
||||||
|
{
|
||||||
|
if ($this->request->isAjax()) {
|
||||||
|
return $this->index();
|
||||||
|
}
|
||||||
|
$mimetype = $this->request->get('mimetype', '');
|
||||||
|
$mimetype = substr($mimetype, -1) === '/' ? $mimetype . '*' : $mimetype;
|
||||||
|
$this->view->assign('mimetype', $mimetype);
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加
|
||||||
|
*/
|
||||||
|
public function add()
|
||||||
|
{
|
||||||
|
if ($this->request->isAjax()) {
|
||||||
|
$this->error();
|
||||||
|
}
|
||||||
|
return $this->view->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除附件
|
||||||
|
* @param array $ids
|
||||||
|
*/
|
||||||
|
public function del($ids = "")
|
||||||
|
{
|
||||||
|
if (!$this->request->isPost()) {
|
||||||
|
$this->error(__("Invalid parameters"));
|
||||||
|
}
|
||||||
|
$ids = $ids ? $ids : $this->request->post("ids");
|
||||||
|
if ($ids) {
|
||||||
|
\think\Hook::add('upload_delete', function ($params) {
|
||||||
|
if ($params['storage'] == 'local') {
|
||||||
|
$attachmentFile = ROOT_PATH . '/public' . $params['url'];
|
||||||
|
if (is_file($attachmentFile)) {
|
||||||
|
@unlink($attachmentFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$attachmentlist = $this->model->where('id', 'in', $ids)->select();
|
||||||
|
foreach ($attachmentlist as $attachment) {
|
||||||
|
\think\Hook::listen("upload_delete", $attachment);
|
||||||
|
$attachment->delete();
|
||||||
|
}
|
||||||
|
$this->success();
|
||||||
|
}
|
||||||
|
$this->error(__('Parameter %s can not be empty', 'ids'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归类
|
||||||
|
*/
|
||||||
|
public function classify()
|
||||||
|
{
|
||||||
|
if (!$this->auth->check('general/attachment/edit')) {
|
||||||
|
\think\Hook::listen('admin_nopermission', $this);
|
||||||
|
$this->error(__('You have no permission'), '');
|
||||||
|
}
|
||||||
|
if (!$this->request->isPost()) {
|
||||||
|
$this->error(__("Invalid parameters"));
|
||||||
|
}
|
||||||
|
$category = $this->request->post('category', '');
|
||||||
|
$ids = $this->request->post('ids');
|
||||||
|
if (!$ids) {
|
||||||
|
$this->error(__('Parameter %s can not be empty', 'ids'));
|
||||||
|
}
|
||||||
|
$categoryList = \app\common\model\Attachment::getCategoryList();
|
||||||
|
if ($category && !isset($categoryList[$category])) {
|
||||||
|
$this->error(__('Category not found'));
|
||||||
|
}
|
||||||
|
$category = $category == 'unclassed' ? '' : $category;
|
||||||
|
\app\common\model\Attachment::where('id', 'in', $ids)->update(['category' => $category]);
|
||||||
|
$this->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user