diff --git a/README.md b/README.md index e13726d..99be819 100644 --- a/README.md +++ b/README.md @@ -1,1530 +1,301 @@ +# YwxApp 企业级应用框架 +> 基于 ThinkPHP 6 的多应用 + 插件化企业建站/应用市场框架。框架免费,通过插件与模板商业化变现(Discuz! 式闭环:提交 → 审核 → 上架 → 购买 → 支付 → 授权 → 域名绑定 → 运行期巡检 → 分润提现)。 -# ThinkPHP8 + Layui/HTML 前后端分离权限管理系统设计文档 +- **版本**:`1.0.16` +- **官网**:https://www.ywxapp.cn +- **运行环境**:PHP >= 7.4(推荐 8.0+)、MySQL >= 5.7、Redis(可选,用于缓存/队列) -## 1. 概述 +--- -### 1.1 项目背景 -随着Web应用复杂度提升,权限管理成为系统安全的基石。本文档详细设计基于ThinkPHP8(后端)和Layui/HTML(前端)实现的前后端分离权限管理系统,采用RBAC(基于角色的访问控制)模型,实现细粒度权限控制。 +## 一、特性 -### 1.2 设计目标 -- 实现完整的RBAC权限管理模型 -- 支持菜单级、按钮级、API接口级权限控制 -- 采用JWT进行无状态认证 -- 前后端完全分离,API接口标准化 -- 高性能、高可扩展性、安全可靠 -- 兼容主流浏览器,适配PC端管理后台 +| 特性 | 说明 | +| --- | --- | +| 多应用架构 | `frontend`(企业站/官网,默认首页)、`backend`(后台,入口 `/admin`)、`member`(会员中心,入口 `/user`)、`api`(接口,入口 `/api`) | +| 插件化体系 | 业务功能以插件形式存在(`addon//`),支持在线安装、离线包安装、热重载、一键修复 | +| 应用市场 | 内置 `appmall` 插件(中心站),对接自托管市场,支持购买、授权、域名绑定、升级巡检 | +| 数据库自愈 | 模型壳内置 `ensureSchema()`,缺表建表、缺列补列,部署无需手跑 SQL | +| 可视化设计器 | 开发模式下后台「设计插件」可可视化生成插件骨架、配置项、菜单、路由、页面 | +| 前后台皮肤 | 支持模板/皮肤叠加(`SkinOverlay`),核心后台布局与业务视图分离 | +| 命令行工具 | 插件管理、脚手架生成、健康检测、授权校验、框架升级等全套 CLI | -### 1.3 技术栈 -- **后端**: ThinkPHP 8.0 + MySQL 8.0 + Redis -- **前端**: Layui 2.7 + 原生HTML/JavaScript + Axios -- **认证**: JWT (JSON Web Token) -- **部署**: Nginx + PHP 8.1 +--- -## 2. 系统架构设计 +## 二、环境要求与准备 + +- PHP 7.4+(推荐 8.0+),需开启扩展:`pdo_mysql`、`mbstring`、`curl`、`openssl`、`fileinfo`、`redis`(可选) +- MySQL 5.7+ / MariaDB 10.2+ +- Composer 2.x +- Web 服务器:Nginx / Apache(推荐 Nginx,已内置伪静态规则) + +### 目录权限 + +部署后请确保以下目录可写(Linux 下建议 `755`,运行时目录 `777`): -### 2.1 整体架构 ``` -+-------------------+ +-------------------+ -| 前端层 | | 后端层 | -| (Layui + HTML/JS) |<---->| (ThinkPHP 8.0) | -+-------------------+ HTTP +-------------------+ - | ↑ | ↑ - | | API调用 | | 业务处理 - ↓ | ↓ | -+-------------------+ +-------------------+ -| 静态资源 | | 服务层 | -| (CDN/本地存储) | | (Service/Logic) | -+-------------------+ +-------------------+ - | ↑ - | | 数据操作 - ↓ | - +-------------------+ - | 数据层 | - | (MySQL + Redis) | - +-------------------+ +runtime/ 运行时缓存、日志 +public/static/ 插件静态资源发布目录(安装/重载时写入) +addon/ 插件源码目录 ``` -### 2.2 权限控制流程 -1. 用户登录获取JWT令牌 -2. 前端存储令牌并在每次请求携带 -3. 后端中间件验证令牌有效性 -4. 根据用户角色/权限决定是否放行请求 -5. 前端根据权限数据动态渲染菜单和按钮 +--- -## 3. 数据库设计 +## 三、安装部署 -### 3.1 数据表结构 +### 方式一:Web 安装器(推荐) -#### 3.1.1 管理员表 (admin) -| 字段名 | 类型 | 描述 | -|-------|------|------| -| id | bigint(20) unsigned | 主键ID | -| username | varchar(50) | 用户名 | -| password | varchar(100) | 密码(BCRYPT加密) | -| nickname | varchar(50) | 昵称 | -| avatar | varchar(255) | 头像 | -| email | varchar(100) | 邮箱 | -| mobile | varchar(20) | 手机号 | -| last_login_ip | varchar(50) | 最后登录IP | -| last_login_time | datetime | 最后登录时间 | -| status | tinyint(1) | 状态(0:禁用,1:正常) | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | +项目内置 Web 安装向导,访问站点根目录即可进入: -#### 3.1.2 角色表 (role) -| 字段名 | 类型 | 描述 | -|-------|------|------| -| id | bigint(20) unsigned | 主键ID | -| name | varchar(50) | 角色名称 | -| key | varchar(50) | 角色标识 | -| description | varchar(255) | 角色描述 | -| sort | int(11) | 排序 | -| status | tinyint(1) | 状态(0:禁用,1:正常) | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | +``` +http://your-domain.com/install/ +``` -#### 3.1.3 权限表 (permission) -| 字段名 | 类型 | 描述 | -|-------|------|------| -| id | bigint(20) unsigned | 主键ID | -| name | varchar(50) | 权限名称 | -| key | varchar(100) | 权限标识 | -| type | tinyint(1) | 类型(1:目录,2:菜单,3:按钮,4:API) | -| parent_id | bigint(20) | 父级ID | -| path | varchar(100) | 前端路由路径 | -| component | varchar(100) | 前端组件路径 | -| icon | varchar(50) | 图标 | -| sort | int(11) | 排序 | -| status | tinyint(1) | 状态(0:禁用,1:正常) | -| api_url | varchar(255) | API接口路径 | -| api_method | varchar(20) | API请求方法(GET/POST/PUT/DELETE) | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | +安装器位于 `public/install/`,流程: -#### 3.1.4 角色-管理员关联表 (role_admin) -| 字段名 | 类型 | 描述 | -|-------|------|------| -| role_id | bigint(20) unsigned | 角色ID | -| admin_id | bigint(20) unsigned | 管理员ID | +1. 环境检测(PHP 版本、扩展、目录权限) +2. 配置数据库连接(读取/生成 `.env`) +3. 导入 `public/install/install.sql` 初始化数据表与管理员账号 +4. 安装完成,**务必删除 `public/install/` 目录**以防被重复触发 -#### 3.1.5 角色-权限关联表 (role_permission) -| 字段名 | 类型 | 描述 | -|-------|------|------| -| role_id | bigint(20) unsigned | 角色ID | -| permission_id | bigint(20) unsigned | 权限ID | +> 注意:`.env` 含数据库敏感配置,请勿将其整体覆盖或提交到公开仓库;修改配置请用局部 `replace_in_file`。 -## 4. 后端实现方案 (ThinkPHP8) +### 方式二:手动部署(已有数据库) -### 4.1 JWT认证模块 +```bash +# 1. 安装依赖 +composer install --no-dev -o + +# 2. 复制环境变量模板并填写 +cp .env.example .env # 如无模板则手动创建 .env + +# 3. 导入数据库(自行执行 install.sql 或已有库) + +# 4. 生成应用密钥并设置运行时权限 +php think key:generate # 如支持 +chmod -R 755 runtime addon public/static +``` + +### 伪静态(Nginx 示例) + +```nginx +location / { + if (!-e $request_filename) { + rewrite ^(.*)$ /index.php?s=/$1 last; + break; + } +} +``` + +Apache 已内置 `.htaccess`。 + +--- + +## 四、目录结构 + +``` +ywxapp_dev/ +├── addon/ # 插件目录(核心业务以插件交付) +│ ├── blog/ # 博客插件 +│ ├── forum/ # 论坛插件 +│ ├── articles/ # 内容管理(CMS)插件 +│ ├── haonav/ # 网址导航插件 +│ ├── appmall/ # 应用市场(中心站) +│ └── wxchat/ # 微信客服插件 +├── app/ # 多应用(系统级) +│ ├── frontend/ # 企业站/官网(默认首页) +│ ├── backend/ # 后台管理(入口 /admin) +│ ├── member/ # 会员中心(入口 /user) +│ └── api/ # 接口应用(入口 /api) +├── ywxapp/ # 框架核心库 +│ ├── AddonBase.php # 插件基类(插件控制器须 use ywxapp\AddonBase) +│ ├── service/ # AppService(启动/命令注册/路由加载)、AddonService、MarketService… +│ ├── model/ # BaseModel(数据库自愈引擎) +│ ├── command/ # 命令行(addon:manage / repair / make / health / license-check / ywxapp:upgrade) +│ └── controller/ # BackendBase / FrontendBase / MemberBase 基类 +├── config/ # 全局配置 +│ ├── ywxapp.php # 框架主配置(验证码/多语言/API地址/插件开关…) +│ ├── app.php # 多应用配置(default_app / app_map) +│ ├── route.php # 路由 +│ └── database.php # 数据库 +├── public/ # Web 根目录 +│ ├── install/ # Web 安装器(安装后删除) +│ ├── static/ # 静态资源(插件资源发布到此) +│ └── index.php # 入口 +├── runtime/ # 运行时(缓存/日志,可写) +├── extend/ # 扩展目录 +├── composer.json +└── README.md +``` + +> 旧版 `addons/`(复数)已重命名为 `addon/`(单数),核心基类 `ywxapp/Addons.php` 已重命名为 `AddonBase.php`,请勿再使用旧的 `use ywxapp\Addons`。 + +--- + +## 五、多应用与访问入口 + +`config/app.php` 配置多应用映射(`app_map`): + +| 访问入口 | 应用目录 | 说明 | +| --- | --- | --- | +| `/`(默认) | `app/frontend` | 企业站/官网,`default_app=frontend` | +| `/admin/*` | `app/backend` | 后台管理(登录页 `/admin/login/index`) | +| `/user/*` | `app/member` | 会员中心(登录页 `/user/login/index`) | +| `/api/*` | `app/api` | 接口应用 | + +后台升级相关控制器使用 `framework` 前缀路由(如 `framework/check`、`framework/upgrade`),请勿改回 `upgrade`。 + +--- + +## 六、插件开发 + +### 6.1 插件目录约定 + +``` +addon// +├── info.php # 插件元信息(name/title/version/state/config…) +├── menu.json # 后台/会员/前台菜单(backend/member/frontend 三段) +├── route/ +│ └── app.php # 路由(写相对组名,由 AppService 自动补全插件前缀) +├── controller/ +│ ├── Index.php # 前台控制器 +│ ├── backend/Article.php # 后台控制器 +│ └── member/Article.php # 会员控制器 +├── model/ # 模型(含 ensureSchema() 自愈) +├── service/ # 业务服务 +├── view/ # 模板 +├── install.sql # 安装 SQL(CREATE TABLE + 种子;禁 -- 注释、禁 \' 转义) +├── upgrade.sql # 升级 SQL +└── config.php # 插件配置(支持 .env 覆盖) +``` + +### 6.2 插件元信息 `info.php` -#### 4.1.1 JWT配置 (config/jwt.php) ```php - 'your_strong_secret_key_here_123456', // 需要替换成强密钥 - 'expire' => 7200, // 2小时 - 'refresh_expire' => 2592000, // 30天 - 'header_name' => 'Authorization', - 'header_prefix' => 'Bearer', - 'algorithm' => 'HS256', + 'name' => 'blog', + 'title' => '博客应用', + 'version' => '1.0.2', + 'state' => 1, // 1=启用 + 'url' => '/blog', + 'config' => [], // 插件配置项 ]; ``` -#### 4.1.2 JWT服务类 (app/service/JwtService.php) +### 6.3 控制器基类 + +插件控制器继承框架基类,后台控制器绑定 `AdminAuth` 须用 `static::class`: + ```php config = config('jwt'); - } - - /** - * 生成JWT令牌 - * @param array $payload 用户信息 - * @param bool $rememberMe 是否记住登录 - * @return string - */ - public function generateToken(array $payload, bool $rememberMe = false): string - { - $time = time(); - $expire = $rememberMe ? $this->config['refresh_expire'] : $this->config['expire']; - - $token = [ - 'iat' => $time, // 签发时间 - 'exp' => $time + $expire, // 过期时间 - 'data' => $payload - ]; - - return JWT::encode($token, $this->config['secret_key'], $this->config['algorithm']); - } - - /** - * 验证JWT令牌 - * @param string $token - * @return array - * @throws Exception - */ - public function verifyToken(string $token): array - { - try { - $decoded = JWT::decode($token, $this->config['secret_key'], [$this->config['algorithm']]); - return (array)$decoded->data; - } catch (ExpiredException $e) { - throw new Exception('登录已过期,请重新登录', 401); - } catch (\Exception $e) { - throw new Exception('无效的访问凭证', 401); - } - } - - /** - * 刷新令牌 - * @param string $token - * @return string - * @throws Exception - */ - public function refreshToken(string $token): string - { - $data = $this->verifyToken($token); - return $this->generateToken((array)$data, true); - } + // 禁止无参 fetch(),须 fetch('backend/article/index') } ``` -### 4.2 RBAC权限模型 +### 6.4 路由与菜单 -#### 4.2.1 模型定义 +- 插件 `route/app.php` 写相对组名,由 `AppService::loadAddonRoutes()` 的 `Route::group('<插件名>')` 补全,**不要**加前缀或 `->prefix('api/')`。 +- **后台 URL 必须用「带点」形式**(`blog/backend.article/index`),因为 `Route::group` 下出现 `backend` 层级 TP 会拼成点分隔;斜杠形式恒 404。 +- 修改 `menu.json` 后执行 `php think addon:repair --menu` 重载菜单。 +- **改完路由务必清路由缓存**:`Remove-Item runtime/route_list.php`,否则旧规则导致 404/500。 -**管理员模型 (app/model/Admin.php)** -```php - 时间字段统一:`create_at` / `update_at` / `delete_at`(软删除 int 时间戳,0=未删)。 +> 后台表 `admin_*` → `backend_*`;用户中心 `user_*` → `member_*`。 -class Admin extends Model -{ - // 关联角色 - public function roles(): BelongsToMany - { - return $this->belongsToMany(Role::class, RoleAdmin::class, 'role_id', 'admin_id'); - } - - // 隐藏字段 - protected $hidden = ['password']; - - // 密码写入时加密 - public function setPasswordAttr($value) - { - return password_hash($value, PASSWORD_BCRYPT); - } -} +--- + +## 七、命令行工具 + +命令在 `ywxapp/service/AppService.php` 的 `boot()` 中显式注册(`config/console.php` 的 commands 不生效)。 + +| 命令 | 说明 | +| --- | --- | +| `php think addon:manage -a develop` | 开发者模式安装(源码已在 `addon/`,原地建表/注入菜单/启用,免打包) | +| `php think addon:manage -a install [--local=] [--force]` | 在线/离线安装插件 | +| `php think addon:manage [] -a reload [--force] [--status]` | 热重载插件(重导菜单/资源) | +| `php think addon:repair [--menu\|--db\|--config]` | 一键修复:菜单重载 + 数据库自愈 + 配置补全;`` 可为 `all` | +| `php think addon:make -a [--name=<类>]` | 脚手架:生成 controller/model/middleware/validate/event/listener/service/subscribe | +| `php think addon:health` | 插件健康检测(依赖/表/菜单完整性) | +| `php think addon:license-check` | 运行期授权校验 | +| `php think ywxapp:upgrade` | 框架在线升级(`framework` 路由配套) | + +示例: + +```bash +# 安装 blog 插件(开发者模式) +php think addon:manage blog -a develop + +# 修复所有插件的菜单与数据库 +php think addon:repair all + +# 用脚手架生成一个后台控制器 +php think addon:make blog -a controller --name=backend/Stat ``` -**角色模型 (app/model/Role.php)** -```php -belongsToMany(Admin::class, RoleAdmin::class, 'admin_id', 'role_id'); - } - - // 关联权限 - public function permissions(): BelongsToMany - { - return $this->belongsToMany(Permission::class, RolePermission::class, 'permission_id', 'role_id'); - } -} -``` +### 中心站 / 客户机模式 -**权限模型 (app/model/Permission.php)** -```php -belongsToMany(Role::class, RolePermission::class, 'role_id', 'permission_id'); - } - - // 获取子权限 - public function children() - { - return $this->where('parent_id', $this->id)->order('sort', 'asc')->select(); - } -} -``` +- 插件包:`scripts/package_addon.php`(版本号自动递增) +- 框架包:`scripts/package_framework.php`(排除 `config/`、`.env`、`addon/`、`public/static/<插件名>/`) +- 升级:`AddonService::onlineUpgrade($version)`;`importsql` 仅放行 `CREATE TABLE`/`INSERT`,拒绝 `ALTER`/`DROP`(旧库迁移须手动 `RENAME TABLE`) -#### 4.2.2 权限验证中间件 (app/middleware/Auth.php) -```php - --menu`,最后清路由缓存 `Remove-Item runtime/route_list.php`。 -class Auth -{ - // 无需验证的路由 - protected $except = [ - 'api/login', - 'api/captcha' - ]; - - public function handle(Request $request, Closure $next) - { - // 检查是否在白名单 - $path = $request->pathinfo(); - foreach ($this->except as $except) { - if (strpos($path, $except) !== false) { - return $next($request); - } - } - - // 验证Token - $token = $request->header('Authorization'); - if (!$token) { - return json(['code' => 401, 'msg' => '未授权访问,请登录'])->status(401); - } - - $token = str_replace($this->getConfig('header_prefix') . ' ', '', $token); - try { - $jwtService = new JwtService(); - $userData = $jwtService->verifyToken($token); - - // 写入请求 - $request->userData = $userData; - - // 超级管理员无需验证API权限 - if (in_array($userData['id'], config('system.super_admin_ids', []))) { - return $next($request); - } - - // 权限验证 - if (!$this->checkPermission($request, $userData)) { - return json(['code' => 403, 'msg' => '没有操作权限'])->status(403); - } - - return $next($request); - } catch (\Exception $e) { - return json(['code' => $e->getCode() ?: 401, 'msg' => $e->getMessage()])->status(401); - } - } - - /** - * 检查权限 - */ - protected function checkPermission(Request $request, array $userData): bool - { - $method = $request->method(); - $path = $request->pathinfo(); - - // 从缓存或数据库获取当前用户的所有API权限 - $permissions = $this->getUserApiPermissions($userData['id']); - - // API权限格式:method:path,如:GET:api/admin/user - $requiredPermission = strtoupper($method) . ':' . ltrim($path, '/'); - - return in_array($requiredPermission, $permissions); - } - - /** - * 获取用户API权限 - */ - protected function getUserApiPermissions(int $adminId): array - { - // 使用Redis缓存减少数据库查询 - $cacheKey = 'admin_api_permissions:' . $adminId; - $redis = Cache::store('redis'); - - if ($redis->has($cacheKey)) { - return $redis->get($cacheKey); - } - - // 获取用户角色ID - $admin = Admin::with(['roles'])->find($adminId); - if (!$admin) { - return []; - } - - $roleIds = $admin->roles->column('id'); - if (empty($roleIds)) { - return []; - } - - // 获取权限 - $permissions = RolePermission::whereIn('role_id', $roleIds) - ->with(['permission' => function($query) { - $query->where('type', 4)->where('status', 1); - }]) - ->select() - ->toArray(); - - $apiPermissions = []; - foreach ($permissions as $item) { - if (isset($item['permission']) && $item['permission']) { - $permission = $item['permission']; - $apiPermissions[] = strtoupper($permission['api_method']) . ':' . ltrim($permission['api_url'], '/'); - } - } - - // 缓存2小时 - $redis->set($cacheKey, $apiPermissions, 7200); - - return $apiPermissions; - } - - protected function getConfig($name) - { - return config('jwt.' . $name); - } -} -``` +**Q2:插件装完页面破图 / 资源 404?** +执行 `php think addon:manage -a reload` 重新发布静态资源到 `public/static//`。 -#### 4.2.3 登录控制器 (app/controller/api/Login.php) -```php - --db`。 -namespace app\controller\api; +**Q4:改了插件代码不生效?** +多半是路由缓存。`Remove-Item runtime/route_list.php` 后重试;生产环境关闭 `APP_DEBUG` 前请先清缓存。 -use app\BaseController; -use app\model\Admin; -use app\service\JwtService; -use think\facade\Cache; -use think\facade\Validate; -use think\captcha\Captcha; +**Q5:PHP 文件上传后报「已发送头」/ 乱码?** +`write_to_file` 可能产生 UTF-8 BOM,上线前请去 BOM(或 `php -l` 校验)。 -class Login extends BaseController -{ - // 无需登录验证 - protected $noAuth = ['index', 'captcha']; - - /** - * 生成验证码 - */ - public function captcha() - { - $captcha = new Captcha((array)config('captcha')); - return $captcha->entry(); - } - - /** - * 用户登录 - */ - public function index() - { - $data = $this->request->post(); - - // 验证输入 - $validate = Validate::make([ - 'username|用户名' => 'require', - 'password|密码' => 'require', - 'captcha|验证码' => 'require|captcha' - ]); - - if (!$validate->check($data)) { - return json(['code' => 400, 'msg' => $validate->getError()]); - } - - // 查询用户 - $admin = Admin::where('username', $data['username'])->find(); - if (!$admin || !password_verify($data['password'], $admin->password)) { - return json(['code' => 401, 'msg' => '用户名或密码错误']); - } - - if ($admin->status != 1) { - return json(['code' => 403, 'msg' => '账号已被禁用']); - } - - // 生成Token - $jwtService = new JwtService(); - $isSuper = in_array($admin->id, config('system.super_admin_ids', [])); - - $payload = [ - 'id' => $admin->id, - 'username' => $admin->username, - 'nickname' => $admin->nickname, - 'is_super' => $isSuper ? 1 : 0 - ]; - - $rememberMe = isset($data['remember']) && $data['remember']; - $token = $jwtService->generateToken($payload, $rememberMe); - - // 记录登录信息 - $admin->last_login_time = date('Y-m-d H:i:s'); - $admin->last_login_ip = $this->request->ip(); - $admin->save(); - - // 清除权限缓存 - Cache::store('redis')->rm('admin_api_permissions:' . $admin->id); - Cache::store('redis')->rm('admin_menu_permissions:' . $admin->id); - - return json([ - 'code' => 200, - 'msg' => '登录成功', - 'data' => [ - 'token' => $token, - 'expires_in' => $rememberMe ? config('jwt.refresh_expire') : config('jwt.expire'), - 'user_info' => [ - 'id' => $admin->id, - 'username' => $admin->username, - 'nickname' => $admin->nickname, - 'avatar' => $admin->avatar ?: '/static/admin/images/default_avatar.jpg', - 'is_super' => $isSuper - ] - ] - ]); - } - - /** - * 获取用户信息和权限 - */ - public function info() - { - $adminId = $this->request->userData['id']; - $isSuper = $this->request->userData['is_super'] == 1; - - $cacheKey = 'admin_menu_permissions:' . $adminId; - $redis = Cache::store('redis'); - - if ($redis->has($cacheKey)) { - $data = $redis->get($cacheKey); - return json(['code' => 200, 'msg' => 'success', 'data' => $data]); - } - - // 获取用户角色 - $admin = Admin::with(['roles'])->find($adminId); - $roles = $admin->roles->toArray(); - $roleKeys = array_column($roles, 'key'); - - // 获取菜单和按钮权限 - if ($isSuper) { - // 超级管理员获取所有权限 - $permissions = \app\model\Permission::where('status', 1) - ->order('sort', 'asc') - ->select() - ->toArray(); - } else { - $roleIds = array_column($roles, 'id'); - $permissions = \app\model\RolePermission::whereIn('role_id', $roleIds) - ->with(['permission' => function($query) { - $query->where('status', 1)->order('sort', 'asc'); - }]) - ->select() - ->hidden(['pivot']) - ->toArray(); - - $permissions = array_column($permissions, 'permission'); - } - - // 构建菜单树 - $menus = $this->buildMenuTree($permissions); - - // 提取按钮和API权限 - $buttonPermissions = []; - $apiPermissions = []; - - foreach ($permissions as $permission) { - if ($permission['type'] == 3) { // 按钮权限 - $buttonPermissions[] = $permission['key']; - } elseif ($permission['type'] == 4) { // API权限 - $apiPermissions[] = strtoupper($permission['api_method']) . ':' . ltrim($permission['api_url'], '/'); - } - } - - $data = [ - 'roles' => $roleKeys, - 'menus' => $menus, - 'permissions' => [ - 'buttons' => array_unique($buttonPermissions), - 'apis' => array_unique($apiPermissions) - ], - 'user_info' => [ - 'id' => $admin->id, - 'username' => $admin->username, - 'nickname' => $admin->nickname, - 'avatar' => $admin->avatar ?: '/static/admin/images/default_avatar.jpg', - 'is_super' => $isSuper - ] - ]; - - // 缓存1小时 - $redis->set($cacheKey, $data, 3600); - - return json(['code' => 200, 'msg' => 'success', 'data' => $data]); - } - - /** - * 退出登录 - */ - public function logout() - { - $adminId = $this->request->userData['id']; - - // 清除权限缓存 - Cache::store('redis')->rm('admin_api_permissions:' . $adminId); - Cache::store('redis')->rm('admin_menu_permissions:' . $adminId); - - return json(['code' => 200, 'msg' => '退出成功']); - } - - /** - * 构建菜单树 - */ - protected function buildMenuTree($permissions, $parentId = 0) - { - $tree = []; - foreach ($permissions as $permission) { - if ($permission['parent_id'] == $parentId && in_array($permission['type'], [1, 2])) { - $children = $this->buildMenuTree($permissions, $permission['id']); - if (!empty($children)) { - $permission['children'] = $children; - } - $tree[] = $permission; - } - } - return $tree; - } -} -``` +--- -### 4.3 API接口设计 +## 十一、许可与商业化 -#### 4.3.1 接口规范 -- **请求方式**: RESTful 风格 -- **返回格式**: JSON -- **状态码**: - - 200: 成功 - - 400: 请求参数错误 - - 401: 未授权/登录过期 - - 403: 没有权限 - - 404: 资源不存在 - - 500: 服务器内部错误 +- 框架核心基于 ThinkPHP 协议,免费使用。 +- 商业化通过 `addon/appmall` 应用市场分发插件与模板,支持购买、授权、域名绑定、分润提现。 +- 详细商业化与运营文档见 `docs/`。 -- **响应结构**: -```json -{ - "code": 200, - "msg": "成功", - "data": { - // 业务数据 - } -} -``` +--- -#### 4.3.2 核心接口列表 - -| 接口路径 | 方法 | 描述 | 权限标识 | -|---------|------|------|----------| -| /api/login | POST | 用户登录 | 无需权限 | -| /api/captcha | GET | 获取验证码 | 无需权限 | -| /api/login/info | GET | 获取用户信息和权限 | 无需额外权限 | -| /api/login/logout | POST | 退出登录 | 无需额外权限 | -| /api/admin/menu | GET | 获取菜单列表 | system:menu:list | -| /api/admin/role | GET | 获取角色列表 | system:role:list | -| /api/admin/role | POST | 添加角色 | system:role:add | -| /api/admin/role/:id | PUT | 更新角色 | system:role:edit | -| /api/admin/role/:id | DELETE | 删除角色 | system:role:delete | -| /api/admin/permission | GET | 获取权限列表 | system:permission:list | -| /api/admin/admin | GET | 获取管理员列表 | system:admin:list | -| /api/admin/admin | POST | 添加管理员 | system:admin:add | -| /api/admin/admin/:id | PUT | 更新管理员 | system:admin:edit | -| /api/admin/admin/:id | DELETE | 删除管理员 | system:admin:delete | - -## 5. 前端实现方案 (Layui + HTML) - -### 5.1 项目结构 -``` -static/ -├── admin/ -│ ├── css/ -│ │ ├── common.css # 全局样式 -│ │ └── theme.css # 主题样式 -│ ├── js/ -│ │ ├── common.js # 公共方法 -│ │ ├── http.js # 请求封装 -│ │ ├── permission.js # 权限控制 -│ │ └── menu.js # 菜单生成 -│ ├── lib/ -│ │ ├── layui/ # Layui框架 -│ │ └── axios/ # Axios库 -│ └── views/ -│ ├── dashboard.html # 首页 -│ ├── login.html # 登录页 -│ ├── system/ -│ │ ├── role.html # 角色管理 -│ │ ├── admin.html # 管理员管理 -│ │ └── menu.html # 菜单管理 -│ └── ... -└── upload/ # 上传文件目录 -``` - -### 5.2 核心功能实现 - -#### 5.2.1 请求封装 (static/admin/js/http.js) -```javascript -// Axios实例 -const http = axios.create({ - baseURL: '/api', - timeout: 10000, - headers: { - 'Content-Type': 'application/json;charset=utf-8' - } -}); - -// 请求拦截器 -http.interceptors.request.use(config => { - // 从localStorage获取token - const token = localStorage.getItem('admin_token'); - if (token) { - config.headers.Authorization = 'Bearer ' + token; - } - - // 显示加载层 - if (!config.hideloading) { - layer.load(2); - } - - return config; -}, error => { - return Promise.reject(error); -}); - -// 响应拦截器 -http.interceptors.response.use(response => { - layer.closeAll('loading'); - - const res = response.data; - if (res.code === 200) { - return res.data; - } else if (res.code === 401) { - // Token过期或无效 - layer.msg(res.msg || '登录已失效', { icon: 5, time: 1500 }, function() { - localStorage.removeItem('admin_token'); - localStorage.removeItem('admin_info'); - localStorage.removeItem('admin_routes'); - localStorage.removeItem('admin_permissions'); - window.location.href = '/static/admin/views/login.html'; - }); - return Promise.reject(new Error(res.msg || '请求失败')); - } else { - layer.msg(res.msg || '请求失败', { icon: 5 }); - return Promise.reject(new Error(res.msg || '请求失败')); - } -}, error => { - layer.closeAll('loading'); - - if (error.message.includes('timeout')) { - layer.msg('请求超时,请重试', { icon: 5 }); - } else if (error.message.includes('Network Error')) { - layer.msg('网络异常,请检查网络连接', { icon: 5 }); - } else { - layer.msg('请求异常: ' + error.message, { icon: 5 }); - } - - return Promise.reject(error); -}); - -// 封装常用方法 -const $http = { - get: (url, params = {}, config = {}) => http.get(url, { params, ...config }), - post: (url, data = {}, config = {}) => http.post(url, data, config), - put: (url, data = {}, config = {}) => http.put(url, data, config), - delete: (url, config = {}) => http.delete(url, config), - download: (url, params = {}, filename = 'download') => { - http.post(url, params, { responseType: 'blob', hideloading: true }) - .then(res => { - const blob = new Blob([res]); - const link = document.createElement('a'); - link.href = window.URL.createObjectURL(blob); - link.download = filename; - link.click(); - window.URL.revokeObjectURL(link.href); - }); - } -}; - -// 挂载到window -window.$http = $http; -``` - -#### 5.2.2 权限控制 (static/admin/js/permission.js) -```javascript -// 权限控制类 -const Permission = { - // 按钮权限 - hasButton: function(permissionKey) { - const permissions = JSON.parse(localStorage.getItem('admin_permissions') || '[]'); - return permissions.includes(permissionKey) || this.isSuperAdmin(); - }, - - // 检查多个按钮权限,满足一个即返回true - hasAnyButton: function(permissionKeys) { - if (!Array.isArray(permissionKeys)) return false; - return permissionKeys.some(key => this.hasButton(key)); - }, - - // 检查所有按钮权限,全部满足才返回true - hasAllButton: function(permissionKeys) { - if (!Array.isArray(permissionKeys)) return false; - return permissionKeys.every(key => this.hasButton(key)); - }, - - // 是否为超级管理员 - isSuperAdmin: function() { - const adminInfo = JSON.parse(localStorage.getItem('admin_info') || '{}'); - return adminInfo.is_super === true; - }, - - // 初始化权限 - init: function() { - // 从本地存储获取权限 - this.permissions = JSON.parse(localStorage.getItem('admin_permissions') || '[]'); - this.routes = JSON.parse(localStorage.getItem('admin_routes') || '[]'); - this.adminInfo = JSON.parse(localStorage.getItem('admin_info') || '{}'); - - // 超级管理员拥有所有权限 - if (this.isSuperAdmin()) { - this.permissions = ['*']; - } - }, - - // 渲染带权限控制的元素 - render: function() { - // 隐藏无权限的按钮 - document.querySelectorAll('[permission]').forEach(el => { - const permissionKey = el.getAttribute('permission'); - if (!this.hasButton(permissionKey)) { - el.style.display = 'none'; - } - }); - - // 处理无权限提示 - document.querySelectorAll('[permission-tip]').forEach(el => { - el.addEventListener('click', (e) => { - const permissionKey = el.getAttribute('permission'); - if (!this.hasButton(permissionKey)) { - e.preventDefault(); - e.stopPropagation(); - layer.msg('您没有操作权限', { icon: 5 }); - } - }); - }); - } -}; - -// 初始化 -Permission.init(); - -// 挂载到window -window.Permission = Permission; -``` - -#### 5.2.3 动态菜单生成 (static/admin/js/menu.js) -```javascript -const Menu = { - // 生成菜单 - render: function(containerId) { - const container = document.getElementById(containerId); - if (!container) return; - - const menus = JSON.parse(localStorage.getItem('admin_menus') || '[]'); - container.innerHTML = this.buildMenuHTML(menus); - - // 绑定点击事件 - this.bindMenuEvents(); - }, - - // 递归构建菜单HTML - buildMenuHTML: function(menus) { - if (!menus || menus.length === 0) return '
  • 暂无菜单
  • '; - - let html = ''; - menus.forEach(menu => { - // 菜单类型:1-目录 2-菜单 3-按钮 4-API - if (menu.type === 1) { - // 目录 - const hasChildren = menu.children && menu.children.length > 0; - html += ` -
  • -
  • - - ${menu.name} -
  • - ${hasChildren ? this.buildMenuHTML(menu.children) : ''} - `; - } else if (menu.type === 2) { - // 菜单项 - const hasChildren = menu.children && menu.children.length > 0; - const menuItemId = 'menu-' + menu.id; - - html += ` -
  • - - ${menu.icon ? `` : ''} - ${menu.name} - ${hasChildren ? '' : ''} - - ${hasChildren ? `
      ${this.buildMenuHTML(menu.children)}
    ` : ''} -
  • - `; - } - }); - - return html; - }, - - // 绑定菜单事件 - bindMenuEvents: function() { - // 菜单项点击 - document.querySelectorAll('.layui-menu-body-item:not(.layui-menu-body-parent)').forEach(item => { - item.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - - const url = item.getAttribute('data-url'); - const id = item.getAttribute('data-id'); - if (url) { - this.openTab(id, item.querySelector('span').textContent, url); - this.setActiveMenu(item); - } - }); - }); - - // 有子菜单的父级点击 - document.querySelectorAll('.layui-menu-body-parent').forEach(item => { - const arrow = item.querySelector('.layui-menu-body-arrow'); - if (arrow) { - arrow.addEventListener('click', (e) => { - e.stopPropagation(); - const childMenu = item.querySelector('.layui-menu-body-child'); - if (childMenu) { - childMenu.style.display = childMenu.style.display === 'block' ? 'none' : 'block'; - arrow.classList.toggle('open'); - } - }); - } - }); - }, - - // 打开标签页 - openTab: function(id, title, url) { - const tabContainer = document.querySelector('.layui-tab[lay-filter="admin-tabs"]'); - const tabContent = document.querySelector('.layui-tab-content'); - const tabExists = document.querySelector(`.layui-tab[lay-filter="admin-tabs"] .layui-tab-title li[lay-id="${id}"]`); - - if (!tabExists) { - // 添加标签 - const tabTitle = `
  • ${title}
  • `; - const tabItem = `
    `; - - tabContainer.querySelector('.layui-tab-title').insertAdjacentHTML('beforeend', tabTitle); - tabContent.insertAdjacentHTML('beforeend', tabItem); - - // 重新渲染tab - layui.element.render('tab'); - } else { - // 激活已有标签 - tabContainer.querySelector(`.layui-tab-title li[lay-id="${id}"]`).click(); - } - }, - - // 设置激活菜单 - setActiveMenu: function(item) { - // 移除所有active - document.querySelectorAll('.layui-menu-body-item.active').forEach(el => { - el.classList.remove('active'); - }); - - // 添加active - item.classList.add('active'); - - // 展开父级菜单 - let parent = item.parentElement; - while (parent && parent.classList.contains('layui-menu-body-child')) { - parent.style.display = 'block'; - const arrow = parent.parentElement.querySelector('.layui-menu-body-arrow'); - if (arrow) arrow.classList.add('open'); - parent = parent.parentElement.parentElement; - } - }, - - // 初始化 - init: function() { - // 从本地存储获取菜单 - this.menus = JSON.parse(localStorage.getItem('admin_menus') || '[]'); - } -}; - -// 初始化 -Menu.init(); - -// 挂载到window -window.Menu = Menu; -``` - -### 5.3 登录页面实现 (static/admin/views/login.html) -```html - - - - - - 系统登录 - 权限管理系统 - - - - - - - - - - - - -``` - -### 5.4 后台主框架 (static/admin/views/dashboard.html) -```html - - - - - - 权限管理系统 - - - - - -
    - -
    -
    - -
    PS
    -
    - -
    -
    -
    -
      -
      -
      - - -
      - -
      -
      -
        -
      • 首页
      • -
      -
      -
      - -
      -
      -
      -
      -
      -
      - - - - - - - - - -``` - -## 6. 安全设计 - -### 6.1 认证安全 -- **JWT令牌**: 使用HS256算法签名,设置合理过期时间 -- **HTTPS**: 生产环境强制使用HTTPS -- **密码安全**: 使用BCRYPT算法加密存储,每次登录更新salt -- **验证码**: 登录时验证码防护暴力破解 -- **登录限制**: 同一IP频繁失败登录临时锁定 - -### 6.2 权限安全 -- **最小权限原则**: 每个角色只分配必要权限 -- **权限缓存**: 使用Redis缓存权限,减少数据库查询,提高性能 -- **权限变更**: 权限变更时清除相关缓存 -- **接口验证**: 后端每次请求验证权限,不依赖前端控制 - -### 6.3 数据安全 -- **参数验证**: 所有API接口进行严格参数验证 -- **SQL防注入**: 使用ORM和参数绑定,避免SQL注入 -- **XSS防护**: 前后端对用户输入进行过滤和转义 -- **CSRF防护**: JWT不依赖cookie,天然防CSRF - -### 6.4 其他安全措施 -- **操作日志**: 记录关键操作日志,包括操作人、时间、IP、操作内容 -- **敏感操作二次验证**: 如修改密码、删除重要数据需要二次验证 -- **IP白名单**: 后台管理可配置IP白名单 -- **定期安全审计**: 定期进行安全漏洞扫描和审计 - -## 7. 部署与维护 - -### 7.1 服务器配置 -- **Web服务器**: Nginx 1.18+ -- **PHP版本**: PHP 8.1+ -- **数据库**: MySQL 8.0+ -- **缓存**: Redis 6.0+ -- **运行内存**: 建议2GB+ - -### 7.2 Nginx配置示例 -```nginx -server { - listen 80; - server_name admin.example.com; - root /var/www/permission-system/public; - index index.php index.html; - - # 前端静态资源 - location ~ ^/static/ { - expires 30d; - access_log off; - } - - # API接口 - location ~ ^/api/ { - try_files $uri $uri/ /index.php?$query_string; - } - - # 后台页面 - location ~ \.html$ { - try_files $uri $uri/ /static/admin/views/dashboard.html; - } - - # PHP处理 - location ~ \.php$ { - fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; - fastcgi_index index.php; - include fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param PATH_INFO $fastcgi_path_info; - } - - # 404处理 - location / { - try_files $uri $uri/ /index.php?$query_string; - } - - # 日志配置 - access_log /var/log/nginx/admin-access.log; - error_log /var/log/nginx/admin-error.log; -} -``` - -### 7.3 性能优化 -- **缓存策略**: 合理使用Redis缓存权限、菜单等数据 -- **静态资源**: 静态资源使用CDN加速,设置长缓存 -- **数据库优化**: 合理设计索引,避免N+1查询问题 -- **代码优化**: 使用ThinkPHP8的高性能特性,避免不必要的计算 -- **懒加载**: 前端页面按需加载,提高首屏速度 - -## 8. 扩展与维护 - -### 8.1 代码规范 -- **PSR-12**: 遵循PSR-12代码规范 -- **ESLint**: 前端JavaScript代码规范 -- **注释要求**: 关键函数、类、复杂逻辑必须有注释 -- **Git提交规范**: 遵循Angular提交规范 - -### 8.2 扩展点 -- **多租户支持**: 通过增加tenant_id字段扩展多租户功能 -- **操作日志**: 记录关键操作,支持审计 -- **数据权限**: 增加数据级权限控制,如部门数据隔离 -- **第三方登录**: 支持微信、钉钉等第三方登录 -- **API文档**: 集成Swagger自动生成API文档 - -### 8.3 维护建议 -- **版本控制**: 使用Git进行版本控制,合理使用分支策略 -- **自动化测试**: 编写关键功能的单元测试和接口测试 -- **监控告警**: 集成系统监控,异常时及时告警 -- **定期备份**: 数据库和重要文件定期备份 -- **日志分析**: 收集并分析系统日志,及时发现潜在问题 - -## 9. 总结 - -本设计方案基于ThinkPHP8 + Layui/HTML实现了前后端分离的权限管理系统,具有以下特点: - -1. **完整的RBAC权限控制**:支持菜单级、按钮级、API接口级的细粒度权限管理 -2. **高性能架构**:使用JWT无状态认证,Redis缓存权限数据,前后端分离提升性能 -3. **安全可靠**:多层安全防护,包括认证、授权、数据安全等 -4. **易于扩展**:模块化设计,便于功能扩展和二次开发 -5. **用户体验良好**:使用Layui框架,界面简洁易用,操作流畅 - -通过本方案,可以快速构建一个安全、高效、易维护的权限管理系统,适用于各类企业级应用。 \ No newline at end of file +© 2026-2036 YwxApp. All rights reserved. https://www.ywxapp.cn