feat: hyperf-admin init

This commit is contained in:
daodao97
2020-06-16 22:33:55 +08:00
commit 8d89932c98
198 changed files with 20104 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.idea
composer.lock
/vendor/

22
README.md Normal file
View File

@@ -0,0 +1,22 @@
`HyperfAdmin`是前后端分离的后台管理系统, 前端基于`vue``vue-admin-template`, 针对后台业务`列表`, `表单`等场景封装了大量业务组件, 后端基于`hyperf`实现, 整体思路是后端定义页面渲染规则, 前端页面渲染时首先拉取配置, 然后组件根据具体配置完成页面渲染, 方便开发者仅做少量的配置工作就能完成常见的`CRUD`工作, 同时支持自定义组件和自定义页面, 以开发更为复杂的页面.
[详细文档](https://hyperf-admin.github.io/)
![HyperfAdmin架构](https://cdn.jsdelivr.net/gh/daodao97/FigureBed@master/uPic/sJaJti.png)
前端为`vue multiple page`多页模式, 可以按模块打包, 默认包含两个模块`default` 默认模块, `system`系统管理模块, 绝大部分业务组件在`src/components`目录
后端为`composer包`模式, 目前包含组件
- 基础组件
- `composer require hyperf-admin/base-utils` hyperf-admin的基础组件包, 脚手架主要功能封装
- `composer require hyperf-admin/validation` 参数验证包, 对规则和参数提示做了较多优化
- `composer require hyperf-admin/alert-manager` 企微/钉钉机器人报警包
- `composer require hyperf-admin/rule-engine` 规则引擎
- `composer require hyperf-admin/event-bus` mq/nsq/kafka消息派发器
- `composer require hyperf-admin/process-manager` 进程管理组件
- 业务组件 (业务组件为包含特定业务功能的包)
- `composer require hyperf-admin/admin` 系统管理业务包
- `composer require hyperf-admin/dev-tools` 开发者工具包, 主要是代码生成, 辅助开发
- `composer require hyperf-admin/cron-center` 定时任务管理, 后台化管理任务
- `composer require hyperf-admin/data-focus` 数据面板模块, 帮你快速制作数据大盘

102
composer.json Normal file
View File

@@ -0,0 +1,102 @@
{
"name": "hyperf-admin/hyperf-admin",
"description": "hyperf-admin",
"authors": [
{
"name": "daodao97",
"email": "daodao97@foxmail.com"
}
],
"require": {
"php": ">=7.2",
"ext-json": "*",
"ext-swoole": ">=4.4",
"ext-pdo": "*"
},
"require-dev": {
"aliyuncs/oss-sdk-php": "^2.3",
"box/spout": "^3.1",
"hyperf/amqp": "^1.1",
"hyperf/cache": "~1.1.0",
"hyperf/command": "~1.1.0",
"hyperf/config": "~1.1.0",
"hyperf/constants": "^1.1",
"hyperf/database": "^1.1",
"hyperf/db-connection": "~1.1.0",
"hyperf/filesystem": "^1.1",
"hyperf/framework": "~1.1.0",
"hyperf/guzzle": "~1.1.0",
"hyperf/http-server": "~1.1.0",
"hyperf/logger": "~1.1.0",
"hyperf/memory": "~1.1.0",
"hyperf/metric": "^1.1",
"hyperf/nsq": "^1.1",
"hyperf/process": "~1.1.0",
"hyperf/redis": "~1.1.0",
"hyperf/snowflake": "^1.1",
"yadakhov/insert-on-duplicate-key": "^1.2",
"hyperf/async-queue": "^1.1",
"hyperf/crontab": "^1.1",
"zoujingli/ip2region": "^1.0",
"hyperf/validation": "^1.1"
},
"replace": {
"hyperf-admin/base-utils": "self.version",
"hyperf-admin/admin": "self.version",
"hyperf-admin/alert-manager": "self.version",
"hyperf-admin/cron-center": "self.version",
"hyperf-admin/event-bus": "self.version",
"hyperf-admin/process-manager": "self.version",
"hyperf-admin/rule-engine": "self.version",
"hyperf-admin/validation": "self.version",
"hyperf-admin/workflow": "self.version",
"hyperf-admin/data-focus": "self.version",
"hyperf-admin/dev-tools": "self.version"
},
"autoload": {
"psr-4": {
"HyperfAdmin\\BaseUtils\\": "./src/base-utils/src",
"HyperfAdmin\\Admin\\": "./src/admin/src",
"HyperfAdmin\\AlertManager\\": "./src/alert-manager/src",
"HyperfAdmin\\CronCenter\\": "./src/cron-center/src",
"HyperfAdmin\\DataFocus\\": "./src/data-focus/src",
"HyperfAdmin\\DevTools\\": "./src/dev-tools/src",
"HyperfAdmin\\EventBus\\": "./src/event-bus/src",
"HyperfAdmin\\ProcessManager\\": "./src/process-manager/src",
"HyperfAdmin\\RuleEngine\\": "./src/rule-engine/src",
"HyperfAdmin\\Validation\\": "./src/validation/src"
},
"files": [
"./src/base-utils/src/Helper/array.php",
"./src/base-utils/src/Helper/common.php",
"./src/base-utils/src/Helper/constants.php",
"./src/base-utils/src/Helper/system.php",
"./src/data-focus/src/Util/func.php",
"./src/event-bus/src/funcs.php",
"./src/admin/src/funcs/common.php",
"./src/data-focus/src/Util/SimpleHtmlDom.php"
],
"classmap": [
"./src/base-utils/src/classmap"
]
},
"extra": {
"hyperf": {
"config": [
"HyperfAdmin\\Admin\\ConfigProvider",
"HyperfAdmin\\BaseUtils\\ConfigProvider@99",
"HyperfAdmin\\AlertManager\\ConfigProvider",
"HyperfAdmin\\CronCenter\\ConfigProvider",
"HyperfAdmin\\DataFocus\\ConfigProvider",
"HyperfAdmin\\DevTools\\ConfigProvider",
"HyperfAdmin\\EventBus\\ConfigProvider",
"HyperfAdmin\\ProcessManager\\ConfigProvider"
]
}
},
"minimum-stability": "dev",
"prefer-stable": true,
"config": {
"sort-packages": true
}
}

0
docs/.nojekyll Normal file
View File

34
docs/README.md Normal file
View File

@@ -0,0 +1,34 @@
`hyperf-admin`是前后端分离的后台管理系统, 前端基于`vue``vue-admin-template`, 针对后台业务`列表`, `表单`等场景封装了大量业务组件, 后端基于`hyperf`实现, 整体思路是后端定义页面渲染规则, 前端页面渲染时首先拉取配置, 然后组件根据具体配置完成页面渲染, 方便开发者仅做少量的配置工作就能完成常见的`CRUD`工作, 同时支持自定义组件和自定义页面, 以开发更为复杂的页面.
![hyperf-admin架构](https://cdn.jsdelivr.net/gh/daodao97/FigureBed@master/uPic/sJaJti.png)
前端为`vue multiple page`多页模式, 可以按模块打包, 默认包含两个模块`default` 默认模块, `system`系统管理模块, 绝大部分业务组件在`src/components`目录, 前端文档详见 [这里](/frontend/)
后端为`composer包`模式, 目前包含组件
- 基础组件
- `composer require hyperf-admin/base-utils` hyperf-admin的基础组件包, 脚手架主要功能封装
- `composer require hyperf-admin/validation` 参数验证包, 对规则和参数提示做了较多优化
- `composer require hyperf-admin/alert-manager` 企微/钉钉机器人报警包
- `composer require hyperf-admin/rule-engine` 规则引擎
- `composer require hyperf-admin/event-bus` mq/nsq/kafka消息派发器
- `composer require hyperf-admin/process-manager` 进程管理组件
- 业务组件 (业务组件为包含特定业务功能的包)
- `composer require hyperf-admin/admin` 系统管理业务包
- `composer require hyperf-admin/dev-tools` 开发者工具包, 主要是代码生成, 辅助开发
- `composer require hyperf-admin/cron-center` 定时任务管理, 后台化管理任务
- `composer require hyperf-admin/data-focus` 数据面板模块, 帮你快速制作数据大盘
后端的详细文档见[这里](/backend/)
## 依赖 & 参考
- 前端
- [Vue](https://github.com/vuejs/vue)
- [ElementUI](https://github.com/ElemeFE/element)
- [FormCreate](http://www.form-create.com/v2/guide)
- [vue-admin-template](https://github.com/PanJiaChen/vue-admin-template)
- [Vue 渲染函数 & JSX](https://cn.vuejs.org/v2/guide/render-function.html)
- 后端
- [Hyperf](http://hyperf.wiki/)
- [Swoole](http://wiki.swoole.com)

12
docs/_coverpage.md Normal file
View File

@@ -0,0 +1,12 @@
![logo](_media/icon.svg)
# docsify
> A magical documentation site generator.
* Simple and lightweight (~12kb gzipped)
* Multiple themes
* Not build static html files
[GitHub](https://github.com/docsifyjs/docsify/)
[Get Started](#quick-start)

0
docs/_navbar.md Normal file
View File

21
docs/_sidebar.md Normal file
View File

@@ -0,0 +1,21 @@
* 入门
* [介绍](guide/desc.md)
* [安装](guide/install.md)
* [开发样例](guide/dev_example.md)
* 后端
* [脚手架](backend/scaffold.md)
* [表单详解](backend/form.md)
* [列表详解](backend/list.md)
* [按钮详解](backend/super-button.md)
* [通用配置](backend/common-config.md)
* [辅助函数](backend/functions.md)
* 业务组件
* [DevTools-开发者工具](backend/components/dev-tools.md)
* [DataFocus-数据面板](backend/components/data-focus.md)
* [CronCenter-任务中心](backend/components/cron-center.md)
* [开发一个业务组件](backend/make_component.md)
* 前端
* [表单](frontend/form.md)
* [列表](frontend/list.md)
* [图表](frontend/chart.md)

View File

@@ -0,0 +1,38 @@
通用配置在只需要使用表单搜集信息, 没有过多业务逻辑和校验规则时使用, 可以无需开发, 完成表单的定义和使用
1. 定义表单 http://localhost:9528/system/#/cconf/list, 此处表单规则同控制器中的form定义
![](http://qupinapptest.oss-cn-beijing.aliyuncs.com/img/DGD103.png)
2. 投放表单, 创建 `/***/cconf_{1中的表单名称}` 即可通过该路由访问
![](http://qupinapptest.oss-cn-beijing.aliyuncs.com/img/b9Pw1z.png)
3. 访问 http://localhost:9528/hyperf/#/lucky/cconf_lucky_round
![](http://qupinapptest.oss-cn-beijing.aliyuncs.com/img/I3pgnz.png)
数据存储位置`hyperf_admin.common_config`
```mysql
CREATE TABLE `common_config` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`namespace` varchar(50) NOT NULL DEFAULT '' COMMENT '命名空间, 字母',
`name` varchar(100) NOT NULL COMMENT '配置名, 字母',
`title` varchar(100) NOT NULL DEFAULT '' COMMENT '可读配置名',
`remark` varchar(100) NOT NULL DEFAULT '' COMMENT '备注',
`rules` text COMMENT '配置规则描述',
`value` text COMMENT '具体配置值 key:value',
`permissions` text COMMENT '权限',
`create_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `unique` (`name`,`namespace`),
KEY `namespace` (`namespace`),
KEY `update_at` (`update_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通用配置';
```
可以通过 http://localhost:9528/system/#/cconf/list 管理命名空间
系统中可以使用`Rcok\BaseUtils\Service\CommonConfig`获取相应的提交数据

View File

@@ -0,0 +1,7 @@
脚本作业的管理中心, 可以在代码中实现`class`类型, `command`类型的脚本作业, 在后台添加相关任务, 即可在相应脚本机上执行
可以对任务的入口, 执行规划, 执行节点, 执行参数 等进行配置, 也可在列表主动触发任务
作业必须基于`App\Util\CronCenter\ClassJobAbstract`, 或`App/Util/CronCenter/CommandJobAbstract.php`抽象类进行实现, 才可进行执行状态的跟踪
`CronCenter`的实现基于`hyperf-crontab`进行实现, 具体代码在`app/Util/CronCenter`, 更多细节可查看[文档](https://hyperf.wiki/#/zh-cn/crontab)

View File

@@ -0,0 +1,97 @@
数据大盘
DataFocus (焦点数据) 用途是帮助大家快速构建一个如下的数据面板, 对数据可视化做了大量封装, 让开发者只用关心数据来源和数据处理, 无需处理复杂的图标构建, 即可轻松制作出漂亮的看板, 为业务决策这提供更直观的参考.
数据面板的定义
数据面板中可以定义, `sql`, `json`, `php代码`, `markdown` , `html` 等级数据格式, 通过统一转换由相应前端组件渲染成`图标``列表`
目前支持的图标样式`LineChart`, `ColumnChart`, `PieChart`, `NumberPanel` 分别会渲染为`曲线图`, `柱状图`, `饼图`, `数字面板`, `列表`
### sql节点
1. 定义改节点的属性
1. id 节点名称, 必须, 不可重复
2. dsn sql 查询说使用的dsn, 可用dsn的范围是 hyperf/config/databaes 中 DataFocus Dsn 中定义的数据源
3. chart 图标类型, 格式为`图表名|X轴,Y轴1,Y轴2,…`, 图标名为必须
4. show_table 默认 `false`, 为`true`时除了渲染图表, 还会渲染数据列表
5. table_plugin 表级的插件, 可以对结果数据做二次干预, 可以调用 DataFocus/plugin_fucntion 中定义的全局插件, 也可在调用当前面板中自定义插件
1. 自定义插件为 当前页面的一段`php function`代码
2. 面板中的所有自定义`php`方法, 必须以`df_`开头
6. span 布局 总24的栅格布局, 具体参见
2. 节点内容, 填写构造数据的查询`sql`即可
1. 一个`<sql></slq>` 节点 只能定义一个`sql`语句
2. 只能使用`select`语句
下面的样例中定义的一个以日期`date``X轴`, 其他数据指标为`Y轴`的曲线图
```php
<sql id="近30日访问趋势" dsn="hyperf_admin" chart='LineChart|date' >
select
visitor_uv as "人数",
visitor_pv as "次数"
data_date as date
from
visitor_log
where
data_date >= {{ date('Y-m-d', strtotime('-30 day')) }}
group by date
</sql>
```
其他类型的图标也基本类似, 只用调整相应的`chart`数据即可, 比如下面的饼图
```php
<sql id="今日访问地区占比" dsn="rock_admin" chart="PieChart" table_plugin="df_list_transposition:地区,数量" span="12">
select
area as "地区",
count(1) as "数量"
from
visitor_log
where
data_date >= {{ date('Y-m-d', strtotime('-1 day')) }}
group by area
</sql>
```
3. 模板变量
细心的同学可能已经发现, 上面的`sql`内容中使用了`{{ date('Y-m-d', strtotime('-1 day')) }}`这样的变量定义,模板变量的定义类似 `twig` 语法, 本质上是把花括号的内容转换为`php`代码进行运算, 然后替换模板变量, 执行`sql`.
格式: `{{ func|pip1|pip2 }}`
`func` 为数据的产生源头, 是必须的. 后面`|` 竖线分隔的管道, 让会源头输出的结构做二次处理, 最终替换到模板中
次数模板替换时, 会默认给变量加引号, 比如上方的`sql` 最终会替换为 `data >= '2020-06-11'`, 若需要原样输出, 可只用 `raw` 管道
管道也可以是一个自定义`php` 方法
更多样例见`系统管理/DataFocus/数据面板` 菜单下
### json
节点属性同 sql
节点内容, 可以填入`json`格式数据
```php
<json>
{
"lable":"value"
}
</json>
```
### php
php并非一个单独节点, 而是可以作为一个片段, 迁移任意节点内
```php
<json>
<?php return json_encode(df_******());?>
</json>
```
### md
### html

View File

@@ -0,0 +1,11 @@
## 代码自动生成工具
针对 数据库, `Model`, `Controller` 通用模型, 可以使用后台提供的`代码生成工具`来初始化大部分代码.
![9fnAUL(1)](https://cdn.jsdelivr.net/gh/daodao97/FigureBed@master/uPic/9fnAUL%20%281%29.png)
连接池对应`config.autoload.databases` 中配置的可用链接, 选择好`连接池`, `数据库`, `表` 后下方表单会根据表结构字段渲染, 完成具体字段的配置, 点击提交.
![b4lL49](http://qupinapptest.oss-cn-beijing.aliyuncs.com/img/b4lL49.png)
相应的`Modle`, `Controller` 便已创建成功.

430
docs/backend/form.md Normal file
View File

@@ -0,0 +1,430 @@
## 字段规则
```php
'field|字段名称' => [
// 字段验证规则,
'rule' => 'required|max:',
// 请参考http://www.form-create.com/v2/element-ui/components/input.html
'type' => 'input',
// 表单默认值
'default' => '',
'info' => '字段备注',
// 只读属性,当编辑时有效
'readonly' => true,
// 表单选项只有支持options选项的组件设置才有效可以定义一个callback方法可以参考formOptionsConvert方法
'options' => [],
// 其他组件属性请参考具体组件的props的定义
'props' => [],
// 定义依赖项
'depend' => [
'field' => 'target_type',
'value' => [],
],
// col 布局规则 http://www.form-create.com/v2/element-ui/col.html
'col' => [
// 表单长度
'span' => 12,
// 标签宽度
'labelWidth' => 150,
],
// 动态修改其他字段规则 详见下方联动小节
'compute' => [
"will_set_field" => [
"when" => ['=', 1],
"set" => [
//
]
]
],
// 该字段规则回调方法,可以用于重置字段规则
'render' => function () {
},
// 是否虚拟字段虚拟字段在查询脚手架model时会忽略该字段
'virtual_field' => true,
// 该字段在表单中是否渲染默认true
'form' => false,
],
```
*rule: 后端字段验证规则*
rule完整支持 hyperf 原生的 validation 的校验[规则]([https://hyperf.wiki/#/zh-cn/validation?id=%e9%aa%8c%e8%af%81%e8%a7%84%e5%88%99](https://hyperf.wiki/#/zh-cn/validation?id=验证规则)), 且切封装了高度灵活的自定义校验 `app/Service/ValidationCustomRule.php` 其中定义的方法均可在`rule` 中直接使用, 还支持`cb_***` 调用定义在当前控制器中的自定义验证.
> 注意:目前还没有根据该规则生成前端的验证规则,前端目前只验证了是否必填的
## 内置组件
type表单项类型以下是支持的组件列表以下所有组件 props 均可支持原始文档中的所有属性
### 1. 普通输入框
[原始文档](http://www.form-create.com/v2/iview/components/input.html)
```php
[
"field_name|字段名" => "required|***"
// or
"field_name|字段名" => [
"type" => "input" // 可省略, 默认 input
"rule" => "required|***",
"default" => "默认值" // 非必须,
"info" => "字段提示文字",
"props" => [
"showCopy" => true, // 开启 copy 功能
]
]
]
```
### 2. 数字(整数)
[原始文档](http://www.form-create.com/v2/iview/components/input-number.html)
```php
[
"field_name|字段名" => [
"type" => "number"
]
]
```
### 3. 数字(两位小数)
[原始文档](http://www.form-create.com/v2/iview/components/input-number.html)
```php
[
"field_name|字段名" => [
"type" => "float",
"props" => [
"precision" => 2, // 小数保留位数, 默认2
]
]
]
```
### 4. 多行输入
[原始文档](http://www.form-create.com/v2/iview/components/input.html)
```php
[
"field_name|字段名" => [
"type" => "textarea",
"props" => [
"row" => 6, // 行数, 默认6
]
]
]
```
### 5. 开关(0/1)
[原始文档](http://www.form-create.com/v2/iview/components/switch.html)
```php
[
"field_name|字段名" => [
"type" => "switch"
]
]
```
### 6. 时间控件
[原始文档](http://www.form-create.com/v2/iview/components/date-picker.html)
```php
[
"field_name|字段名" => [
"type" => "datetime"
]
]
```
### 7. 时间区间
[原始文档](http://www.form-create.com/v2/iview/components/date-picker.html)
```php
[
"field_name|字段名" => [
"type" => "datetime_range",
"props" => [
"range" => [
"after" => date('Y-m-d'),
"before" => date('Y-m-d', strtotime('+6 days'))
],
// or 简写
"range" => "afterToday", // afterToday 今天之后, beforeToday 今天之前, 包含今天
]
]
]
```
### 8. 日期
[原始文档](http://www.form-create.com/v2/iview/components/date-picker.html)
```php
[
"field_name|字段名" => [
"type" => "date"
]
]
```
### 9. 日期区间
[原始文档](http://www.form-create.com/v2/iview/components/date-picker.html)
```php
[
"field_name|字段名" => [
"type" => "date_range"
]
]
```
### 10. 下拉选择框
[原始文档](http://www.form-create.com/v2/iview/components/select.html)
```php
[
"field_name|字段名" => [
"type" => "select",
"options" => [ // 远程搜索是无需 支持回调函数 function() { return 备选项; }
1 => "lable1",
2 => "lable2",
],
"props" => [
"selectApi" => "/coupon/act", // 远程搜索模式
"multiple" => true, // 是否多选, 默认false
"multipleLimit" => 10, // 多选时的上限
]
]
]
```
### 11. 上传图片
[原始文档](http://www.form-create.com/v2/iview/components/upload.html)
```php
[
"field_name|字段名" => [
"type" => "image",
"props" => [
// 上传张数上线, 默认1单个
"limit" => 1,
//支持下载
"downloadable" => true,
// 限制上传文件的后缀名
"format " => ['jpg', 'jpeg', 'png', 'gif'],
// 限制上传文件的大小 单位是 kb
"maxSize" => 200,
// 上传的目标 bucket
'bucket' => 'aliyuncs',
// 是否为私有
'private' => true,
]
]
]
```
### 12. 上传文件
[原始文档](http://www.form-create.com/v2/iview/components/upload.html)
```php
[
"field_name|字段名" => [
"type" => "file",
"props" => [
// 上传张数上线, 默认1单个
"limit" => 1,
//支持下载
"downloadable" => true,
// 限制上传文件的后缀名
"format " => ['doc', 'exl', 'ppt'],
// 限制上传文件的大小 单位是 kb
"maxSize" => 200,
]
]
]
```
### 13. 级联选择器
```php
[
"field_name|字段名" => [
"type" => "cascader",
"props" => [
"limit" => 1, // 上传张数上线, 默认1单个
]
]
]
```
### 14. json 组件
```php
[
"field_name|字段名" => [
"type" => "json",
]
]
```
### 15. 富文本
```php
[
"field_name|字段名" => [
"type" => "html",
]
]
```
### 16. 图标选择器
```php
[
"field_name|字段名" => [
"type" => "icon-select",
]
]
```
示例效果:
![icon](http://qupinapptest.oss-cn-beijing.aliyuncs.com/1/202005/9e62be2e0affafc0cdee8bc7fba3c0fd.png)
### 17. 嵌套表单 SubForm
```php
'test|嵌套表单' => [
'type' => 'sub-form',
'children' => [ // 子表单的规则, 同一级规则
'test_sub|嵌套1' => 'required',
'test_sub1|嵌套2' => 'required',
],
'repeat' => true, // 是否可动态添加
'default' => [ // 默认值
[
'test_sub' => 1,
'test_sub1' => 1,
],
[
'test_sub' => 1,
'test_sub1' => 1,
],
],
],
```
示例效果:
![subForm](http://qupinapptest.oss-cn-beijing.aliyuncs.com/img/Snipaste_2020-02-21_19-29-35.png)
### 18. 区域输入框
[前端文档](http://localhost:8080/hyperfdoc/frontend/components/3_InputRange.html)
```php
'test|区域输入框' => [
'type' => 'inputRange',
// value值 type Array or String
// - Array例如[1, 10], 返回结果也将是数组
// - String例如1,10, 返回结果也将是字符串
'value' => [1, 10] or '1,10',
'props' => {
// 是否允许清除
'clearable': true,
// 可控制item宽度等样式 默认宽度300px
'style': 'width: 300px',
// 开始值和结束值的placeholder
'placeholder': ['min', 'max']
},
],
```
## 组件联动
```php
// depend
[
"field_1" => "",
"field_2" => [
"depend" => [
"field" => "field_1", // 依赖字段
"value" => '1' // 当 field_1 = 1 时 field_2 此项才会显示
]
]
]
// hidden
[
"field_1" => "",
"field_2" => [
"hidden" => [
"field" => "field_1", // 影响字段
"value" => '1' // 当 field_2 = 1 时 field_1 项会隐藏
]
]
]
// 备选项 条件控制
[
"field_1" => [
"type" => "select",
"options" => [
[
"value" => 1,
"lable" => ""
],
[
"value" => 0,
"lable" => "",
// 当 disabled_when 条件运算结果, 即为 disabled 的值
"disabled_when" => [
"field_1", '=', 0
],
// or
"disabled_when" => [
["field_2", '=', 0],
["field_3", '!=', 4],
],
]
]
]
]
// 进阶用法 compute 动态计算
[
"field" => [
"compute" => [
"when" => ['=', 1], // 注意这里只有个 比较操作符 和 比较值
// set 操作项
"set" => [
"field_2" => [
// 此处支持控件除 type 外, 完整属性设置
"value" => 1,
// 此处支持 值为 callable
"value" => function() { return time();},
// 重写rule
"rule" => "required"
"props" => [
// ...
]
],
// ... "field_3"
],
// append 项, 尚未实现
// remove 项, 尚未实现
]
]
]
```

78
docs/backend/functions.md Normal file
View File

@@ -0,0 +1,78 @@
## 数组函数
#### array_group_k2k
#### array_group_by
#### array_node_append
#### array_map_recursive
#### array_copy
#### array_sort_by_key_length
#### array_sort_by_value_length
#### array_to_kv
#### array_flat
#### array_depth
#### array_merge_node
#### array_change_v2k
#### array_group
#### array_last
#### array_split
#### array_get_by_keys
#### array_remove
#### array_get_node
#### array_remove_keys_not_in
#### array_remove_keys
#### mt_array_merge
## 系统函数
#### server
#### swoole_server
#### dispatcher
#### register_route
#### move_local_file_to_oss
#### oss_private_url
#### call_self_api
#### select_options
#### process_list_filter
#### get_sub_dir
#### db_complete
#### format_exception
## 内置常量
`DAY`, `HOUR`, `MINUTE`, `YES`, `NO`

571
docs/backend/list.md Normal file
View File

@@ -0,0 +1,571 @@
![](http://km.innotechx.com/download/attachments/82253382/image2020-5-8_17-43-53.png?version=1&modificationDate=1588931034077&api=v2)
```php
return [
......
// 列表定义
'table' => [
// 树型结构列表默认false
"is_tree" => true
// tree 节点为非必须, 默认pid的名称为pid, 不同时需要重写
"tree" => [
"pid" => "pid"
],
// tabs 列表页分页签
'tabs' => [],
// 定义渲染列表, 未定义则获取 form 中所有
'columns' => [],
// 订单行操作按钮
'rowActions' => [],
// 列表上方批量操作的按钮
'batchButtons' => [],
// 页面上方操作按钮
'topActions' => [],
],
];
```
## 列定义
`columns`中定义列表中显示的字段与表头,具体配置如下:
```php
'columns' => [ // 非必须项, 无则从form转义
'字段', // 简写模式, 直接从form配置转义
// or
[
'field' => 'mall_name',
'title' => '店铺',
// 是否显示该字段默认false
'hidden' => true,
// 字段渲染规则,默认为空, 详见列渲染
'type' => '',
// 是否虚拟字段虚拟字段在查询脚手架model时会忽略该字段
'virtual_field' => true,
// 设置Popover提示信息其中
'popover' => [
'messages' => [
'原因:{remark}',
],
'when' => [
['status', '=', Activity::STATUS_OFF]
]
],
// 表头说明
'info' => '括号内为商家承担',
// 定义该字段显示在哪些tab选项中
'depend' => [
'tab' => [(string)Coupon::TYPE_MALL_MONEY_OFF],
],
// 按字段升降查询功能
'sortable' => true,
// 是否允许编辑,调用*/rowchange/:id接口
'edit' => true,
// 允许编辑的条件
'when' => [
['status', '=', Activity::STATUS_OFF]
],
// 枚举值可以options中的数据转换成Tag显示效果https://element.eleme.cn/#/zh-CN/component/tag
'options' => [],
'enum' => [ // tag 的 type 类型, 参见 element 标签
0 => 'info',
1 => 'success',
],
// 列宽设置,默认为均分模式,不支持百分比
'width': '100px',
],
],
```
## when用法
### 使用说明
```php
'when' => ["field_1", ">", 1],
// or 多个条件时为"与"判断
'when' => [
["field_1", ">", 1],
["field_2", "=", 1]
]
```
::: warning 注意
操作符支持`=``>``>=``<``<=``!=``in``not_in`,多个条件时为"与"判断,且注意参数的数据类型是否一致。
:::
- 可以控制行操作及批量按钮是否显示;
- 可以控制批量操作的过滤掉不满足条件的行;
```
'batchButtons' => [
[
.....
// 控制根据条件显示这里的条件字段来源为queryString
'when'=> [
[字段, 操作符, 值]
],
// 为区别控制显示的when关键字这里使用`selectFilter`
'selectFilter' => [
[字段, 操作符, 值]
]
]
]
```
- 控制列表行的某一列的编辑状态
- 控制列渲染的popover模式下的启用状态
### 查看源码
```javascript
export function whereFilter(obj, where, fakeKey) {
if (!where) {
return true
}
let ret = true
let real_where = where
if (where[0] && typeof where[0] === 'string') {
real_where = [where]
}
for (let i = 0; i < real_where.length; i++) {
const item = real_where[i]
const key = fakeKey ? item[0].replace('.', '-') : item[0]
if (item[1] === '=') {
ret = obj[key] === item[2]
}
if (item[1] === '>') {
ret = obj[key] > item[2]
}
if (item[1] === '<') {
ret = obj[key] < item[2]
}
if (item[1] === '>=') {
ret = obj[key] >= item[2]
}
if (item[1] === '<=') {
ret = obj[key] <= item[2]
}
if (item[1] === '!=') {
ret = obj[key] !== item[2]
}
if (item[1] === 'in') {
ret = item[2].indexOf(obj[key]) !== -1
}
if (item[1] === 'not_in') {
ret = item[2].indexOf(obj[key]) === -1
}
if (!ret) {
return false
}
}
return ret
}
```
## 列渲染
渲染类型:
- `number``switch``input`
- `icon``image``extrude``tag``link``iframe``html`
### number
**渲染条件** `'edit' => true` 且 满足when中定义的条件
**用法**
```php
'type' => 'number',
'edit' => true`,
'when' => [
['status', '=', Activity::STATUS_OFF]
],
```
### switch
**渲染条件** `'edit' => true` 且 满足when中定义的条件
**用法**
```php
'type' => 'switch',
'edit' => true`,
'when' => [
['status', '=', Activity::STATUS_OFF]
],
```
### input
**渲染条件** `'edit' => true` 且 满足when中定义的条件
**用法**
```php
'edit' => true`,
'when' => [
['status', '=', Activity::STATUS_OFF]
],
```
::: warning 注意
目前行编辑的表单组件只支持以上三种类型且不能定义该组件的props
:::
### icon
**渲染条件** `'type' => 'icon'`
**效果展示**<i class="omsfont">&#xe65d;</i>
### image
**渲染条件** `'type' => 'image'`,数据为数组时可以渲染多张图片
**效果展示**![u](http://km.innotechx.com/download/attachments/82253382/image2020-5-8_14-6-56.png?version=1&modificationDate=1588918016653&api=v2)
#### extrude
**用法**
```php
[
"field" => "field_1",
"type" => 'extrude',
"render" => function($field_value, $row) {
// return "<优惠券|balck|yellow>*****"; 单个
// or 支持多个
return [
"<优惠券{replace_field}|balck|yellow>*****{replace_field}"
]; // 格式 <文字|背景色|文字色>, 支持前端变量替换
}
]
```
**效果展示**![o](http://km.innotechx.com/download/attachments/47482497/Snipaste_2020-02-20_09-19-03.png?version=1&modificationDate=1582161564093&api=v2)
### tag
**用法**
```php
[
"field" => "field_1",
"options" => [
0 => '禁用',
1 => '启用',
],
"enum" => [ // tag 样式, 参见 https://element.eleme.io/#/zh-CN/component/tag
0 => 'info',
1 => 'success'
]
]
```
**效果展示**![p](http://km.innotechx.com/download/attachments/47482497/Snipaste_2020-02-20_09-27-03.png?version=1&modificationDate=1582162039284&api=v2)
### link
**用法**
```php
[
"field" => "field_1",
"href" => "http://hyperfadmin.com/page/{id}"
]
// or
[
"field" => "field_1",
"href" => [
"href" => "http://hyperfadmin.com/page/{id}",
"type" => "primary",
"target" => "_blank"
]
]
```
**效果展示**![p](http://km.innotechx.com/download/attachments/47482497/Snipaste_2020-02-20_09-22-39.png?version=1&modificationDate=1582161776808&api=v2)
### iframe
**用法**
```php
[
'field' => 'state_info',
'title' => '运行状态',
'type' => 'iframe',
// 列以button形式显示style控制按钮type, [info|success|primary|danger|warning]
'style' => 'primary',
// 弹出窗口宽度,默认500px
'width' => '500px',
// 弹出窗口高度,默认600px
'height' => '600px'
"render" => function($field_value, $row) {
return "url";// 返回 iframe的src路径
}
]
```
### html
**用法**
```php
[
'field' => 'state_info',
'title' => '运行状态',
'type' => 'html',
"render" => function($field_value, $row) {
return '<p>xxxxxxx<br>xxxxx</p>'; //
}
]
```
### supperButton
**用法**
```php
[
'field' => 'field',
'type' => 'supperButton',
// supperButton的配置信息 必须有
// 配置文档详见supperButton部分
'config' => {
}
]
```
### popover弹出框
支持以上除编辑模式下的列渲染类型
**用法**
```php
[
'field' => 'state_info',
'title' => '运行状态',
'popover' => [
'messages' => [
'上线时间: {state.start_time}',
'最后活跃时间: {state.last_time}',
'运行次数: {state.counter}',
],
'when' => ["field_1", ">", 1],
// or
'when' => [
["field_1", ">", 1],
["field_2", "=", 1]
]
]
]
```
**效果展示**![popover弹出框](http://qupinapptest.oss-cn-beijing.aliyuncs.com/img/Snipaste_2020-02-21_18-06-06.png)
### progress
**用法**
```php
[
'field' => 'sync_progress',
'title' => '同步进度',
'type' => 'progress',
// 'props' => []
]
```
> props里是progress组件的属性配置请参考[Progress 进度条](https://element.eleme.cn/#/zh-CN/component/progress#attributes)
**效果展示**![Progress 进度条](http://qupinapptest.oss-cn-beijing.aliyuncs.com/img/Snipaste_2020-02-21_18-06-06.png)
## 搜索项
```php
[
......
// 搜索条件, 前端页面会根据此处配置渲染搜索条件,可以像表单一样配置规则
// 搜索条件中支持模糊搜索也很简单 %field_name%, field_name%, %field_name, 如此定义字段即可
'filter' => [
'id',
'username%',
'create_at|创建时间' => [
'type' => 'date_range',
'search_type' => 'between',
'default' => [date('Y-m-d', time() - DAY * 7), date('Y-m-d', time() + DAY)]
]
],
]
```
## tab切换
```php
'tabs' => [
[
'label' => '平台券',
'value' => (string) Coupon::TYPE_PLATFORM_MONEY_OFF,
'icon' => 'el-icon-s-grid',
],
[
'label' => '店铺券',
'value' => (string) Coupon::TYPE_MALL_MONEY_OFF,
'icon' => 'el-icon-s-grid',
],
],
'columns' => [
[
'field' => 'state_info',
'title' => '运行状态',
// 定义该字段显示在哪些tab选项中
'depend' => [
'tab' => [(string)Coupon::TYPE_MALL_MONEY_OFF],
],
]
]
```
**效果展示**![](http://qupinapptest.oss-cn-beijing.aliyuncs.com/img/Snipaste_2020-02-21_19-46-14.png)
## 操作按钮
支持 列操作按钮, 批量操作按钮, 列表页顶部按钮,前端组件为`SuperButton`
### 基础属性
```php
[
"text" => "", //按钮文案,支持参数替换
"type" => "jump", // 按钮类型 默认 jump, 可选 form, api
"target" => "", // 动作目标 本地路由, 网址, 后端api支持参数替换
"props" => [ // 按钮属性, 更多可见 https://element.eleme.io/#/zh-CN/component/button
"icon" => "", // 按钮图标 默认无
"circle" => false, // 圆角 默认false
"size" => "small", // 默认 small, 可选 medium / small / mini
"type" => "info", //默认text, primary / success / warning / danger / info / text
],
"when" => [ // 当前按钮的显示条件, 默认无
["field_1", "=", 1] // filter过滤的比对数据为当前行 或者 当前页面基础数据
]
]
// 除以上基础属性外, 根据按钮类型, 也会有其他额外属性, 具体见下面
```
### 普通跳转按钮
```javascript
[
"text" => "编辑",
"target" => "/user/12"
]
// or
[
"text" => "文档",
"target" => "http://hyperf.wiki"
]
```
### 动作按钮(请求后端API)
点击按钮后, 提示二次确认, 确认后请求后端api
```javascript
[
"text" => "删除",
"target" => "/user/12",
"method" => "POST", // 请求api的方式, 默认POST
]
```
### 表单型按钮
点击按钮后将以弹窗形式渲染指定表单, 然后搜集后端数据请求到指定api
```javascript
[
"text" => "审核通过",
"target" => "/user/12",
"method" => "POST", // 请求api的方式, 默认POST
"rules" => [ // 表单的rule规则具体参见表单部分
"reason|原因" => [
"rule" => "required"
]
]
]
// 有联动时
[
"text" => "审核通过",
"target" => "/user/12",
"method" => "POST", // 请求api的方式, 默认POST
"rules" => [
// 待补充
]
]
// or
[
"text" => "审核通过",
"target" => "/user/form", // get 方式拉取表单配置, post 方式保存数据
"method" => "POST", // 请求api的方式, 默认POST
]
```
### 列表型按钮
点击按钮后将以弹窗的形式渲染指定列表
```php
[
'type' => 'table', // 调用 src/components/Scaffold/tablist.vue 渲染
'target' => '', // target 留空
'props' => [
'listApi' => '/merchantlog/list?merchant_id={id}', // 列表数据拉取接口
'infoApi' => '/merchantlog/info', // 列表 配置拉取接口
'options' => [ // 表单的配置项
'showFilter' => false,
'createAble' => false
]
],
'text' => '招商记录',
]
```
### 抽屉型按钮
点击按钮后将打开抽屉, 抽屉内部指定动态调用指定组件
```php
[
'type' => 'drawer',
'target' => '', // target 留空
'text' => '查看日志',
'props' => [
'component' => 'SocketList', // 需动态调用的组件 src/components/Common 下
'componentProps' => [ // 组件的 props
'url' => env('OMS_WEBSOCKET_URL') . '/cronlog?name={name}'
],
// drawer** 为抽屉属性的定义
// 详见 https://element.eleme.io/#/zh-CN/component/drawer
'drawerWithHeader' => false,
'drawerSize' => '80%',
'drawerTitle' => '{title}日志',
'drawerDirection' => 'ttb'
]
]
```
### 下拉按钮
上面 SuperButton 的结构改为数组形式即可

408
docs/backend/scaffold.md Normal file
View File

@@ -0,0 +1,408 @@
## 路由注册
一个独立的业务模块需要在`config/routes/`下添加业务的路由文件,在该文件内完成业务模块所有的路由定义。可以使用`register_route`方法来定义您的路由。
```php
<?php
use Hyperf\HttpServer\Router\Router;
use App\Controller\IndexController;
register_route('/index', IndexController::class, function ($controller) {
// 其他路由的定义
Router::get('/hello-hyperf', [$controller, 'hello']);
});
```
!> 如果完全是自定义的前端页面,建议不使用`register_route`注册路由,`register_route`内部会注册一些脚手架路由
**脚手架路由**
| uri | 请求方式 | 控制器方法 | 说明 |
| :----------------------------------------------------------- | :------- | :---------------- | :---------------------------------------------- |
| `path`/list.json<br>`path`/info | GET | info | 下发列表页的配置 |
| `path`/form.json<br>`path`/form<br>`path`/{id:\d+}.json<br>`path`/{id:\d+} | GET | form、edit | 下发表单配置 |
| `path`/list | GET | list | 下发列表数据 |
| `path`/form | POST | save | 新增时数据保存接口 |
| `path`/{id:\d+} | POST | save | 编辑时数据保存接口 |
| `path`/delete | POST | delete | 删除接口 |
| `path`/batchdel | POST | batchDelete | 批量删除接口 |
| `path`/rowchange/{id:\d+} | POST | rowChange | 行编辑数据保存接口 |
| `path`/childs/{id:\d+} | GET | getTreeNodeChilds | 树结构的列表页动态获取子节点的接口 |
| `path`/newversion/{id:\d+}/{last_ver_id:\d+} | GET | newVersion | 表单编辑时或数据对象的版本信息接口 |
| `path`/export | POST | export | 导出任务接口 |
| `path`/act | GET | act | 可用于当前model提供select组件远程搜索的数据接口 |
| `path`/import | POST | import | 导入接口 |
## 脚手架概览
在编写控制器时需`继承`脚手架的抽象类`AbstractController`,并在`scaffoldOptions`方法中定义页面的配置。
```php
<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\Admin\Controller\AdminAbstractController;
class IndexController extends AdminAbstractController
{
// 操作的 model 对象
public $model_class = User::class;
// 操作的entity
// entity 为脚手架抽象出的一个实体, 包含对象的 curd 操作
// 目前支持 mysql/es/mongo/api
// model 和 entity 任选其一即可
public $entity_class = UserEntity::class;
// 脚手架核心配置
public function scaffoldOptions()
{
return [
// 自定义创建按钮的跳转路由, 默认 /user/form
'form_path' => '/custom/path',
// 是否允许创建, 默认 true, false怎隐藏页面列表上方的新建按钮
'createAble' => false,
// 是否允许删除, 默认 true
'deleteAble' => true,
// 是否开启通知查询功能,开启后在页面路由发生变化时,会根据当前页面参数查询页面有没有通知消息
'noticeAble' => true,
// 是否需要分页器 默认true
'paginationEnable' => true,
// 是否显示导出按钮, 默认true
'exportAble' => true,
// 列表页是否默认执行查询, 默认执行查询
'defaultList' => false,
// 搜索条件, 前端页面会根据此处配置渲染搜索条件,可以像表单一样配置规则
// 搜索条件中支持模糊搜索也很简单 %field_name%, field_name%, %field_name, 如此定义字段即可
'filter' => ['id', 'username%', 'create_at'],
// 列表的基础筛选条件, 列表的查询均会携带上此处的条件, 详情请查看where2query方法
'where' => [
'type' => User::STATUS_ON,
],
// 筛选条件是否同步到地址栏
"filterSyncToQuery" => false,
// 列表的排序
'order_by' => 'id desc',
// 表单页面的UI配置, 详参 http://form-create.com/v2/iview/global.html
'formUI' => [
'form' => [
'lableWidth' => '300px'
],
'submitBtn' => [
'innerText' => '这是提交按钮'
]
],
// form表单的定义, 核心配置, 不可或缺,请求请查看表单页配置
'form' => [
'field|字段名称' => [
// 字段验证规则
'rule' => 'required|max',
'type' => 'input',
'info' => '字段备注',
],
],
// 页面提示
'notices' => [
[
'type' => 'warning',
'message' => '提示信息',
'actionsPlacement' => 'right',
'closable' => true,
'actions' => [
[
'props' => [
'size' => 'mini',
'type' => 'success',
],
// 勿动!! 选品页面是监听点击事件同当前 text 比对.
'text' => '点我更新',
'type' => 'native',
]
],
'when' => function($filters) { return true;}
]
],
// 第三方数据补充, 子项定义规范, 详见下方列表第三方数据补充部分
'hasOne' => [
'mt_oms.mt_oms.user_role:user_id,role_id'
],
// 列表定义
'table' => [
// tabs 列表页分页签
'tabs' => [],
// 定义渲染列表, 未定义则获取 form 中所有
'columns' => [],
// 订单行操作按钮
'rowActions' => [],
// 列表上方批量操作的按钮
'batchButtons' => [],
// 页面上方操作按钮
'topActions' => [],
],
];
}
}
```
## 内置钩子
```php
// 列表页下发配置接口的前置钩子
public function beforeInfo(&$info) {}
// 列表页执行搜索前的钩子, 可用于修改 where 条件
public function beforeListQuery(&$conditions) {}
// 列表数据响应前的钩子, 可用于补充额外数据
public function beforeListResponse(&$list) {}
// form 规则下发前的干预钩子
public function meddleFormRule($id, &$form_rule) {}
// form 响应前的钩子
public function beforeFormResponse($id, &$record) {}
// 表单保存前端钩子函数
public function beforeSave($pk_val, &$data) {}
// 表单保存后的钩子函数
public function afterSave($pk_val, &$data) {}
// 删除前的回调钩子
public function beforeDelete($pk_val) {}
// 删除后的回调钩子
public function afterDelete($pk_val, $deleted) {}
```
## 表单定义
```php
public function scaffoldOptions()
{
return [
....
// 表单配置
'form' => [
// 字段验证规则
// 请参考 https://hyperf.wiki/#/zh-cn/validation?id=%e9%aa%8c%e8%af%81%e8%a7%84%e5%88%99
'rule' => 'required|max:10',
// 请参考 http://www.form-create.com/v2/element-ui/components/input.html
'type' => 'input',
// 表单默认值
'default' => '',
'info' => '字段备注',
// 只读属性,当编辑时有效
'readonly' => true,
// 表单选项只有支持options选项的组件设置才有效可以定义一个callback方法可以参考formOptionsConvert方法
'options' => [],
// 其他组件属性请参考具体组件的props的定义
'props' => [],
// 定义依赖项
'depend' => [
'field' => 'target_type',
'value' => [],
],
// col 布局规则 http://www.form-create.com/v2/element-ui/col.html
'col' => [
// 表单长度
'span' => 12,
// 标签宽度
'labelWidth' => 150,
],
// 动态修改其他字段规则 详见下方联动小节
'compute' => [
"will_set_field" => [
"when" => ['=', 1],
"set" => [
//
]
]
],
// 该字段规则回调方法,可以用于重置字段规则
'render' => function () {
},
// 开启input框的复制功能
'copy_show' => true,
],
];
}
```
## 列表定义
```php
public function scaffoldOptions()
{
return [
....
// 非必须项, 没有定义则从form转义
'columns' => [
'字段名', // 简写模式, 直接从form配置转义
[
'field' => 'mall_name',
'title' => '店铺',
// 字段渲染规则,默认为空
'type' => '',
// 是否虚拟字段虚拟字段在查询脚手架model时会忽略该字段
'virtual_field' => true,
// 表头说明
'info' => '括号内为商家承担',
// 定义该字段显示在哪些tab选项中
'depend' => [
'tab' => [(string)Coupon::TYPE_MALL_MONEY_OFF],
],
// 按字段升降查询功能
'sortable' => true,
// 是否允许编辑,调用*/rowchange/:id接口
'edit' => true,
// 枚举值可以options中的数据转换成Tag显示效果https://element.eleme.cn/#/zh-CN/component/tag
'options' => [],
'enum' => [ // tag 的 type 类型, 参见 element 标签
0 => 'info',
1 => 'success',
],
// 单独处理某个字段
'render' => function($val, $row) { return $val;}
],
],
];
}
```
## 按钮
`rowActions`, `topButtons`, `topActions`, `notices.*.actions` 的节点定义
1. 页面跳转
```php
[
'type' => 'jump',
'target' => '/crontab/{id}', // 本地路由或三方地址
'text' => '编辑',
'props' => [] // element el-button 的属性
]
```
2. 请求后端api
```php
[
'type' => 'api',
'target' => '/resource/delete', // 支持变量替换
'text' => '删除',
'method' => 'POST', // 默认POST
'props' => [
'type' => 'danger',
],
// 当前按钮可以定义依赖条件, 动态显示
'when' => [
['gid', '=', Resource::RESOURCE_ROOT_ID]
]
],
```
3. model弹窗表单
```php
// 直接定义表单规则 rules
[
'action' => 'module',
'target' => '/user/test/{id}',
'text' => '弹窗',
// rules 的定义同 form rule
'rules' => [
'file|视频' => [
'type' => 'file',
],
],
]
// 调用其他Controller form表单
[
'action' => 'module',
// 若没有rules节点则自定调用 target 接口拉取表单配置
'target' => '/user/form',
'text' => '弹窗',
]
// 调用其他 Controller 的列表
[
'type' => 'table',
'target' => '',
'props' => [
'listApi' => '/role/list?id={id}',
'infoApi' => '/role/info',
'options' => [
'showFilter' => false,
'createAble' => false
]
],
'text' => '**记录',
]
```
按钮较多时均可调整为按钮组
```php
[
[
$action_conf1,
$action_conf2
]
]
```
## 关联数据
```php
public function scaffoldOptions()
{
return [
....
// 一对一关系
'hasOne' => [
// 此处定义了补充的第三方数据是什么, 从哪里取
// [pool.]db.table:[local_key->]foreign_key,other_key
'hyperf_admin.hyperf_admin.user_role:id->user_id,role_id', // 完整定义
'hyperf_admin.user_role:id->user_id,role_id', // 缺省 pool
'hyperf_admin.user_role:user_id,role_id', // 缺省 pool,local_key
'hyperf_admin.user_role:user_id,role_id as rid', // 补充字段使用别名, 避免覆盖list中同名字段
],
// 一对多或多对多关系
'hasMany' => [
'hyperf_admin.hyperf_admin.operator_log:id->user_id,username'
]
];
}
```
`[pool.]db.table:[local_key->]foreign_key,other_key`
分别对应`连接池`(非必须, 默认dfault), `库名`, `表名`, `本地关联字段`(非必须, 默认id), `逻辑外键`, `其他要补充的字段`, 若系统为查询到第三方数据, 相应的补充字段将初始为 `null`
## 页面提示
```php
public function scaffoldOptions()
{
return [
....
// 页面提示信息
'notices' => [
[
'type' => 'warning',
'message' => '提示信息',
'actionsPlacement' => 'right',
'closable' => true,
'actions' => [], // 按钮
'when' => function($filters) { return true;}
]
]
];
}
```

View File

@@ -0,0 +1,132 @@
支持 列操作按钮, 批量操作按钮, 列表页顶部按钮
### 基础属性
```php
[
"text" => "", //按钮文案
"type" => "jump", // 按钮类型 默认 jump, 可选 form, api
"target" => "", // 动作目标 本地路由, 网址, 后端api
"props" => [ // 按钮属性, 更多可见 https://element.eleme.io/#/zh-CN/component/button
"icon" => "", // 按钮图标 默认无
"circle" => false, // 圆角 默认false
"size" => "small", // 默认 small, 可选 medium / small / mini
"type" => "info", //默认text, primary / success / warning / danger / info / text
],
"when" => [ // 当前按钮的显示条件, 默认无
["field_1", "=", 1] // filter过滤的比对数据为当前行 或者 当前页面基础数据
]
]
// 除以上基础属性外, 根据按钮类型, 也会有其他额外属性, 具体见下面
```
#### 普通跳转按钮
```javascript
[
"text" => "编辑",
"target" => "/user/12"
]
// or
[
"text" => "文档",
"target" => "http://hyperf.wiki"
]
```
#### 动作按钮(请求后端API)
点击按钮后, 提示二次确认, 确认后请求后端api
```javascript
[
"text" => "删除",
"target" => "/user/12",
"method" => "POST", // 请求api的方式, 默认POST
]
```
#### 表单型按钮
点击按钮后将以弹窗形式渲染指定表单, 然后搜集后端数据请求到指定api
```javascript
[
"text" => "审核通过",
"target" => "/user/12",
"method" => "POST", // 请求api的方式, 默认POST
"rules" => [ // 表单的rule规则具体参见表单部分
"reason|原因" => [
"rule" => "required"
]
]
]
// 有联动时
[
"text" => "审核通过",
"target" => "/user/12",
"method" => "POST", // 请求api的方式, 默认POST
"rules" => [
// 待补充
]
]
// or
[
"text" => "审核通过",
"target" => "/user/form", // get 方式拉取表单配置, post 方式保存数据
"method" => "POST", // 请求api的方式, 默认POST
]
```
#### 列表型按钮
点击按钮后将以弹窗的形式渲染指定列表
```php
[
'type' => 'table', // 调用 src/components/Scaffold/tablist.vue 渲染
'target' => '', // target 留空
'props' => [
'listApi' => '/merchantlog/list?merchant_id={id}', // 列表数据拉取接口
'infoApi' => '/merchantlog/info', // 列表 配置拉取接口
'options' => [ // 表单的配置项
'showFilter' => false,
'createAble' => false
]
],
'text' => '招商记录',
]
```
抽屉型按钮
点击按钮后将打开抽屉, 抽屉内部指定动态调用指定组件
```php
[
'type' => 'drawer',
'target' => '', // target 留空
'text' => '查看日志',
'props' => [
'component' => 'SocketList', // 需动态调用的组件 src/components/Common 下
'componentProps' => [ // 组件的 props
'url' => env('OMS_WEBSOCKET_URL') . '/cronlog?name={name}'
],
// drawer** 为抽屉属性的定义
// 详见 https://element.eleme.io/#/zh-CN/component/drawer
'drawerWithHeader' => false,
'drawerSize' => '80%',
'drawerTitle' => '{title}日志',
'drawerDirection' => 'ttb'
]
]
```
### SuperButtonGroup 下拉按钮
上面 SuperButton 的结构改为数组形式即可

BIN
docs/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

1
docs/frontend/chart.md Normal file
View File

@@ -0,0 +1 @@
图表

1
docs/frontend/form.md Normal file
View File

@@ -0,0 +1 @@
表单

1
docs/frontend/list.md Normal file
View File

@@ -0,0 +1 @@
图表

37
docs/guide/desc.md Normal file
View File

@@ -0,0 +1,37 @@
`hyperf-admin`是前后端分离的后台管理系统, 前端基于`vue``vue-admin-template`, 针对后台业务`列表`, `表单`等场景封装了大量业务组件, 后端基于`hyperf`实现, 整体思路是后端定义页面渲染规则, 前端页面渲染时首先拉取配置, 然后组件根据具体配置完成页面渲染, 方便开发者仅做少量的配置工作就能完成常见的`CRUD`工作, 同时支持自定义组件和自定义页面, 以开发更为复杂的页面.
### 架构
![hyperf-admin架构](https://cdn.jsdelivr.net/gh/daodao97/FigureBed@master/uPic/sJaJti.png)
前端为`vue multiple page`多页模式, 可以按模块打包, 默认包含两个模块`default` 默认模块, `system`系统管理模块, 绝大部分业务组件在`src/components`目录, 前端文档详见 [这里](/frontend/)
后端为`composer包`模式, 目前包含组件
- 基础组件
- `composer require hyperf-admin/base-utils` hyperf-admin的基础组件包, 脚手架主要功能封装
- `composer require hyperf-admin/validation` 参数验证包, 对规则和参数提示做了较多优化
- `composer require hyperf-admin/alert-manager` 企微/钉钉机器人报警包
- `composer require hyperf-admin/rule-engine` 规则引擎
- `composer require hyperf-admin/event-bus` mq/nsq/kafka消息派发器
- `composer require hyperf-admin/process-manager` 进程管理组件
- 业务组件 (业务组件为包含特定业务功能的包)
- `composer require hyperf-admin/admin` 系统管理业务包
- `composer require hyperf-admin/dev-tools` 开发者工具包, 主要是代码生成, 辅助开发
- `composer require hyperf-admin/cron-center` 定时任务管理, 后台化管理任务
- `composer require hyperf-admin/data-focus` 数据面板模块, 帮你快速制作数据大盘
后端的详细文档见[这里](/backend/)
### 依赖 & 参考
- 前端
- [Vue](https://github.com/vuejs/vue)
- [ElementUI](https://github.com/ElemeFE/element)
- [FormCreate](http://www.form-create.com/v2/guide)
- [vue-admin-template](https://github.com/PanJiaChen/vue-admin-template)
- [Vue 渲染函数 & JSX](https://cn.vuejs.org/v2/guide/render-function.html)
- 后端
- [Hyperf](http://hyperf.wiki/)
- [Swoole](http://wiki.swoole.com)

119
docs/guide/dev_example.md Normal file
View File

@@ -0,0 +1,119 @@
我们通过一个具体案例, 来看看如何应用`hyperf-admin`快速实现
### 1.需求描述
实现一个某校年级内各班级学生的各科成绩管理后台, 要求如下
1. 列表显示学生 年级,班级,学习,学科,成绩,时间,性别,年龄
2. 可以按成绩 倒序/正序排列
3. 可以批量导入/导出学生成绩
4. 可以通过 年级,学生名称,班级 等条件筛选
5. 最好列表可以分页签直接显示各科成绩
6. 没有原型图
### 2. 数据库定义
这里不做太复杂的设计, 仅用一张表来完成此需求
```sql
CREATE TABLE `student_score` (
`id` int(12) unsigned NOT NULL AUTO_INCREMENT,
`grade` tinyint(4) unsigned NOT NULL COMMENT '年级',
`class` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '班级',
`subject` tinyint(4) unsigned NOT NULL COMMENT '学科',
`score` int(12) unsigned NOT NULL DEFAULT '0' COMMENT '分数',
`name` varchar(10) NOT NULL DEFAULT '' COMMENT '学生名称',
`sex` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '性别, 0女生, 1难受',
`create_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
接下来在`MySql`中创建表
### 3.功能开发
1. `hyperf`中添加db信息
```php
// config/autoload/databases.php
'local' => db_complete([
'host' => '127.0.0.1',
'database' => 'test',
'username' => 'root',
'password' => 'root'
])
```
2. 通过`DevTools`开发者工具创建 `student_score` 相关的 `Model`, `Controller`
![YPdEli](https://cdn.jsdelivr.net/gh/daodao97/FigureBed@master/uPic/YPdEli.png)
选择好相应的表后, 点击提交, 此时工具已经帮我们创建好相应的`app/Controller/StudentScoreController.php`和`app/Model/Test/StudentScore.php`
3. 添加目录和菜单
![cs0SYX](https://cdn.jsdelivr.net/gh/daodao97/FigureBed@master/uPic/cs0SYX.png)
注册路由
```php
// config/routes.php
register_route('/student_score', StudentScoreController::class);
```
此时我们也已经完成了基础的`CRUD`开发
![MEoM4p](https://cdn.jsdelivr.net/gh/daodao97/FigureBed@master/uPic/MEoM4p.png)
哦对了, 还有各种筛选条件呢? 也很简单, 在 `scaffoldOptions` 中增加 `filter`配置即可
```php
public function scaffoldOptions()
{
return [
'filter' => [
'grade', 'class', 'subject', 'name%',
'score|分数' => [
'type' => 'input-range',
'select_type' => 'between'
]
],
];
}
```
![u68v1D](https://cdn.jsdelivr.net/gh/daodao97/FigureBed@master/uPic/u68v1D.png)
还有, 大家别忘了, 需求中还要去可以按页签显示, 改怎么办呢, 这个ui可有点复杂啊, 不过在`hyperf-admin`里也同样简单
`scaffoldOptions` 中增加 `table.tabs`配置即可
```php
public function scaffoldOptions()
{
return [
'table' => [
'tabs' => [
[
'label' => '语文',
'value' => 1,
'icon' => 'el-icon-s-grid',
],
[
'label' => '数学',
'value' => 2,
'icon' => 'el-icon-s-grid',
],
]
],
];
}
```
![Ax0WWD](https://cdn.jsdelivr.net/gh/daodao97/FigureBed@master/uPic/Ax0WWD.png)
至此我们已经完成了绝大部分的功能开发, 如果使用熟练, 我们应该能在十分钟内完成整个功能的前后端开发, 而且还支持复杂的前端效果.
?> 当然`hyperf-admin`还支持更多复杂的功能, 快快用你明亮的眼睛去发现他吧.

39
docs/guide/install.md Normal file
View File

@@ -0,0 +1,39 @@
?> `hyperf-admin`目前尚未开源, 敬请期待.
## 前端
```shell
# 环境依赖
# 1. node ^v11.2.0 https://nodejs.org/zh-cn/download/
# 2. npm ^6.4.1
git clone https://github.com/hyperf-admin/hyperf-admin-front.git
cd hyperf-admin-front
npm i
npm run dev
```
!> 请根据实际情况修改`vue.config.js`中的代理 `proxy.target`地址
```shell
# 打包
npm run build:prod
npm run build:test
```
## 后端
```shell
# 环境依赖 php ^7.1 composer swoole
# 下载demo项目
git clone https://github.com/hyperf-admin/hyperf-admin-skeleton.git
cd hyperf-admin-skeleton
composer i
# 启动
composer watch
```
## nginx配置
```nginx
# conf
```

63
docs/index.html Normal file
View File

@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>hyperf-admin</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="Description">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
<link rel="shortcut icon" type="image/icon" href="/favicon.ico">
</head>
<body>
<div id="app"></div>
<script src="//unpkg.com/docsify/lib/docsify.min.js"></script>
<script>
window.$docsify = {
name: 'hyperf-admin',
//logo: '/logo.png',
repo: 'https://github.com/hyperf-admin',
//coverpage: true, // 开启封面
loadNavbar: true,
loadSidebar: true,
maxLevel: 4,
subMaxLevel: 3,
autoHeader: true,
search: {
maxAge: 86400000, // 过期时间,单位毫秒,默认一天
paths: 'auto', // or 'auto'
placeholder: 'Type to search'
},
plugins: [
function (hook, vm) {
hook.beforeEach(function (html) {
if (/githubusercontent\.com/.test(vm.route.file)) {
url = vm.route.file
.replace('raw.githubusercontent.com', 'github.com')
.replace(/\/master/, '/blob/master')
} else {
url = 'https://github.com/hyperf-admin/hyperf-admin.github.io/edit/master/' + vm.route.file
}
var editHtml = '[:memo: 编辑文档!](' + url + ')&nbsp;&nbsp;'
return html
+ '\n\n----\n\n'
+ editHtml
+ '<a href="https://docsify.js.org" target="_blank" style="color: inherit; font-weight: normal; text-decoration: none;">Powered by docsify</a>'
})
}
]
}
if (typeof navigator.serviceWorker !== 'undefined') {
navigator.serviceWorker.register('https://raw.githubusercontent.com/hyperf-admin/hyperf-admin.github.io/master/ws.js')
}
</script>
<script src="//unpkg.com/prismjs/components/prism-bash.js"></script>
<script src="//unpkg.com/prismjs/components/prism-php.js"></script>
<script src="//unpkg.com/prismjs/components/prism-json.js"></script>
<script src="//unpkg.com/prismjs/components/prism-sql.js"></script>
<script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script>
<script src="//unpkg.com/docsify-copy-code"></script>
<script src="//unpkg.com/docsify/lib/plugins/zoom-image.js"></script>
</body>
</html>

BIN
docs/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

83
docs/ws.js Normal file
View File

@@ -0,0 +1,83 @@
/* ===========================================================
* docsify sw.js
* ===========================================================
* Copyright 2016 @huxpro
* Licensed under Apache 2.0
* Register service worker.
* ========================================================== */
const RUNTIME = 'docsify'
const HOSTNAME_WHITELIST = [
self.location.hostname,
'fonts.gstatic.com',
'fonts.googleapis.com',
'cdn.jsdelivr.net'
]
// The Util Function to hack URLs of intercepted requests
const getFixedUrl = (req) => {
var now = Date.now()
var url = new URL(req.url)
// 1. fixed http URL
// Just keep syncing with location.protocol
// fetch(httpURL) belongs to active mixed content.
// And fetch(httpRequest) is not supported yet.
url.protocol = self.location.protocol
// 2. add query for caching-busting.
// Github Pages served with Cache-Control: max-age=600
// max-age on mutable content is error-prone, with SW life of bugs can even extend.
// Until cache mode of Fetch API landed, we have to workaround cache-busting with query string.
// Cache-Control-Bug: https://bugs.chromium.org/p/chromium/issues/detail?id=453190
if (url.hostname === self.location.hostname) {
url.search += (url.search ? '&' : '?') + 'cache-bust=' + now
}
return url.href
}
/**
* @Lifecycle Activate
* New one activated when old isnt being used.
*
* waitUntil(): activating ====> activated
*/
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim())
})
/**
* @Functional Fetch
* All network requests are being intercepted here.
*
* void respondWith(Promise<Response> r)
*/
self.addEventListener('fetch', event => {
// Skip some of cross-origin requests, like those for Google Analytics.
if (HOSTNAME_WHITELIST.indexOf(new URL(event.request.url).hostname) > -1) {
// Stale-while-revalidate
// similar to HTTP's stale-while-revalidate: https://www.mnot.net/blog/2007/12/12/stale
// Upgrade from Jake's to Surma's: https://gist.github.com/surma/eb441223daaedf880801ad80006389f1
const cached = caches.match(event.request)
const fixedUrl = getFixedUrl(event.request)
const fetched = fetch(fixedUrl, { cache: 'no-store' })
const fetchedCopy = fetched.then(resp => resp.clone())
// Call respondWith() with whatever we get first.
// If the fetch fails (e.g disconnected), wait for the cache.
// If theres nothing in cache, wait for the fetch.
// If neither yields a response, return offline pages.
event.respondWith(
Promise.race([fetched.catch(_ => cached), cached])
.then(resp => resp || fetched)
.catch(_ => { /* eat any errors */ })
)
// Update the cache with the version we fetched (only for ok status)
event.waitUntil(
Promise.all([fetchedCopy, caches.open(RUNTIME)])
.then(([response, cache]) => response.ok && cache.put(event.request, response))
.catch(_ => { /* eat any errors */ })
)
}
})

3
src/admin/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.idea
composer.lock
vendor

31
src/admin/composer.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "rock-admin/admin",
"type": "project",
"license": "MIT",
"authors": [
{
"name": "daodao97",
"email": "daodao97@foxmail.com"
}
],
"require": {
"rock-admin/base-utils": "~0.0.1",
"rock-admin/validation": "~0.0.1"
},
"autoload": {
"psr-4": {
"Rock\\Admin\\": "./src"
},
"files": [
"src/funcs/common.php"
]
},
"config": {
"sort-packages": false
},
"extra": {
"hyperf": {
"config": "Rock\\Admin\\ConfigProvider"
}
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace HyperfAdmin\Admin;
use HyperfAdmin\Admin\Install\InstallCommand;
use HyperfAdmin\Admin\Install\UpdateCommand;
use HyperfAdmin\Admin\Middleware\AuthMiddleware;
use HyperfAdmin\Admin\Middleware\PermissionMiddleware;
use HyperfAdmin\BaseUtils\Middleware\CorsMiddleware;
use HyperfAdmin\BaseUtils\Middleware\HttpLogMiddleware;
class ConfigProvider
{
public function __invoke(): array
{
$config = require_once __DIR__ . '/config/config.php';
return array_overlay($config, [
'commands' => [
InstallCommand::class,
UpdateCommand::class,
],
'dependencies' => [],
'listeners' => [],
'publish' => [],
'middlewares' => [
'http' => [
CorsMiddleware::class,
AuthMiddleware::class,
PermissionMiddleware::class,
HttpLogMiddleware::class
]
]
]);
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace HyperfAdmin\Admin\Controller;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Psr\Container\ContainerInterface;
use HyperfAdmin\Admin\Model\User;
use Hyperf\Di\Annotation\Inject;
use HyperfAdmin\Admin\Service\AuthService;
use HyperfAdmin\Admin\Service\PermissionService;
use HyperfAdmin\BaseUtils\Scaffold\Controller\AbstractController;
abstract class AdminAbstractController extends AbstractController
{
/*
* @Inject()
* @var AuthService
*/
protected $auth_service;
/*
* @Inject()
* @var PermissionService
*/
protected $permission_service;
public function __construct(ContainerInterface $container, RequestInterface $request, ResponseInterface $response)
{
$this->auth_service = make(AuthService::class);
$this->permission_service = make(PermissionService::class);
parent::__construct($container, $request, $response);
}
public function getRecordHistory()
{
$history_versions = $this->getEntity()->lastVersion($this->previous_version_number);
$history_versions = array_node_append($history_versions, 'user_id', 'username', function ($uids) {
$ret = User::query()->select(['id', 'username'])->whereIn('id', $uids)->get();
if (!$ret) {
return [];
}
$ret = $ret->toArray();
array_change_v2k($ret, 'id');
foreach ($ret as &$item) {
$item = $item['username'];
unset($item);
}
return $ret;
});
return $history_versions;
}
/**
* 新版本检查
*
* @param int $id
* @param int $last_ver_id
*
* @return array
*/
public function newVersion(int $id, int $last_ver_id)
{
$last = $this->getEntity()->lastVersion();
if (!$last || $last->id == $last_ver_id) {
return $this->success(['has_new_version' => false]);
}
if ($last->user_id == auth()->get('id')) {
return $this->success(['has_new_version' => false]);
}
$user = User::query()->find($last->user_id);
return $this->success([
'has_new_version' => true,
'message' => sprintf("%s在%s保存了新的数据, 请刷新页面获取最新数据", $user->username, $last->create_at),
]);
}
public function userId()
{
return auth()->get('id');
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace HyperfAdmin\Admin\Controller;
use HyperfAdmin\Admin\Model\CommonConfig;
use HyperfAdmin\Admin\Service\CommonConfig as CommonConfigService;
use HyperfAdmin\BaseUtils\Constants\ErrorCode;
class CommonConfigController extends AdminAbstractController
{
protected $model_class = CommonConfig::class;
public function scaffoldOptions()
{
return [
'filter' => ['name%'],
'form' => [
'id|#' => '',
'namespace|命名空间' => [
'rule' => 'required',
'type' => 'select',
'options' => function ($field, $data) {
$namespaces = $this->getModel()->where(['name' => 'namespace'])->value('value');
$options = [];
foreach ($namespaces as $value => $label) {
$options[] = [
'value' => $value,
'label' => $label,
];
}
return $options;
},
'default' => 'common',
],
'name|名称' => [
'rule' => 'required|unique:hyperf_admin.common_config,name',
'readonly' => true,
],
'title|可读名称' => [
'rule' => 'required',
],
'rules|规则' => [
'type' => 'json',
'rule' => 'json',
'depend' => [
'field' => 'is_need_form',
'value' => CommonConfig::NEED_FORM_YES,
],
],
'remark|备注' => 'max:100',
'is_need_form|是否使用表单' => [
'rule' => 'integer',
'type' => 'radio',
'options' => CommonConfig::$need_form,
'default' => CommonConfig::NEED_FORM_YES,
],
'value|配置值' => [
'type' => 'json',
'rule' => 'json',
'depend' => [
'field' => 'is_need_form',
'value' => CommonConfig::NEED_FORM_NO,
],
],
],
'table' => [
'columns' => [
'id',
[
'field' => 'namespace',
'enum' => [
'default' => '通用',
],
],
'name',
'title',
[
'field' => 'is_need_form',
'hidden' => true,
],
],
'rowActions' => [
['action' => '/cconf/{id}', 'text' => '编辑'],
[
'action' => '/cconf/cconf_{name}',
'text' => '表单',
'when' => [
['is_need_form', '=', CommonConfig::NEED_FORM_YES],
],
],
],
],
];
}
public function detail($key)
{
$conf = CommonConfigService::getConfigByName($key);
if (!$conf || !$conf['rules']) {
return $this->fail(ErrorCode::CODE_ERR_PARAM, '通用配置未找到 ' . $key);
}
$rules = $this->formOptionsConvert($conf['rules'], true, false, false, $conf['value']);
$compute_map = $this->formComputeConfig($rules);
return $this->success([
'form' => $rules,
'compute_map' => (object)$compute_map,
]);
}
public function saveDetail($key)
{
$conf = CommonConfig::query()->where(['name' => $key])->select([
'id',
'rules',
])->first();
if (!$conf) {
return $this->fail(ErrorCode::CODE_ERR_PARAM, '参数错误');
}
$saved = $conf->fill(['value' => $this->request->all()])->save();
return $saved ? $this->success() : $this->fail(ErrorCode::CODE_ERR_SYSTEM);
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace HyperfAdmin\Admin\Controller;
use Carbon\Carbon;
use HyperfAdmin\Admin\Model\OperatorLog;
class LogController extends AdminAbstractController
{
protected $model_class = OperatorLog::class;
public function scaffoldOptions()
{
return [
'exportAble' => true,
'createAble' => false,
'order_by' => 'id desc',
'filter' => [
'page_url',
'page_name',
'action',
'operator_id',
'relation_ids',
'client_ip',
'detail_json',
'create_at',
],
'formUI' => [
'form' => [
'size' => 'mini',
],
],
'form' => [
'id|#' => 'required|int',
'page_url|页面URL' => 'required|string',
'page_name|页面名称' => 'required|string',
'action|动作' => 'required|string',
'relation_ids|处理ID' => 'required|string',
'client_ip|客户端IP' => 'required|string',
'operator_id|操作人' => [
'rule' => 'required|int',
'type' => 'select',
'props' => [
'allowCreate' => true,
'filterable' => true,
'remote' => true,
'selectApi' => '/search/user',
],
],
'detail_json|其他内容' => 'string',
'create_at|记录时间' => [
'type' => 'date_range',
],
],
'table' => [
'columns' => [
[
'field' => 'operator_id',
'hidden' => true,
],
['field' => 'id', 'title' => 'ID', 'hidden' => true],
[
'field' => 'create_at',
'title' => '记录时间',
'width' => '150px',
],
[
'field' => 'nickname',
'title' => '操作人',
],
[
'field' => 'page_url',
'title' => '页面URL',
'width' => '150px',
],
[
'field' => 'page_name',
'title' => '页面名称',
'width' => '220px',
],
'action',
[
'field' => 'relation_ids',
'title' => '处理ID',
'width' => '150px',
],
[
'field' => 'detail_json',
'hidden' => true,
],
[
'field' => 'description',
'title' => '描述',
'virtual_field' => true,
'width' => '480px',
'render' => function ($field, $row) {
if (!is_array($row['detail_json'])) {
$row['detail_json'] = json_decode($row['detail_json'], true);
}
return $row['detail_json']['description'] ?? '';
},
],
[
'field' => 'client_ip',
'title' => '客户端IP',
'width' => '100px',
],
[
'field' => 'remark',
'title' => '备注',
'virtual_field' => true,
'width' => '200px',
'render' => function ($field, $row) {
if (!is_array($row['detail_json'])) {
$row['detail_json'] = json_decode($row['detail_json'], true);
}
return $row['detail_json']['remark'] ?? '';
},
],
],
],
];
}
public function beforeListQuery(&$filters)
{
foreach ($filters as $field => $filter) {
if (in_array($field, [
'page_url',
'page_name',
'action',
'detail_json',
'relation_ids',
'client_ip',
])) {
$filters[$field] = ['like' => "%$filter%"];
}
}
if (!empty($filters['create_at']['between'])) {
$filters['create_at']['between'][0] = Carbon::parse($filters['create_at']['between'][0])->toDateTimeString();
$filters['create_at']['between'][1] = Carbon::parse($filters['create_at']['between'][1] . ' +1 day last second')
->toDateTimeString();
}
unset($filters);
}
}

View File

@@ -0,0 +1,572 @@
<?php
namespace HyperfAdmin\Admin\Controller;
use Hyperf\HttpServer\Router\DispatcherFactory;
use Hyperf\Utils\Str;
use HyperfAdmin\Admin\Model\CommonConfig;
use HyperfAdmin\Admin\Model\FrontRoutes;
use HyperfAdmin\Admin\Model\RoleMenu;
use HyperfAdmin\Admin\Service\Menu;
use HyperfAdmin\BaseUtils\Constants\ErrorCode;
class MenuController extends AdminAbstractController
{
protected $model_class = FrontRoutes::class;
public function scaffoldOptions()
{
return [
'exportAble' => false,
'createAble' => false,
'where' => [
'pid' => 0,
],
'formUI' => [
'form' => [
'size' => 'mini',
],
],
'form' => [
'id|#' => 'int',
'module|#' => [
'type' => 'hidden',
'render' => function ($field, &$data) {
$data['value'] = request()->input('module', $data['value'] ?? 'system');
},
],
'type|菜单类型' => [
'type' => 'radio',
'default' => 1,
'options' => ['目录', '菜单', '权限'],
'compute' => [
[
'when' => ['in', [0, 2]],
'set' => [
'is_scaffold' => [
'type' => 'hidden',
],
'other_menu' => [
'type' => 'hidden',
],
'path' => [
'type' => 'hidden',
],
'page_type' => [
'type' => 'hidden',
],
],
],
[
'when' => ['=', 1],
'set' => [
'path' => [
'rule' => 'required',
],
'label' => [
'title' => '菜单标题',
'col' => [
'span' => 12,
],
],
],
],
[
'when' => ['=', 2],
'set' => [
'icon' => [
'type' => 'hidden',
],
'is_menu' => [
'type' => 'hidden',
],
'label' => [
'title' => '权限名称',
'col' => [
'span' => 24,
],
],
],
],
[
'when' => ['=', 0],
'set' => [
'permission' => [
'type' => 'hidden',
],
'label' => [
'title' => '菜单标题',
'col' => [
'span' => 12,
],
],
],
],
],
],
'icon|菜单图标' => [
'type' => 'icon-select',
'options' => [
'example' => 'example',
],
'col' => [
'span' => 12,
],
],
'sort|菜单排序' => [
'type' => 'number',
'default' => 99,
'col' => [
'span' => 12,
],
],
'label|菜单标题' => [
'rule' => 'required|string|max:10',
'col' => [
'span' => 12,
],
],
'path|路由地址' => [
'rule' => 'string|max:100',
'default' => '',
'col' => [
'span' => 12,
],
],
'is_menu|菜单可见' => [
'type' => 'radio',
'options' => [
0 => '否',
1 => '是',
],
'default' => 1,
'col' => [
'span' => 12,
],
],
'is_scaffold|渲染方式' => [
'type' => 'radio',
'options' => [
1 => '脚手架',
0 => '自定义',
],
'default' => 1,
'col' => [
'span' => 12,
],
'compute' => [
'when' => ['=', 0],
'set' => [
'view' => [
'rule' => 'required',
],
],
],
],
'view|组件路径' => [
'rule' => 'string|max:50',
'default' => '',
'depend' => [
'field' => 'is_scaffold',
'value' => 0,
],
],
'scaffold_action|预置权限' => [
'type' => 'checkbox',
'virtual_field' => true,
'options' => function ($field, $data) {
$scaffold_permissions = config('scaffold_permissions');
$options = [];
foreach ($scaffold_permissions as $key => $permission) {
$options[] = [
'value' => $key,
'label' => $permission['label'],
];
}
return $options;
},
'info' => '新增和编辑会创建/form或/:id的前端路由',
'depend' => [
'field' => 'is_scaffold',
'value' => 1,
],
],
'permission|权限标识' => [
'type' => 'select',
'default' => [],
'options' => function ($field, $data) {
return $this->permission_service->getSystemRouteOptions();
},
'props' => [
'multiple' => true,
],
],
'pid|上级类目' => [
'rule' => 'array',
'type' => 'cascader',
'default' => [],
'options' => function ($field, $data) {
$module = request()->input('module', $data['module'] ?? 'system');
return (new Menu())->tree([
'module' => $module,
'type' => [0, 1],
]);
},
'props' => [
'style' => 'width: 100%;',
'clearable' => true,
'props' => [
'checkStrictly' => true,
],
],
],
'roles|分配角色' => [
'rule' => 'array',
'type' => 'cascader',
'virtual_field' => true,
'props' => [
'style' => 'width: 100%;',
'props' => [
'multiple' => true,
'leaf' => 'leaf',
'emitPath' => false,
'checkStrictly' => true,
],
],
'render' => function ($field, &$data) {
$id = (int)$this->request->route('id', 0);
$data['value'] = $this->permission_service->getMenuRoleIds($id);
$data['options'] = $this->permission_service->getRoleTree();
},
],
],
'table' => [
'is_tree' => true,
'tabs' => function() {
$conf = \HyperfAdmin\Admin\Service\CommonConfig::getValByName('website_config');
$system_module = $conf['system_module'] ?? [];
return array_map(function ($item) {
return [
'label' => $item['label'],
'value' => $item['name'],
'icon' => $item['icon']
];
}, $system_module);
},
'rowActions' => [
[
'text' => '编辑',
'type' => 'form',
'target' => '/menu/{id}',
'formUi' => [
'form' => [
'labelWidth' => '80px',
'size' => 'mini',
],
],
'props' => [
'type' => 'primary',
],
],
[
'text' => '加子菜单',
'type' => 'form',
'target' => '/menu/form?pid[]={id}&module={tab_id}',
'formUi' => [
'form' => [
'labelWidth' => '80px',
'size' => 'mini',
],
],
'props' => [
'type' => 'success',
],
],
[
'text' => '删除',
'type' => 'api',
'target' => '/menu/delete',
'props' => [
'type' => 'danger',
],
],
],
'topActions' => [
[
'text' => '清除权限缓存',
'type' => 'api',
'target' => '/menu/permission/clear',
'props' => [
'icon' => 'el-icon-delete',
'type' => 'warning',
],
],
[
'text' => '公共资源',
'type' => 'jump',
'target' => '/cconf/cconf_permissions',
'props' => [
'icon' => 'el-icon-setting',
'type' => 'primary',
],
],
[
'text' => '新建',
'type' => 'form',
'target' => '/menu/form?module={tab_id}',
'formUi' => [
'form' => [
'labelWidth' => '80px',
'size' => 'mini',
],
],
'props' => [
'icon' => 'el-icon-plus',
'type' => 'success',
],
],
],
'columns' => [
['field' => 'id', 'hidden' => true],
['field' => 'pid', 'hidden' => true],
[
'field' => 'label',
'width' => '250px',
],
[
'field' => 'is_menu',
'enum' => [
0 => 'info',
1 => 'success',
],
'width' => '80px;',
],
[
'field' => 'icon',
'type' => 'icon',
'width' => '80px;',
],
'path',
'permission',
[
'field' => 'sort',
'edit' => true,
'width' => '170px;',
],
],
],
'order_by' => 'sort desc',
];
}
protected function beforeFormResponse($id, &$record)
{
if (in_array($record['type'], [
1,
2,
])
&& !empty($record['permission'])) {
$record['permission'] = array_map(function ($item) use ($record) {
if (!Str::contains($item, '::')) {
$http_method = FrontRoutes::$http_methods[$record['http_method']];
return "{$http_method}::{$item}";
}
return $item;
}, array_filter(explode(',', $record['permission'])));
}
$scaffold_action = json_decode($record['scaffold_action'], true);
$record['scaffold_action'] = $scaffold_action ? array_keys($scaffold_action) : [];
$record['pid'] = (new Menu())->getPathNodeIds($id);
}
protected function beforeSave($id, &$data)
{
if ($data['type'] == 1) {
if ($data['path'] == '#' || $data['path'] == '') {
$this->exception('菜单路由地址不能为空或"#"', ErrorCode::CODE_ERR_PARAM);
}
$paths = array_filter(explode('/', $data['path']));
if (count($paths) > 5) {
$this->exception('路由地址层级过深>5请设置精简一些', ErrorCode::CODE_ERR_PARAM);
}
} else {
$data['path'] = '#';
}
$data['is_menu'] = $data['type'] == 2 ? 0 : $data['is_menu'];
$data['permission'] = implode(',', $data['permission'] ?? []);
$pid = array_pop($data['pid']);
if ($pid == $id) {
$pid = array_pop($data['pid']);
}
$data['pid'] = (int)$pid;
if ($data['type'] > 1) {
$parent_info = $this->getModel()->find($data['pid']);
if (!$parent_info || $parent_info['type'] != 1) {
$this->exception('菜单类型为权限时请选择一个上级类目', ErrorCode::CODE_ERR_PARAM);
}
}
$data['status'] = YES;
}
protected function afterSave($pk_val, $data, $entity)
{
// 更新预置的脚手架权限
$scaffold_action = json_decode($entity->scaffold_action, true);
$action_keys = $scaffold_action ? array_keys($scaffold_action) : [];
$need_del_ids = $scaffold_action ? array_values($scaffold_action) : [];
$router_ids = [];
if (!empty($data['scaffold_action'])) {
$need_del_ids = collect($scaffold_action)->except($data['scaffold_action'])->values()->toArray();
$scaffold_action = collect($scaffold_action)->only($data['scaffold_action'])->toArray();
$paths = array_filter(explode('/', $data['path']));
array_pop($paths);
$prefix = implode('/', $paths);
foreach ($data['scaffold_action'] as $k => $action) {
if (in_array($action, $action_keys)) {
continue;
}
$action_conf = config("scaffold_permissions.{$action}");
$menu = [
'pid' => $pk_val,
'label' => $action_conf['label'],
'path' => !empty($action_conf['path']) ? "/{$prefix}" . $action_conf['path'] : '',
'permission' => str_replace('/*/', "/{$prefix}/", $action_conf['permission']),
'is_scaffold' => $action_conf['type'] == 1 ? 1 : 0,
'module' => $data['module'],
'type' => $action_conf['type'],
'sort' => 99 - $k,
'status' => 1,
];
$model = make(FrontRoutes::class);
$model->fill($menu)->save();
$scaffold_action[$action] = $model->id;
$router_ids[] = $model->id;
}
} else {
$scaffold_action = '';
}
$entity->scaffold_action = json_encode($scaffold_action);
// todo entity
//$entity->save();
// 删除路由
if (!empty($need_del_ids)) {
$this->getModel()->destroy($need_del_ids);
make(RoleMenu::class)->where2query(['router_id' => $need_del_ids])->delete();
}
// 分配角色
if (!empty($data['roles'])) {
$role_menus = [];
$router_ids[] = $pk_val;
foreach ($data['roles'] as $role_id) {
foreach ($router_ids as $router_id) {
$role_menus[] = [
'router_id' => $router_id,
'role_id' => $role_id,
];
}
}
make(RoleMenu::class)->insertOnDuplicateKey($role_menus);
} else {
// 删除当前菜单已分配的角色
make(RoleMenu::class)->where2query(['router_id' => $pk_val])->delete();
}
// 清除缓存
$this->permission_service->getPermissionCacheKey(0, true);
}
protected function afterDelete($pk_val, $deleted)
{
if ($deleted) {
// 删除子菜单
$sub_ids = $this->getModel()->where2query(['pid' => $pk_val])->select(['id'])->get()->toArray();
if ($sub_ids) {
$sub_ids = array_column($sub_ids, 'id');
$this->afterDelete($sub_ids, $deleted);
}
if (is_array($pk_val)) {
make(FrontRoutes::class)->where2query(['id' => $pk_val])->delete();
}
make(RoleMenu::class)->where2query(['router_id' => $pk_val])->delete();
}
}
public function clearPermissionCache()
{
$this->permission_service->getPermissionCacheKey(0, true);
return $this->success();
}
public function getOpenApis()
{
$field = $this->request->input('field', 'open_api');
$conf = CommonConfig::query()->where([
'namespace' => 'system',
'name' => 'permissions',
])->value('value')[$field] ?? [];
$router = container(DispatcherFactory::class)->getRouter('http');
$data = $router->getData();
$options = [];
foreach ($data as $routes_data) {
foreach ($routes_data as $http_method => $routes) {
$route_list = [];
if (isset($routes[0]['routeMap'])) {
foreach ($routes as $map) {
array_push($route_list, ...$map['routeMap']);
}
} else {
$route_list = $routes;
}
foreach ($route_list as $route => $v) {
$route = is_string($route) ? rtrim($route) : rtrim($v[0]->route);
$route_key = "$http_method::{$route}";
if (in_array($route_key, $conf)) {
continue;
}
// 过滤掉脚手架页面配置方法
$callback = is_array($v) ? ($v[0]->callback) : $v->callback;
if (!is_array($callback)) {
continue;
}
[$controller, $action] = $callback;
if (empty($action) || in_array($action, $this->permission_service->scaffold_actions)) {
continue;
}
$options[] = [
'id' => $route_key,
'controller' => $controller,
'action' => $action,
'http_method' => $http_method,
];
}
}
}
$right_options = [];
foreach ($conf as $route) {
[$http_method, $uri] = explode("::", $route, 2);
$dispatcher = container(DispatcherFactory::class)->getDispatcher('http');
$route_info = $dispatcher->dispatch($http_method, $uri);
if (!empty($route_info[1]->callback[0])) {
$right_options[] = [
'id' => $route,
'controller' => $route_info[1]->callback[0],
'action' => $route_info[1]->callback[1],
'http_method' => $http_method,
];
}
}
return $this->success([
'left' => $options,
'right' => $right_options,
]);
}
public function beforeListQuery(&$where)
{
$where['module'] = $this->request->input('tab_id', 'default');
}
}

View File

@@ -0,0 +1,198 @@
<?php
namespace HyperfAdmin\Admin\Controller;
use HyperfAdmin\Admin\Model\Role;
use HyperfAdmin\Admin\Model\RoleMenu;
use HyperfAdmin\Admin\Model\UserRole;
class RoleController extends AdminAbstractController
{
protected $model_class = Role::class;
public function scaffoldOptions()
{
return [
'createAble' => true,
'deleteAble' => true,
'importAble' => true,
'filter' => ['name'],
'where' => [
'pid' => 0,
],
'form' => [
'id' => 'int',
'name|名称' => [
'rule' => 'required|max:20',
'type' => 'input',
'props' => [
'size' => 'small',
'maxlength' => 20,
],
],
'pid|上级角色' => [
'rule' => 'int',
'type' => 'select',
'info' => '没有上级角色则为一级角色',
'default' => 0,
'props' => [
'multipleLimit' => 1,
],
'options' => function ($field, &$data) {
$options = $this->permission_service->getAllRoleList(['pid' => 0], [
'id as value',
'name as label',
]);
array_unshift($options, ['value' => 0, 'label' => '无']);
return $options;
},
],
'sort|排序' => [
'rule' => 'int',
'type' => 'number',
'default' => 0,
],
'permissions|权限设置' => [
'rule' => 'Array',
'type' => 'el-cascader-panel',
'virtual_field' => true,
'props' => [
'style' => 'height:500px;',
'props' => [
'multiple' => true,
'leaf' => 'leaf',
'checkStrictly' => false,
],
],
'render' => function ($field, &$data) {
$id = (int)$this->request->route('id', 0);
[
$data['value'],
$data['props']['options'],
] = $this->permission_service->getPermissionOptions($id);
},
],
'user_ids|授权用户' => [
'type' => 'select',
'props' => [
'multiple' => true,
'selectApi' => '/user/act',
'remote' => true,
],
'virtual_field' => true,
'render' => function ($field, &$data) {
$id = (int)$this->request->route('id', 0);
$data['value'] = $this->permission_service->getRoleUserIds($id);
if (!empty($data['value'])) {
$data['options'] = select_options($data['props']['selectApi'], $data['value']);
}
},
],
],
'table' => [
'columns' => [
['field' => 'id', 'hidden' => true],
['field' => 'pid', 'hidden' => true],
'name',
[
'field' => 'sort',
'edit' => true,
],
],
'rowActions' => [
[
'action' => '/role/{id}',
'text' => '编辑',
],
[
'action' => 'api',
'api' => '/role/delete',
'text' => '删除',
'type' => 'danger',
],
],
],
'order_by' => 'pid asc, sort desc',
];
}
protected function beforeListResponse(&$list)
{
$ids = array_column($list, 'id');
$children = $this->getModel()->where2query(['pid' => $ids])->get()->toArray();
$list = array_merge($list, $children);
$list = generate_tree($list);
}
protected function beforeSave($pk_val, &$data)
{
$data['permissions'] = $data['permissions'] ? json_encode($data['permissions']) : '';
unset($data);
}
protected function afterSave($pk_val, $data)
{
$data['permissions'] = json_decode($data['permissions'], true);
if (empty($data['permissions'])) {
return true;
}
// 1、删除角色拥有的菜单
$id = $data['id'] ?? 0;
if ((int)$id == $pk_val) {
// 删除角色关联的菜单
RoleMenu::where('role_id', $pk_val)->delete();
// 删除角色关联的用户
$user_ids = $this->permission_service->getRoleUserIds($pk_val);
if (!empty($user_ids)) {
make(UserRole::class)->where2query([
'role_id' => $pk_val,
'user_id' => array_values(array_diff($user_ids, $data['user_ids'] ?? [])),
])->delete();
}
// 清除缓存
$this->permission_service->getPermissionCacheKey(0, true);
}
$menu_ids = [];
foreach ($data['permissions'] as $permissions) {
unset($permissions[0]);
$menu_ids = array_merge($menu_ids, $permissions);
}
// 2、保存角色新分配的菜单
$role_menus = [];
$menu_ids = array_unique($menu_ids);
foreach ($menu_ids as $menu_id) {
$role_menus[] = [
'role_id' => $pk_val,
'router_id' => (int)$menu_id,
'create_at' => date('Y-m-d H:i:s'),
'update_at' => date('Y-m-d H:i:s'),
];
}
if (!empty($role_menus)) {
RoleMenu::insertOnDuplicateKey($role_menus, [
'role_id',
'router_id',
]);
}
// 3、保存角色关联的用户
if (!empty($data['user_ids'])) {
$user_role_ids = [];
foreach ($data['user_ids'] as $user_id) {
$user_role_ids[] = [
'role_id' => $pk_val,
'user_id' => (int)$user_id,
'create_at' => date('Y-m-d H:i:s'),
'update_at' => date('Y-m-d H:i:s'),
];
}
if (!empty($user_role_ids)) {
UserRole::insertOnDuplicateKey($user_role_ids, [
'role_id',
'user_id',
]);
}
}
return true;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace HyperfAdmin\Admin\Controller;
use HyperfAdmin\Admin\Model\ExportTasks;
use HyperfAdmin\Admin\Service\CommonConfig;
use HyperfAdmin\Admin\Service\ExportService;
use HyperfAdmin\BaseUtils\Constants\ErrorCode;
class SystemController extends AdminAbstractController
{
public function state()
{
$swoole_server = swoole_server();
return $this->success([
'state' => $swoole_server->stats(),
]);
}
public function config()
{
$config = CommonConfig::getValue('system', 'website_config', [
'open_export' => false,
'navbar_notice' => '',
]);
return $this->success($config);
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace HyperfAdmin\Admin\Controller;
use OSS\Core\OssException;
use HyperfAdmin\BaseUtils\Constants\ErrorCode;
use HyperfAdmin\BaseUtils\Log;
use HyperfAdmin\BaseUtils\Scaffold\Controller\Controller;
class UploadController extends Controller
{
public function image()
{
$file = $this->request->file('file');
if(!$file->isValid()) {
return $this->fail(ErrorCode::CODE_ERR_PARAM);
}
$tmp_file = $file->toArray()['tmp_file'];
$md5_filename = md5_file($tmp_file);
$path = '1/' . date('Ym') . '/' . $md5_filename . '.' . $file->getExtension();
$bucket = $this->request->input('bucket', 'aliyuncs');
$private = $this->request->input('private', false);
try {
$uploaded = move_local_file_to_oss($tmp_file, $path, $private, $bucket);
if($uploaded === false) {
return $this->fail(ErrorCode::CODE_ERR_SERVER, '上传失败');
}
[$width, $height] = getimagesize($tmp_file);
$info = [
'path' => $uploaded['path'],
'url' => $uploaded['file_path'],
'key' => 'file',
'size' => $file->toArray()['size'],
'width' => $width,
'height' => $height,
];
return $this->success($info);
} catch (OssException $e) {
Log::get('upload')->error($e->getMessage());
return $this->fail(ErrorCode::CODE_ERR_SERVER, $e->getMessage());
}
}
public function privateFileUrl()
{
$oss_path = $this->request->input('key');
if(!$oss_path) {
return $this->fail(ErrorCode::CODE_ERR_PARAM);
}
$private_url = oss_private_url($oss_path);
if(!$private_url) {
return $this->fail(ErrorCode::CODE_ERR_SYSTEM);
}
return $this->response->redirect($private_url);
}
}

View File

@@ -0,0 +1,325 @@
<?php
namespace HyperfAdmin\Admin\Controller;
use Carbon\Carbon;
use HyperfAdmin\Admin\Model\ExportTasks;
use HyperfAdmin\Admin\Model\User;
use HyperfAdmin\Admin\Model\UserRole;
use HyperfAdmin\Admin\Service\ExportService;
use HyperfAdmin\Admin\Service\Menu;
use HyperfAdmin\BaseUtils\Constants\ErrorCode;
use HyperfAdmin\BaseUtils\JWT;
class UserController extends AdminAbstractController
{
protected $model_class = User::class;
public $open_resources = ['login'];
public function scaffoldOptions()
{
return [
'createAble' => true,
'deleteAble' => true,
'defaultList' => true,
'filter' => ['realname%', 'username%', 'create_at'],
'form' => [
'id' => 'int',
'username|登录账号' => [
'rule' => 'required',
'readonly' => true,
],
'avatar|头像' => [
'type' => 'image',
'rule' => 'string',
],
'realname|昵称' => '',
'mobile|手机' => '',
'email|邮箱' => 'email',
'sign|签名' => '',
'pwd|密码' => [
'virtual_field' => true,
'default' => '',
'props' => [
'size' => 'small',
'maxlength' => 20,
],
'info' => '若设置, 将会更新用户密码'
],
'status|状态' => [
'rule' => 'required',
'type' => 'radio',
'options' => User::$status,
'default' => 0,
],
'is_admin|类型' => [
'rule' => 'int',
'type' => 'radio',
'options' => [
NO => '普通管理员',
YES => '超级管理员',
],
'info' => '普通管理员需要分配角色才能访问角色对应的资源;超级管理员可以访问全部资源',
'default' => NO,
'render' => function ($field, &$rule) {
if ($this->auth_service->isSupperAdmin()) {
$rule['type'] = 'hidden';
}
},
],
'role_ids|角色' => [
'rule' => 'array',
'type' => 'el-cascader-panel',
'virtual_field' => true,
'props' => [
'props' => [
'multiple' => true,
'leaf' => 'leaf',
'emitPath' => false,
'checkStrictly' => true,
],
],
'render' => function ($field, &$data) {
$id = (int)$this->request->route('id', 0);
$data['value'] = $this->permission_service->getUserRoleIds($id);
$data['props']['options'] = $this->permission_service->getRoleTree();
},
],
'create_at|创建时间' => [
'form' => false,
'type' => 'date_range',
],
],
'hasOne' => [
'hyperf_admin.hyperf_admin.user_role:user_id,role_id',
],
'table' => [
'columns' => [
'id',
'realname',
'username',
[
'field' => 'mobile',
'render' => function ($field, $row) {
return data_desensitization($field, 3, 4);
},
],
['field' => 'avatar', 'render' => 'avatarRender'],
'email',
[
'field' => 'status',
'enum' => [
User::USER_DISABLE => 'info',
User::USER_ENABLE => 'success',
],
],
[
'field' => 'role_id',
'title' => '权限',
'virtual_field' => true,
],
],
'rowActions' => [
['action' => '/user/{id}', 'text' => '编辑',],
],
],
];
}
public function menu()
{
$module = $this->request->input('module', 'default');
$user = auth()->user();
$base_path = BASE_PATH . '/runtime/menu/';
$cache_key = $this->permission_service->getPermissionCacheKey($user['id']);
$cache_file = "{$base_path}{$cache_key}/{$module}.menu.{$user['id']}.cache";
$menu_list = file_exists($cache_file) ? require $cache_file : [];
if (empty($menu_list)) {
$where = [
'module' => $module,
'type' => [0, 1],
];
if (!$this->auth_service->isSupperAdmin()) {
$user_role_ids = $this->permission_service->getUserRoleIds($user['id']);
$where['id'] = $this->permission_service->getRoleMenuIds($user_role_ids);
}
$menu_list = (new Menu())->tree($where, [
'id',
'pid',
'label as menu_name',
'is_menu as hidden',
'is_scaffold as scaffold',
'path as url',
'view',
'icon',
], 'id');
if (!empty($menu_list)) {
if (file_exists($base_path)) {
rmdir_recursive($base_path);
}
mkdir($base_path . $cache_key, 0755, true);
file_put_contents($cache_file, '<?php return ' . var_export($menu_list, true) . ';');
}
}
return $this->success([
'menuList' => $menu_list,
]);
}
protected function beforeSave($pk_val, &$data)
{
if (!empty($data['pwd'])) {
$data['password'] = $this->passwordHash($data['pwd']);
}
}
public function afterSave($pk_val, $data)
{
$role_ids = array_filter(array_unique($data['role_ids']));
if ((int)$data['id'] == $pk_val) {
UserRole::where('user_id', $pk_val)->delete();
// 清除缓存
$this->permission_service->getPermissionCacheKey(0, true);
}
$user_roles = [];
foreach ($role_ids as $role_id) {
$user_roles[] = [
'user_id' => $pk_val,
'role_id' => (int)$role_id,
];
}
if (!empty($user_roles)) {
UserRole::insertOnDuplicateKey($user_roles, ['user_id', 'role_id']);
}
return true;
}
public function login()
{
$username = $this->request->input('username', '');
$password = $this->request->input('password', '');
if (!$username || !$password) {
return $this->fail(ErrorCode::CODE_ERR_PARAM);
}
$user = $this->getModel()->where('username', $username)->first();
if (!$user || $user['status'] !== YES) {
return $this->fail(ErrorCode::CODE_ERR_PARAM, '该用户不存在或已被禁用');
}
if ($user->password !== $this->passwordHash($password)) {
return $this->fail(ErrorCode::CODE_ERR_PARAM);
}
$data = [
'iat' => Carbon::now()->timestamp,
'exp' => Carbon::now()->addDay()->timestamp,
'user_info' => [
'id' => $user->id,
'name' => $user->username,
'alias_name' => $user->realname,
'email' => '',
'avatar' => $user->avatar,
'mobile' => $user->mobile,
],
];
$token = JWT::token($data);
return $this->success([
'id' => $user->id,
'mobile' => $user->mobile,
'name' => $user->realname,
'avatar' => $user->avatar,
'token' => $token,
]);
}
public function passwordHash($password)
{
return sha1(md5($password) . md5(config('password.salt')));
}
public function logout()
{
$user = $this->auth_service->logout();
return $this->success();
}
public function act()
{
$attr = ['select' => ['id as value', 'realname as label']];
$model = $this->getModel();
$options = $model->search($attr, [], 'realname');
return $this->success($options);
}
public function export()
{
$url = $this->request->input('url');
$task = new ExportTasks();
$task->name = $this->request->input('name');
$task->list_api = $url;
$task->filters = array_filter($this->request->input('filters'), function ($item) {
return $item !== '';
});
$task->operator_id = $this->user()['id'] ?? 0;
if ((new ExportService())->getFirstSameTask($task->list_api, $task->filters, $task->operator_id)) { // 如果当天已经有相同过滤条件且还未成功生成文件的任务
return $this->success([], '已经有相同的任务,请勿重复导出');
}
$task->current_page = 0;
$saved = $task->save();
log_operator($this->getModel(), '导出', $task->id ?? 0);
$limit_max = ExportTasks::LIMIT_SIZE_MAX;
return $saved ? $this->success([], '导出任务提交成功, 请在右上角小铃铛处查看任务状态,您将最多导出' . $limit_max . '条数据。') : $this->fail(ErrorCode::CODE_ERR_SERVER, '导出失败');
}
public function exportTasks()
{
/** @var ExportService $export */
$export = make(ExportService::class);
$list = $export->getTasks(null, $this->auth_service->get('id'), [
'id',
'name',
'status',
'total_pages',
'current_page',
'download_url',
'create_at',
]);
return $this->success([
'list' => $list,
]);
}
public function exportLimit()
{
$limit_max = ExportTasks::LIMIT_SIZE_MAX;
return $this->success([
'max' => $limit_max
]);
}
/**
* 导出任务重试
* 重新丢入队列
*
* @param int $id 重试id
*
* @return Mixed
*/
public function exportTasksRetry($id)
{
$export_tasks = ExportTasks::find($id);
$updated = false;
if ($export_tasks) {
$updated = $export_tasks->update([
'status' => 0,
'current_page' => 0
]);
}
return $this->success([
'retry' => $updated
]);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace HyperfAdmin\Admin\Crontab;
use HyperfAdmin\Admin\Model\ExportTasks;
use HyperfAdmin\Admin\Service\ExportService;
use HyperfAdmin\CronCenter\ClassJobAbstract;
use HyperfAdmin\BaseUtils\Log;
class ExportTask extends ClassJobAbstract
{
public function handle($params = null)
{
/**
* @var ExportService $export
*/
$export = make(ExportService::class);
Log::get('export_service')->info(__METHOD__ . ' ==================> started');
$list = $export->getTasks(0, 0, ['*'], $params ?? []);
$ids = is_array($list) ? array_column($list, 'id') : $list->pluck('id')->toArray();
if($ids) {
ExportTasks::whereIn('id', $ids)
->update(['status' => ExportTasks::STATUS_PRE_PROCESSING]); // 设置预处理状态
}
foreach($list as $task) {
$export->processTask($task);
}
}
protected function evaluate(): bool
{
return true;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace HyperfAdmin\Admin\Install;
use Hyperf\Command\Command as HyperfCommand;
use Hyperf\DbConnection\Db;
class InstallCommand extends HyperfCommand
{
protected $name = 'hyperf-admin:admin-install';
protected function configure()
{
$this->setDescription('install db from hyperf-admin.');
}
public function handle()
{
$db_conf = config('databases.hyperf_admin');
if (!$db_conf || !$db_conf['host']) {
$this->output->error('place set hyperf_admin db config in env');
}
$sql = file_get_contents(__DIR__ . '/install.sql');
$re = Db::connection('hyperf_admin')->getPdo()->exec($sql);
$this->output->success('hyperf-admin db install success');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace HyperfAdmin\Admin\Install;
use Composer\Semver\Comparator;
use Hyperf\Command\Command as HyperfCommand;
use Hyperf\DbConnection\Db;
use Hyperf\Utils\Composer;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
class UpdateCommand extends HyperfCommand
{
protected $name = 'hyperf-admin:admin-update';
protected function configure()
{
$this->setDescription('update db for hyperf-admin.')
->addArgument('version', InputArgument::REQUIRED, 'the update db version.');
}
public function handle()
{
$version = $input->getArgument('version');
$db_conf = config('databases.hyperf_admin');
if (!$db_conf || !$db_conf['host']) {
$this->output->error('place set hyperf_admin db config in env');
return 1;
}
$update_sql_file = __DIR__ . "/update_{$version}.sql";
if (!file_exists($update_sql_file)) {
$this->output->error("the version {$version} file not found");
return 1;
}
$sql = file_get_contents($update_sql_file);
$re = Db::connection('hyperf_admin')->getPdo()->exec($sql);
$this->output->success('hyperf-admin db update success');
}
}

View File

@@ -0,0 +1,141 @@
-- rock-admin db 安装
CREATE TABLE `common_config` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`namespace` varchar(50) NOT NULL DEFAULT '' COMMENT '命名空间, 字母',
`name` varchar(100) NOT NULL COMMENT '配置名, 字母',
`title` varchar(100) NOT NULL DEFAULT '' COMMENT '可读配置名',
`remark` varchar(100) NOT NULL DEFAULT '' COMMENT '备注',
`rules` text COMMENT '配置规则描述',
`value` text COMMENT '具体配置值 key:value',
`permissions` text COMMENT '权限',
`create_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_need_form` tinyint(3) unsigned NOT NULL DEFAULT '1' COMMENT '是否启用表单01',
PRIMARY KEY (`id`),
UNIQUE KEY `unique` (`name`,`namespace`),
KEY `namespace` (`namespace`),
KEY `update_at` (`update_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通用配置';
CREATE TABLE `export_tasks` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '任务名称',
`list_api` varchar(255) NOT NULL COMMENT '列表接口',
`filters` text COMMENT '过滤条件',
`status` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '任务状态: 0未开始, 1进行中, 2已完成, 3失败',
`total_pages` int(11) unsigned NOT NULL DEFAULT '1' COMMENT '总页数',
`current_page` int(11) unsigned NOT NULL DEFAULT '1' COMMENT '当前页',
`operator_id` int(11) unsigned NOT NULL COMMENT '管理员id',
`download_url` varchar(100) NOT NULL DEFAULT '' COMMENT '下载地址',
`create_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`host_ip` varchar(50) DEFAULT '' COMMENT '主机ip',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `front_routes` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`pid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '父级ID',
`label` varchar(50) NOT NULL DEFAULT '' COMMENT 'label名称',
`module` varchar(50) NOT NULL DEFAULT '' COMMENT '模块',
`path` varchar(100) NOT NULL DEFAULT '' COMMENT '路径',
`view` varchar(100) NOT NULL DEFAULT '' COMMENT '非脚手架渲染是且path路径为正则时, vue文件路径',
`icon` varchar(50) NOT NULL DEFAULT '' COMMENT 'icon',
`open_type` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '打开方式 0 当前页面 2 新标签页',
`is_scaffold` tinyint(4) unsigned NOT NULL DEFAULT '1' COMMENT '是否脚手架渲染, 1是, 0否',
`is_menu` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '是否菜单 0 否 1 是',
`status` tinyint(4) unsigned NOT NULL DEFAULT '1' COMMENT '状态0 禁用 1 启用',
`sort` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '排序,数字越大越在前面',
`create_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`permission` text NOT NULL COMMENT '权限标识',
`http_method` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '请求方式; 0, Any; 1, GET; 2, POST; 3, PUT; 4, DELETE;',
`type` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '菜单类型 0 目录 1 菜单 2 其他',
`page_type` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '页面类型: 0 列表 1 表单',
`scaffold_action` varchar(255) NOT NULL DEFAULT '' COMMENT '脚手架预置权限',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `global_config` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`namespace` varchar(50) NOT NULL DEFAULT '',
`name` varchar(100) NOT NULL,
`title` varchar(100) NOT NULL DEFAULT '',
`remark` varchar(100) NOT NULL DEFAULT '',
`value` longtext,
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_name` (`name`),
KEY `namespace` (`namespace`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='全局配置';
CREATE TABLE `role_menus` (
`role_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '角色ID',
`router_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '路由ID',
`create_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `role_router_id` (`role_id`,`router_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `roles` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`pid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '父级ID',
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '名称',
`permissions` text NOT NULL COMMENT '角色拥有的权限',
`status` tinyint(4) unsigned NOT NULL DEFAULT '1' COMMENT '状态0 禁用 1 启用',
`sort` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '排序,数字越大越在前面',
`create_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
`realname` varchar(50) NOT NULL DEFAULT '',
`password` char(40) NOT NULL,
`mobile` varchar(20) NOT NULL DEFAULT '',
`email` varchar(50) NOT NULL DEFAULT '',
`status` tinyint(4) NOT NULL DEFAULT '1',
`login_time` timestamp NULL DEFAULT NULL,
`login_ip` varchar(50) DEFAULT NULL,
`is_admin` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'is admin',
`is_default_pass` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否初始密码1:是,0:否',
`qq` varchar(20) NOT NULL DEFAULT '' COMMENT '用户qq',
`roles` varchar(50) NOT NULL DEFAULT '10',
`sign` varchar(255) NOT NULL DEFAULT '' COMMENT '签名',
`avatar` varchar(255) NOT NULL DEFAULT '',
`avatar_small` varchar(255) NOT NULL DEFAULT '',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `user_role` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(11) unsigned NOT NULL DEFAULT '0',
`role_id` int(11) unsigned NOT NULL DEFAULT '0',
`create_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_role_id` (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `operator_log` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`page_url` varchar(50) DEFAULT '' COMMENT '页面url',
`page_name` varchar(50) DEFAULT '' COMMENT '页面面包屑/名称',
`action` varchar(50) DEFAULT '' COMMENT '动作',
`operator_id` int(11) DEFAULT '0' COMMENT '操作人ID',
`nickname` varchar(50) DEFAULT '' COMMENT '操作人名称',
`relation_ids` text COMMENT '多个id-当前版本ID[id-current_version_id,]',
`detail_json` text COMMENT '需要灵活记录的json',
`client_ip` varchar(50) DEFAULT '' COMMENT '客户端地址',
`create_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4511 DEFAULT CHARSET=utf8 COMMENT='通用操作日志';

View File

@@ -0,0 +1,54 @@
<?php
/**
* hyperf_admin 登录状态检测中间件
*/
namespace HyperfAdmin\Admin\Middleware;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface as HttpResponse;
use Hyperf\HttpServer\CoreMiddleware;
use Hyperf\Logger\LoggerFactory;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use HyperfAdmin\Admin\Service\AuthService;
class AuthMiddleware extends CoreMiddleware
{
/**
* @var RequestInterface
*/
protected $request;
/**
* @var HttpResponse
*/
protected $response;
/**
* @var LoggerFactory
*/
protected $log;
/**
* @var AuthService
*/
protected $auth_service;
public function __construct(ContainerInterface $container, HttpResponse $response, RequestInterface $request, LoggerFactory $logger)
{
$this->log = $logger->get('auth');
$this->auth_service = make(AuthService::class);
parent::__construct($container, 'http');
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// 检验登录状态
$user = $this->auth_service->check();
$this->log->info('当前登录用户信息', $user);
return $handler->handle($request);
}
}

View File

@@ -0,0 +1,147 @@
<?php
/**
* hyperf_admin 鉴权中间件
* 统一负责 资源权限校验
*/
namespace HyperfAdmin\Admin\Middleware;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface as HttpResponse;
use Hyperf\HttpServer\CoreMiddleware;
use Hyperf\Logger\LoggerFactory;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use HyperfAdmin\Admin\Service\PermissionService;
use HyperfAdmin\Admin\Service\AuthService;
use HyperfAdmin\BaseUtils\AKSK;
use HyperfAdmin\BaseUtils\Constants\ErrorCode;
class PermissionMiddleware extends CoreMiddleware
{
/**
* @var RequestInterface
*/
protected $request;
/**
* @var HttpResponse
*/
protected $response;
/**
* @var LoggerFactory
*/
protected $log;
/**
* @var PermissionService
*/
protected $permission_service;
/**
* @var AuthService
*/
protected $auth_service;
public function __construct(ContainerInterface $container, HttpResponse $response, RequestInterface $request, LoggerFactory $logger)
{
$this->container = $container;
$this->response = $response;
$this->request = $request;
$this->log = $logger->get('permission');
$this->permission_service = make(PermissionService::class);
$this->auth_service = make(AuthService::class);
parent::__construct($container, 'http');
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$uri = $request->getUri();
$path = $uri->getPath();
$method = $request->getMethod();
// 其他系统调用走AKSK中间件验证
$client_token = $request->getHeader('Authorization')[0] ?? '';
if ($client_token) {
if (!$this->akSkAuth($uri, $method, $client_token)) {
return $this->fail(ErrorCode::CODE_ERR_DENY, '接口权限校验失败');
}
return $handler->handle($request);
}
// 内部请求时不做鉴权
if ($this->getRealIp() == '127.0.0.1') {
return $handler->handle($request);
}
// 开放资源,不进行鉴权
if ($this->permission_service->isOpen($path, $method)) {
return $handler->handle($request);
}
// 检验登录状态
$user = $this->auth_service->user();
if (empty($user)) {
return $this->fail(ErrorCode::CODE_LOGIN, '请先登录');
}
if (!$this->permission_service->hasPermission($path, $method)) {
return $this->fail(ErrorCode::CODE_NO_AUTH, "{$path}权限不足");
}
return $handler->handle($request);
}
protected function getRealIp()
{
return $this->request->header('x-real-ip');
}
/**
* @param int $code
* @param string|null $message
*
* @return \Psr\Http\Message\ResponseInterface
*/
public function fail(int $code = -1, ?string $message = null)
{
$response = [
'code' => $code,
'message' => $message ?: ErrorCode::getMessage($code),
'payload' => (object)[],
];
$this->log->warning($this->request->getUri()->getPath() . ' fail', $response);
return $this->response->json($response);
}
private function akSkAuth($uri, $method, $client_token)
{
$host = env('APP_DOMAIN', '');
$query = $this->request->getQueryParams();
if (!empty($query)) {
ksort($query);
$query = http_build_query($query);
} else {
$query = '';
}
$path = $uri->getPath();
$content_type = $this->request->getHeader('Content-type')[0] ?? '';
$body = $this->request->getParsedBody();
if (!empty($body)) {
ksort($body);
$body = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} else {
$body = '';
}
$ak = str_replace('ha ', '', explode(':', $client_token)[0] ?? '');
$sk = config('client_user')[$ak] ?? '';
$auth = new AKSK($ak, $sk);
$token = $auth->token($method, $path, $host, $query, $content_type, $body);
$this->log->info('aksk auth:', [
'client_token' => $client_token,
'except_token' => $token,
]);
return $token === $client_token;
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare (strict_types=1);
namespace HyperfAdmin\Admin\Model;
use HyperfAdmin\BaseUtils\Model\BaseModel;
/**
* @property string $namespace
* @property string $name
* @property string $title
* @property string $remark
* @property string $rules
* @property string $value
* @property string $permissions
* @property string $is_need_form
*/
class CommonConfig extends BaseModel
{
protected $table = 'common_config';
protected $connection = 'hyperf_admin';
protected $fillable = [
'namespace',
'name',
'title',
'remark',
'rules',
'value',
'permissions',
'is_need_form',
];
protected $casts = [
'value' => 'array',
'rules' => 'array',
'is_need_form' => 'integer',
];
const NEED_FORM_NO = 0;
const NEED_FORM_YES = 1;
public static $need_form = [
self::NEED_FORM_NO => '否',
self::NEED_FORM_YES => '是',
];
}

View File

@@ -0,0 +1,54 @@
<?php
declare (strict_types=1);
namespace HyperfAdmin\Admin\Model;
use HyperfAdmin\BaseUtils\Model\BaseModel;
/**
* @property int $id
* @property string $name
* @property string $list_api
* @property array $filters
* @property int $status
* @property int $total_pages
* @property int $current_page
* @property int $operator_id
* @property string $download_url
*/
class ExportTasks extends BaseModel
{
protected $table = 'export_tasks';
protected $connection = 'hyperf_admin';
protected $fillable = [
'name',
'list_api',
'filters',
'status',
'total_pages',
'current_page',
'operator_id',
'download_url',
];
protected $casts = [
'filters' => 'array',
'status' => 'integer',
'total_pages' => 'integer',
'current_page' => 'integer',
'operator_id' => 'integer',
];
const STATUS_NOT_START = 0;
const STATUS_PRE_PROCESSING = 10; // 预处理状态
const STATUS_PROCESSING = 1;
const STATUS_SUCCESS = 2;
const STATUS_FAIL = 3;
const LIMIT_SIZE_MAX = 30000; // 导出最大条数
}

View File

@@ -0,0 +1,102 @@
<?php
declare (strict_types=1);
namespace HyperfAdmin\Admin\Model;
use HyperfAdmin\BaseUtils\Model\BaseModel;
/**
* @property int $pid
* @property string $label
* @property string $module
* @property string $path
* @property string $icon
* @property int $open_type
* @property int $is_menu
* @property int $state
* @property int $sort
*/
class FrontRoutes extends BaseModel
{
protected $table = 'front_routes';
protected $connection = 'hyperf_admin';
protected $fillable = [
'pid',
'label',
'module',
'path',
'view',
'icon',
'open_type',
'is_menu',
'status',
'is_scaffold',
'sort',
'type',
'permission',
'http_method',
'page_type',
];
protected $casts = [
'pid' => 'integer',
'open_type' => 'integer',
'type' => 'integer',
'is_menu' => 'integer',
'status' => 'integer',
'is_scaffold' => 'integer',
'page_type' => 'integer',
'sort' => 'integer',
];
const HTTP_METHOD_ANY = 0;
const HTTP_METHOD_GET = 1;
const HTTP_METHOD_POST = 2;
const HTTP_METHOD_PUT = 3;
const HTTP_METHOD_DELETE = 4;
const PAGE_TYPE_LIST = 0;
const PAGE_TYPE_FORM = 1;
const IS_MENU = 1;
const IS_NOT_MENU = 0;
const RESOURCE_OPEN = 0;
const RESOURCE_NEED_AUTH = 1;
public static $http_methods = [
self::HTTP_METHOD_ANY => 'ANY',
self::HTTP_METHOD_GET => 'GET',
self::HTTP_METHOD_POST => 'POST',
self::HTTP_METHOD_PUT => 'PUT',
self::HTTP_METHOD_DELETE => 'DELETE',
];
public function scopeDefaultSelect($query)
{
return $query->select([
'id',
'pid',
'label as menu_name',
'is_menu as hidden',
'is_scaffold as scaffold',
'path as url',
'view',
'icon',
'sort',
]);
}
public function scopeOnlyMenu($query)
{
return $query->where('is_menu', YES);
}
}

View File

@@ -0,0 +1,96 @@
<?php
declare (strict_types=1);
namespace HyperfAdmin\Admin\Model;
use HyperfAdmin\BaseUtils\Redis\Redis;
use HyperfAdmin\BaseUtils\Model\BaseModel;
/**
* @property int $id
* @property string $namespace
* @property string $name
* @property string $title
* @property string $remark
* @property string $rules
* @property string $value
*/
class GlobalConfig extends BaseModel
{
protected $table = 'global_config';
protected $connection = 'hyperf_admin';
protected $fillable = [
'namespace',
'name',
'title',
'remark',
'rules',
'value',
];
protected $casts = ['id' => 'int'];
const CACHE_PREFIX = 'omsapi_config_';
const CACHE_TIME = 60 * 5;
const VALUE_STATUS_YES = 1; //启用
const VALUE_STATUS_NO = 0; //禁用
const VALUE_STATUS_MAP = [
self::VALUE_STATUS_YES => '启用',
self::VALUE_STATUS_NO => '禁用',
];
const NAMESPACE_AB_WHITE_LIST = 'ab_white_list';
public static function getConfig($name, $default = null, $cache = false)
{
$cache_key = self::getConfigCache($name);
$cache_config = Redis::get($cache_key);
if($cache_config) {
return json_decode($cache_config, true);
}
$item = static::query()->where('name', $name)->get()->toArray();
if(empty($item)) {
return $default;
}
$config = json_decode($item[0]['value'], true);
if(is_null($config)) {
return $default;
}
if($cache) {
Redis::setex($cache_key, self::CACHE_TIME, json_encode($config));
}
return $config;
}
public static function getConfigCache($name)
{
return self::CACHE_PREFIX . $name;
}
public static function setConfig($name, $value, $ext = [], $raw = true)
{
$namespace = '';
if(($pos = strpos($name, '.')) !== false) {
$namespace = substr($name, 0, $pos);
}
if($raw) {
$value = json_encode($value);
}
$ins = [
'name' => $name,
'value' => $value,
'namespace' => $namespace,
];
if($ext) {
$ins = array_merge($ins, $ext);
}
return static::query()->updateOrInsert(['name' => $name], $ins);
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare (strict_types=1);
namespace HyperfAdmin\Admin\Model;
use HyperfAdmin\BaseUtils\Model\BaseModel;
class OperatorLog extends BaseModel
{
protected $table = 'operator_log';
protected $connection = 'hyperf_admin';
protected $fillable = [
'page_url',
'page_name',
'action',
'operator_id',
'nickname',
'relation_ids',//多个id-当前版本ID[id-current_version_id,]
'detail_json',//需要灵活记录的json
'client_ip', // 客户端地址
];
}

View File

@@ -0,0 +1,43 @@
<?php
declare (strict_types=1);
namespace HyperfAdmin\Admin\Model;
use HyperfAdmin\BaseUtils\Model\BaseModel;
class RequestLog extends BaseModel
{
const CREATED_AT = 'create_at';
const UPDATED_AT = 'update_at';
protected $connection = 'hyperf_admin';
protected $table = 'request_log';
protected $fillable = [
'host',
'method',
'path',
'header',
'params',
'user_id',
'req_id',
];
protected $casts = [
'header' => 'array',
'params' => 'array',
'user_id' => 'integer',
'req_id' => 'integer',
];
/**
* 获取产生当前版本的用户信息
*
* @return \Hyperf\Database\Model\Model|\Hyperf\Database\Model\Relations\BelongsTo|object|null
*/
public function getUser()
{
return $this->belongsTo(User::class, 'user_id', 'id')->first();
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare (strict_types=1);
namespace HyperfAdmin\Admin\Model;
use HyperfAdmin\BaseUtils\Model\BaseModel;
/**
* @property int $id
* @property int $pid
* @property string $name
* @property int $status
* @property int $sort
* @property \Carbon\Carbon $create_at
* @property \Carbon\Carbon $update_at
*/
class Role extends BaseModel
{
protected $table = 'roles';
protected $connection = 'hyperf_admin';
protected $fillable = [
'name',
'pid',
'status',
'sort',
'permissions',
];
protected $casts = [
'status' => 'integer',
'permissions' => 'array',
'sort' => 'integer',
'name' => 'string',
'pid' => 'integer',
'id' => 'integer',
'value' => 'integer',
];
public function menus()
{
return $this->hasMany('App\System\Model\MtOms\RoleMenu', 'role_id');
}
public function resources()
{
return $this->hasMany('App\System\Model\MtOms\RoleResource', 'role_id');
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare (strict_types=1);
namespace HyperfAdmin\Admin\Model;
use HyperfAdmin\BaseUtils\Model\BaseModel;
/**
* @property int $role_id
* @property int $router_id
* @property \Carbon\Carbon $create_at
* @property \Carbon\Carbon $update_at
*/
class RoleMenu extends BaseModel
{
protected $table = 'role_menus';
protected $connection = 'hyperf_admin';
protected $fillable = [
'role_id',
'router_id',
];
protected $casts = [
'role_id' => 'integer',
'router_id' => 'integer',
];
}

View File

@@ -0,0 +1,85 @@
<?php
declare (strict_types=1);
namespace HyperfAdmin\Admin\Model;
use HyperfAdmin\BaseUtils\Model\BaseModel;
/**
* @property int $id
* @property string $username
* @property string $realname
* @property string $password
* @property string $mobile
* @property string $email
* @property int $status
* @property string $login_time
* @property string $login_ip
* @property int $is_admin
* @property int $is_default_pass
* @property string $qq
* @property string $roles
* @property string $sign
* @property string $avatar
* @property string $avatar_small
* @property \Carbon\Carbon $create_at
* @property \Carbon\Carbon $update_at
*/
class User extends BaseModel
{
protected $connection = 'hyperf_admin';
protected $table = 'user';
protected $fillable = [
'id',
'username',
'realname',
'password',
'mobile',
'email',
'status',
'login_time',
'login_ip',
'is_admin',
'is_default_pass',
'qq',
'roles',
'sign',
'avatar',
'avatar_small',
];
protected $guarded = [];
protected $casts = [
'id' => 'int',
'status' => 'integer',
'is_admin' => 'integer',
'is_default_pass' => 'integer',
'value' => 'integer',
];
const USER_ENABLE = 1;
const USER_DISABLE = 0;
public static $status = [
self::USER_DISABLE => '禁用',
self::USER_ENABLE => '启用',
];
public function getRolesAttribute($value)
{
return explode(',', $value);
}
public function setRolesAttribute($value)
{
return implode(',', $value);
}
public function getRealnameAttribute($value)
{
return $value ?: $this->username;
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare (strict_types=1);
namespace HyperfAdmin\Admin\Model;
use HyperfAdmin\BaseUtils\Model\BaseModel;
/**
* @property int $id
* @property int $user_id
* @property int $role_id
* @property \Carbon\Carbon $create_at
* @property \Carbon\Carbon $update_at
*/
class UserRole extends BaseModel
{
protected $table = 'user_role';
protected $connection = 'hyperf_admin';
protected $fillable = [
'user_id',
'role_id',
];
protected $casts = [
'user_id' => 'integer',
'role_id' => 'integer',
];
}

View File

@@ -0,0 +1,76 @@
<?php
declare (strict_types=1);
namespace HyperfAdmin\Admin\Model;
use HyperfAdmin\BaseUtils\Model\BaseModel;
class Version extends BaseModel
{
const CREATED_AT = 'create_at';
const UPDATED_AT = 'update_at';
protected $connection = 'hyperf_admin';
protected $table = 'data_version';
protected $fillable = [
'pk',
'table',
'content',
'req_id',
'user_id',
'action',
'modify_fields'
];
protected $casts = [
'content' => 'array',
'modify_fields' => 'array',
];
public function versionable()
{
return $this->morphTo(null, 'table', 'pk');
}
/**
* 获取产生当前版本的请求信息
* @return \Hyperf\Database\Model\Model|\Hyperf\Database\Model\Relations\BelongsTo|object|null
*/
public function getRequest()
{
return $this->belongsTo(RequestLog::class, 'req_id', 'req_id')->first();
}
/**
* 获取产生当前版本的用户信息
* @return \Hyperf\Database\Model\Model|\Hyperf\Database\Model\Relations\BelongsTo|object|null
*/
public function getUser()
{
return $this->belongsTo(User::class, 'user_id', 'id')->first();
}
/**
* TODO: 该方法应该不可用
* 回滚版本
* @return mixed
*/
public function revert()
{
$model = new $this->versionable_type();
$model->unguard();
$model->fill($this->content);
$model->exists = true;
$model->reguard();
unset($model->{$model->getCreatedAtColumn()});
unset($model->{$model->getUpdatedAtColumn()});
if (method_exists($model, 'getDeletedAtColumn')) {
unset($model->{$model->getDeletedAtColumn()});
}
$model->save();
return $model;
}
}

View File

@@ -0,0 +1,195 @@
<?php
declare (strict_types=1);
namespace HyperfAdmin\Admin\Model;
use Hyperf\Database\Model\Events\Saved;
use Hyperf\Database\Model\Events\Saving;
use Hyperf\Utils\Context;
use Psr\Http\Message\ServerRequestInterface;
use HyperfAdmin\BaseUtils\JWT;
trait Versionable
{
protected $versioning_enable = true;
protected $is_update;
public function isVersionEnable()
{
return $this->versioning_enable;
}
/**
* 启动版本控制
*
* @return $this
*/
public function enableVersioning()
{
$this->versioning_enable = true;
return $this;
}
/**
* 关闭版本控制
*
* @return $this
*/
public function disableVersioning()
{
$this->versioning_enable = false;
return $this;
}
/**
* 定义版本关联
*
* @return mixed
*/
public function versions()
{
return $this->morphMany(Version::class, null, 'table', 'pk');
}
public function getMorphClass()
{
if (strpos($this->getTable(), '.') !== false) {
return $this->getTable();
}
return $this->getConnectionName() . '.' . $this->getTable();
}
/**
* 是否是一次有效的版本控制
*
* @return bool
*/
private function isValid()
{
$versionable_fields = $this->getVersionableFields();
return $this->versioning_enable
&& Context::has(ServerRequestInterface::class)
&& $this->isDirty($versionable_fields);
}
public function getVersionableFields()
{
$remove_version_keys = $this->versioning_expect_fields ?? [];
$remove_version_keys[] = $this->getUpdatedAtColumn();
if (method_exists($this, 'getDeletedAtColumn')) {
$remove_version_keys[] = $this->getDeletedAtColumn();
}
return collect($this->getAttributes())->except($remove_version_keys)->keys()->all();
}
public function saving(Saving $event)
{
$this->is_update = $this->exists;
}
/**
* 监听saved事件保存表更数据
*
* @param Saved $event
*/
public function saved(Saved $event)
{
if (!$this->isValid()) {
return;
}
$request_log = $this->processRequest();
$version = new Version();
$version->pk = $this->getKey();
$version->table = strpos($this->getTable(), '.') ? $this->getTable() : $this->getConnectionName() . '.' . $this->getTable();
$version->content = $this->getAttributes();
$versionable_fields = $this->getVersionableFields();
$changes = array_remove_keys_not_in($this->getChanges(), $versionable_fields);
$version->modify_fields = array_keys($changes);
$version->action = $this->is_update ? 'update' : 'insert';
$version->user_id = $request_log->user_id;
/** @var ServerRequestInterface $request */
$request = container(ServerRequestInterface::class);
$version->req_id = $request->getAttribute('_req_id');
$version->save();
$this->clearOldVersions();
}
/**
* 记录数据版本前先记录请求
*
* @return RequestLog
*/
private function processRequest(): RequestLog
{
/** @var ServerRequestInterface $request */
$request = container(ServerRequestInterface::class);
$req_id = $request->getAttribute('_req_id');
if (empty($req_id)) {
$req_id = id_gen();
$request = Context::set(ServerRequestInterface::class, $request->withAttribute('_req_id', $req_id));
return RequestLog::create([
'host' => $request->getUri()->getHost(),
'path' => $request->getUri()->getPath(),
'method' => $request->getMethod(),
'header' => $request->getHeaders(),
'params' => $request->getParsedBody(),
'user_id' => JWT::verifyToken(cookie('X-Token') ?? '')['user_info']['id'] ?? 0,
'req_id' => $req_id,
]);
} else {
return RequestLog::where('req_id', $req_id)->first();
}
}
/**
* 默认保留100个数据版本可被覆盖
*/
private function clearOldVersions()
{
$keep = $this->keep_version_count ?? 100;
$count = $this->versions()->count();
if ($keep > 0 && $count > $keep) {
$this->versions()->limit($count - $keep)->delete();
}
}
/**
* 返回当前版本的数据
*
* @return mixed
*/
public function currentVersion()
{
return $this->versions()->orderBy(Version::CREATED_AT, 'DESC')->orderBy('id', 'DESC')->first();
}
/**
* 前几个版本
*
* @param int $previous
*
* @return mixed
*/
public function previousVersion(int $previous = 1)
{
return $this->versions()->orderBy(Version::CREATED_AT, 'DESC')->orderBy('id', 'DESC')->offset($previous)->first();
}
/**
* 最后几个版本
*
* @param int $num
*
* @return mixed
*/
public function lastVersion($num = 1)
{
return $this->versions()->orderBy('id', 'DESC')->limit($num)
//->offset(1) // 排除当前版本
->get();
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace HyperfAdmin\Admin\Service;
use Hyperf\Utils\Arr;
use Hyperf\Utils\Context;
use HyperfAdmin\BaseUtils\Log;
use HyperfAdmin\BaseUtils\Redis\Redis;
use HyperfAdmin\BaseUtils\JWT;
class AuthService
{
public function check()
{
$token = cookie(config('admin_cookie_name', '')) ?: (request_header('x-token')[0] ?? '');
$payload = JWT::verifyToken($token);
if(!is_production()) {
Log::get('userinfo')->info('hyperf_admin_token_payload', is_array($payload) ? $payload : [$payload]);
}
if(!$payload) {
return [];
}
$sso_user_info = Arr::get($payload, 'user_info');
if(empty($sso_user_info)) {
return [];
}
$cache_key = config('user_info_cache_prefix') . md5(json_encode($sso_user_info));
if(!$user = Redis::get($cache_key)) {
$user = container(UserService::class)->findUserOrCreate($sso_user_info);
$expire = $payload['exp'] - time();
if($user && $expire > 0) {
// 缓存用户信息
Redis::setex($cache_key, json_encode($user), $expire);
}
} else {
$user = json_decode($user, true);
}
return $this->setUser($user);
}
public function user()
{
return Context::get('user_info') ?? [];
}
public function isSupperAdmin()
{
$user = $this->user();
return ($user['is_admin'] ?? NO) === YES;
}
public function setUser($data)
{
return Context::set('user_info', $data);
}
public function get($key)
{
return Arr::get($this->user(), $key);
}
public function logout()
{
$user = $this->user();
$cache_key = config('user_info_cache_prefix') . md5(json_encode($user));
Redis::connection()->del($cache_key);
Context::set('user_info', null);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace HyperfAdmin\Admin\Service;
use HyperfAdmin\Admin\Model\CommonConfig as CommonConfigModel;
class CommonConfig
{
public static function getNamespaces()
{
return CommonConfigModel::query()
->where('namespace', 'system')
->where('name', 'namespace')
->first()
->toArray()['rules'] ?? [];
}
public static function getValue($namespace, $name, $default = [])
{
if($record = CommonConfigModel::query()
->where('namespace', $namespace)
->where('name', $name)
->first()) {
return $record->toArray()['value'];
}
return $default;
}
public static function getConfigByName($name)
{
$conf = CommonConfigModel::query()->where(['name' => $name])->select([
'id',
'rules',
'value',
])->first();
if(!$conf) {
return false;
}
return $conf->toArray();
}
public static function getValByName($name)
{
$value = CommonConfigModel::query()->where(['name' => $name])->value('value');
return $value ?: [];
}
}

View File

@@ -0,0 +1,197 @@
<?php
namespace HyperfAdmin\Admin\Service;
use Carbon\Carbon;
use Hyperf\Utils\Str;
use HyperfAdmin\Admin\Model\ExportTasks;
use HyperfAdmin\BaseUtils\Guzzle;
use HyperfAdmin\BaseUtils\Log;
class ExportService
{
const LIST_API_SUFFIX = '/list';
const INFO_API_SUFFIX = '/info';
/**
* @param int $status
* @param int $operator_id
* @param array $columns
* @param array $filter_options ['not_like' => ['list_api' => ['%123%', '%345'], 'filters' => ['%123%', '%456%']],'like' => ['list_pai' => ['%ttt%']], 'in' => ['operator_id' => [123,123,123]], 'not_in' => ['operator_id' => [456,5466,234]]]
*
* @return array
*/
public function getTasks($status = 0, $operator_id = 0, $columns = ['*'], $filter_options = [])
{
$query = ExportTasks::query()->select($columns);
if($status !== null) {
$query->where('status', $status);
}
if($operator_id) {
$query->where('operator_id', $operator_id);
}
if(!empty($filter_options['not_like'])) {
$query->where(function ($q) use ($filter_options) {
foreach($filter_options['not_like'] as $column => $likes) {
foreach($likes as $like) {
$q->where($column, 'not like', $like);
}
}
});
}
if(!empty($filter_options['like'])) {
$query->where(function ($q) use ($filter_options) {
foreach($filter_options['like'] as $column => $likes) {
foreach($likes as $like) {
$q->where($column, 'like', $like);
}
}
});
}
if(!empty($filter_options['in'])) {
$query->where(function ($q) use ($filter_options) {
foreach($filter_options['in'] as $column => $ins) {
$q->whereIn($column, $ins);
}
});
}
if(!empty($filter_options['not_in'])) {
$query->where(function ($q) use ($filter_options) {
foreach($filter_options['not_in'] as $column => $ins) {
$q->whereNotIn($column, $ins);
}
});
}
$query->orderBy('id', 'desc');
return $query->get() ?: [];
}
public function processTask(ExportTasks $task)
{
try {
if(in_array(ExportTasks::find($task->id)->status, [
ExportTasks::STATUS_PROCESSING,
ExportTasks::STATUS_SUCCESS,
])) {
Log::get('export_service')->info('正在处理该任务,不要重复处理', [$task]);
return;
}
$task->fill(['status' => ExportTasks::STATUS_PROCESSING])->save();
$list_api = 'http://127.0.0.1:' . config('server.servers.0.port') . $task->list_api;
$query['_page'] = ($query['_page'] ?? 1);
$size = 100;
$query['_size'] = $size;
$query = array_merge($query, $task->filters);
$headers = [
'X-Real-IP' => '127.0.0.1',
];
$total = 999;
if(Str::endsWith($task->list_api, self::LIST_API_SUFFIX)) {
$info_api = 'http://127.0.0.1:' . config('server.servers.0.port') . str_replace(self::LIST_API_SUFFIX, self::INFO_API_SUFFIX, $task->list_api);
} else {
$subject = explode('/', $task->list_api)[1];
$info_api = 'http://127.0.0.1:' . config('server.servers.0.port') . '/' . $subject . '/info';
}
$info = Guzzle::get($info_api, [], $headers);
$table_headers = array_filter($info['payload']['tableHeader'], function ($item) {
return $item['hidden'] ?? true;
});
$table_headers_str = [];
foreach($table_headers as $item) {
$table_headers_str[] = $item['title'];
}
$table_headers_str = implode(',', $table_headers_str);
$file_name = '《' . $task->name . '》下载-' . Carbon::now()->format('YmdHis') . '.csv';
$file_path = '/tmp/' . $file_name;
file_put_contents($file_path, $this->encoding($table_headers_str) . PHP_EOL);
$counter = 0;
$parse = parse_url($list_api);
if(isset($parse['query'])) {
parse_str($parse['query'], $_query);
$query = array_merge($query, $_query);
}
while(($query['_page'] - 1) * $size < $total) { // offset值
$ret = Guzzle::get($list_api, $query, $headers);
$total = $ret['payload']['total'] <= ExportTasks::LIMIT_SIZE_MAX ? $ret['payload']['total'] : ExportTasks::LIMIT_SIZE_MAX;
if(!is_array($ret['payload']['list'])) {
throw new \Exception('列表获取异常任务id' . $task->id);
}
foreach($ret['payload']['list'] as $item) {
$row = $this->getRow($table_headers, $item);
$counter++;
file_put_contents($file_path, $this->encoding($row) . PHP_EOL, FILE_APPEND);
}
$task->fill([
'total_pages' => ceil($total / $size),
'current_page' => $query['_page'],
])->save();
$query['_page'] += 1;
}
$info = move_local_file_to_oss($file_path, '1/export_task/' . $file_name, true);
if($info) {
$task->fill([
'status' => ExportTasks::STATUS_SUCCESS,
'download_url' => $info['path'],
])->save();
Log::get('export_service')
->info(sprintf('export task success, file_name:%s id:%s rows:%s', $info['file_path'], $task->id, $counter), [], 'export_task');
} else {
Log::get('export_service')
->error(sprintf('export task fail id:%s', $task->id), [], 'export_task');
$task->fill(['status' => ExportTasks::STATUS_FAIL])->save();
}
} catch (\Exception $exception) {
Log::get('export_service')
->error(sprintf('export task fail id:%s', $task->id), ['exception' => $exception], 'export_task');
$task->fill(['status' => ExportTasks::STATUS_FAIL])->save();
}
}
public function getRow($table_headers, $data)
{
$arr = [];
foreach($table_headers as $item) {
if(isset($item['options'])) {
$arr[] = $item['options'][$data[$item['field']]] ?? ($data[$item['field']] ?? '');
continue;
}
if(isset($item['enum'])) {
$arr[] = $item['enum'][$data[$item['field']]] ?? '';
continue;
}
$arr[] = $data[$item['field']] ?? '';
}
$arr = array_map(function ($item) {
$item = csv_big_num($item);
$item = preg_replace('/\\n/', ' ', $item);
return $item;
}, $arr);
return implode(',', array_map(function ($item) {
if(is_array($item)) {
return json_encode($item, JSON_UNESCAPED_UNICODE);
}
return $item;
}, $arr));
}
public function encoding($str)
{
//return iconv('utf-8', 'gbk//ignore', $str);
return mb_convert_encoding($str, "GBK", "UTF-8");
}
public function getFirstSameTask($url, $filters, $operator_id)
{
return ExportTasks::where('filters', json_encode($filters))
->where('list_api', $url)
->where('operator_id', $operator_id)
->where('create_at', '>=', Carbon::today()->toDateTimeString())
->where('status', '!=', ExportTasks::STATUS_SUCCESS)
->first();
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace HyperfAdmin\Admin\Service;
use HyperfAdmin\Admin\Model\GlobalConfig as GlobalModel;
use HyperfAdmin\BaseUtils\Redis\Redis;
class GlobalConfig
{
public static function setConfig($name, $value, $ext = [], $raw = true)
{
$namespace = '';
if(($pos = strpos($name, '.')) !== false) {
$namespace = substr($name, 0, $pos);
}
if($raw) {
$value = json_encode($value);
}
$ins = [
'name' => $name,
'value' => $value,
'namespace' => $namespace,
];
if($ext) {
$ins = array_merge($ins, $ext);
}
$model = GlobalModel::query();
$model->getConnection()->beginTransaction();
$id = $model->where('name', $name)->value('id');
if($id) {
$res = $model->where('id', $id)->update($ins);
} else {
$res = $model->insert($ins);
}
if(empty($res)) {
$model->getConnection()->rollBack();
return false;
}
$model->getConnection()->commit();
return true;
}
public static function getConfig($name, $default = null)
{
$cache_key = self::getCacheKey($name);
$data = Redis::get($cache_key);
if($data !== false) {
return my_json_decode($data);
}
$data = GlobalModel::query()->where('name', $name)->select('value')->first()->toArray();
$data = $data ?: $default;
Redis::setex($cache_key, json_encode($data, JSON_UNESCAPED_UNICODE), 5 * MINUTE);
return $data;
}
public static function getCacheKey($name)
{
return "omsapi:global_config:{$name}";
}
public static function getIdByName($name)
{
return GlobalModel::query()->where('name', $name)->value('id');
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace HyperfAdmin\Admin\Service;
use HyperfAdmin\Admin\Model\FrontRoutes;
class Menu
{
public function query()
{
return FrontRoutes::query()->newQuery();
}
public function getModuleMenus($module = '', $menu_ids = [])
{
$query = $this->query();
if (!empty($menu_ids)) {
$query->where(function ($query) use ($menu_ids) {
return $query->whereIn('id', $menu_ids)->orWhere(function ($query) use ($menu_ids) {
return $query->where('is_menu', 0)->whereIn('pid', $menu_ids);
});
});
}
$query = $query->select([
'id',
'pid',
'label as menu_name',
'is_menu as hidden',
'is_scaffold as scaffold',
'path as url',
'view',
'icon',
])->where('status', 1);
if ($module) {
$query->where('module', $module);
}
$list = $query->orderBy('sort', 'desc')->get();
if (empty($list)) {
return [];
}
$list = $list->toArray();
foreach ($list as &$item) {
$item['hidden'] = !(bool)$item['hidden'];
$item['scaffold'] = (bool)$item['scaffold'];
unset($item);
}
return generate_tree($list);
}
public function tree(
$where = [], $fields = [
'id as value',
'pid',
'label',
], $pk_key = 'value'
) {
$where['status'] = 1;
$query = make(FrontRoutes::class)->where2query($where)->select($fields);
$list = $query->orderBy('sort', 'desc')->get();
if (empty($list)) {
return [];
}
$list = $list->toArray();
return generate_tree($list, 'pid', $pk_key, 'children', function (&$item) use ($pk_key) {
$item[$pk_key] = (int)$item[$pk_key];
$item['pid'] = (int)$item['pid'];
if (isset($item['hidden'])) {
$item['hidden'] = !(bool)$item['hidden'];
}
if (isset($item['scaffold'])) {
$item['scaffold'] = (bool)$item['scaffold'];
}
unset($item);
});
}
public function getPathNodeIds($id)
{
$parents = [];
while ($p = $this->getParent($id)) {
$id = (int)$p['pid'];
if ($id) {
$parents[] = $id;
}
}
return array_reverse($parents);
}
public function getParent($id)
{
return $this->query()->select(['id', 'pid'])->find($id);
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace HyperfAdmin\Admin\Service;
use Carbon\Carbon;
use HyperfAdmin\Admin\Model\OperatorLog;
use HyperfAdmin\Admin\Model\Version;
use HyperfAdmin\BaseUtils\Log;
use HyperfAdmin\BaseUtils\Model\BaseModel;
class OperatorLogService
{
/**
* 记录日志
* 调用示例log_operator($model, $pk_val ? '编辑':'新增', $pk_val, '备注一下');
* 为保证版本数据完整性,建议在保存完成之后执行此操作
*
* @param $action string (新增|编辑|删除|导入|导出)
* @param $ids mixed 可以是id或数组
* @param $model mixed string/object/null 模型类此处模型需要自定义传入考虑到保存的模型未必是控制器的getModel也可能包括其他模型的保存或根本不需要模型
* @param string $remark 备注内容, default ''
* @param array $options 其他选项, default []
* @param int $user_id
*
* @return mixed
*/
public static function write($model, $action, $ids, $remark = '', $options = [], $user_id = 0)
{
if(!is_array($ids)) {
$ids = [$ids];
}
try {
// 页面url和名称
$page_url = request()->header('page-url');
$parse_url = parse_url($page_url);
$fragment = $parse_url['fragment'] ?? '/'; // 抽出#后面的部分
$fragments = explode('?', $fragment); // 去掉querystring
$page_url = array_shift($fragments);
$page_name = urldecode(request()->header('page-name', '')); // 页面名称
$relation_ids = json_encode($ids, JSON_UNESCAPED_UNICODE); // 如果没有版本启用则只记录操作的id
// 关联id-版本id记录
if(is_string($model) && $model) {
$model = make($model);
}
if($model
&& $model instanceof BaseModel
&& method_exists($model, 'isVersionEnable')
&& $model->isVersionEnable()) { // 如果有版本则记录版本id
$table = strpos($model->getTable(), '.') ? $model->getTable() : $model->getConnectionName() . '.' . $model->getTable();
$relation_ids = Version::whereIn('pk', $ids)
->where('table', $table)
->selectRaw('concat_ws("-", pk, max(id)) as relation_ids, pk') // 最大版本id为当前版本id
->groupBy('pk')
->orderBy('pk', 'desc')
->get()
->pluck('relation_ids')
->toArray();
$relation_ids = json_encode($relation_ids, JSON_UNESCAPED_UNICODE);
}
// 其他记录
$detail_json = [];
if($remark) {
$detail_json['remark'] = $remark;
}
if($options) {
$detail_json += $options;
}
// 用户信息
$user_info = auth()->user() ?: (new UserService())->getUser($user_id);
$client_ip = request()->header('x-real-ip') ?? (request()->getServerParams()['remote_addr'] ?? '');
$save_data = [
'page_url' => $page_url,
'page_name' => $page_name,
'action' => $action,
'relation_ids' => $relation_ids,
'client_ip' => $client_ip,
'operator_id' => $user_info['id'] ?? 0,
'nickname' => $user_info['realname'] ?? ($user_info['username'] ?? ''),
];
// {谁}于{时间}在{页面名称}{操作:新增|编辑|删除|导入|导出}了ID为{支持多个}的记录
$now_date = Carbon::now()->toDateTimeString();
$detail_json['description'] = "{$save_data['nickname']}{$now_date}{$save_data['page_name']}{$save_data['action']}了ID为" . implode('、', $ids) . '的记录';
$detail_json = json_encode($detail_json, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$save_data['detail_json'] = $detail_json;
return (bool)OperatorLog::create($save_data);
} catch (\Exception $e) { // 如果发生错误为避免中断主程catch后记录错误信息即可
Log::get('operator_log')->error('记录通用操作日志发生错误:' . $e->getMessage() . PHP_EOL . $e->getTraceAsString());
return false;
}
}
}

View File

@@ -0,0 +1,388 @@
<?php
namespace HyperfAdmin\Admin\Service;
use FastRoute\Dispatcher;
use FastRoute\RouteCollector;
use Hyperf\HttpServer\Router\DispatcherFactory;
use Hyperf\HttpServer\Router\Handler;
use Hyperf\Utils\Str;
use HyperfAdmin\Admin\Model\CommonConfig;
use HyperfAdmin\Admin\Model\FrontRoutes;
use HyperfAdmin\Admin\Model\Role;
use HyperfAdmin\Admin\Model\RoleMenu;
use HyperfAdmin\Admin\Model\UserRole;
use HyperfAdmin\BaseUtils\Redis\Redis;
class PermissionService
{
/**
* 解析系统路由
*
* @return array
*/
public function getSystemRouteOptions()
{
$router = container(DispatcherFactory::class)->getRouter('http');
$data = $router->getData();
$options = [];
foreach($data as $routes_data) {
foreach($routes_data as $http_method => $routes) {
$route_list = [];
if(isset($routes[0]['routeMap'])) {
foreach($routes as $map) {
array_push($route_list, ...$map['routeMap']);
}
} else {
$route_list = $routes;
}
foreach($route_list as $route => $v) {
// 过滤掉脚手架页面配置方法
$callback = is_array($v) ? ($v[0]->callback) : $v->callback;
if(!is_array($callback)) {
continue;
}
$route = is_string($route) ? rtrim($route) : rtrim($v[0]->route);
$route_key = "$http_method::{$route}";
$options[] = [
'value' => $route_key,
'label' => $route_key,
];
}
}
}
return $options;
}
public function getRolePermissionValues($router_ids, $module = 'system')
{
if(empty($router_ids)) {
return [];
}
$data = [];
$routers = make(Menu::class)->tree([
'module' => $module,
'id' => $router_ids,
]);
if(!empty($routers)) {
$paths = array_keys(tree_2_paths($routers, $module));
foreach($paths as $path) {
$data[] = explode('-', $path);
}
}
return $data;
}
/**
* 构造角色权限设置options
*
* @return array
*/
public function getPermissionOptions($role_id = 0)
{
// todo 配置化
$options = [
[
'value' => 'default',
'label' => '默认',
'children' => make(Menu::class)->tree(['module' => 'default']),
],
[
'value' => 'system',
'label' => '系统',
'children' => make(Menu::class)->tree(['module' => 'system']),
],
];
$router_ids = $this->getRoleMenuIds([$role_id]);
$values = $this->getRolePermissionValues($router_ids, 'default');
$values = array_merge($values, $this->getRolePermissionValues($router_ids, 'system'));
return [$values, $options];
}
public function getAllRoleList($where = [], $fields = ['*'])
{
$model = new Role();
$roles = $model->where2query($where)
->select($fields)
->orderByRaw('pid asc, sort desc')
->get();
return $roles->toArray();
}
public function getRoleMenuIds($role_ids)
{
if(empty($role_ids)) {
return [];
}
$routes = RoleMenu::query()
->distinct(true)
->select(['router_id'])
->whereIn('role_id', $role_ids)
->get()
->toArray();
return $routes ? array_column($routes, 'router_id') : [];
}
public function getRoleTree()
{
$roles = make(Role::class)->search([
'select' => ['id as value', 'name as label', 'pid'],
'limit' => 200,
'order_by' => 'sort desc',
], [
'status' => YES,
], 'name', 'id', 'and', true);
return generate_tree($roles, 'pid', 'value');
}
public function getUserRoleIds($user_id)
{
if(!$user_id) {
return [];
}
return UserRole::query()
->select(['role_id'])
->where('user_id', $user_id)
->get()
->pluck('role_id')
->toArray();
}
public function getRoleUserIds($role_id)
{
if(!$role_id) {
return [];
}
return UserRole::query()
->select(['user_id'])
->where('role_id', $role_id)
->get()
->pluck('user_id')
->toArray();
}
public function getMenuRoleIds($menu_id)
{
if(!$menu_id) {
return [];
}
return RoleMenu::query()
->select(['role_id'])
->where('router_id', $menu_id)
->get()
->pluck('role_id')
->toArray();
}
public function getUserResource($user_id)
{
if(!$user_id) {
return [];
}
$user_role_ids = $this->getUserRoleIds($user_id);
$role_menu_ids = $this->getRoleMenuIds($user_role_ids);
$list = make(FrontRoutes::class)->where2query([
'id' => $role_menu_ids,
'type' => ['>' => 0],
'status' => YES,
])->distinct(true)->select([
'http_method',
'path',
'is_scaffold',
'permission',
'scaffold_action',
])->get()->toArray();
$resources = [];
foreach($list as $route) {
if(Str::contains($route['permission'], '::')) {
$permissions = array_filter(explode(',', $route['permission']));
foreach($permissions as $permission) {
[
$http_method,
$uri,
] = array_filter(explode('::', $permission, 2));
$resources[] = [
'http_method' => $http_method,
'uri' => $uri,
];
}
} else {
// 这段代码为兼容老的数据
$paths = array_filter(explode('/', $route['path']));
$suffix = array_pop($paths);
$prefix = implode('/', $paths);
if($suffix == 'list') {
$action_conf = config("scaffold_permissions.list.permission");
$scaffold_permissions = array_filter(explode(',', str_replace('/*/', "/{$prefix}/", $action_conf)));
foreach($scaffold_permissions as $scaffold_permission) {
[
$http_method,
$uri,
] = array_filter(explode('::', $scaffold_permission, 2));
$resources[] = [
'http_method' => $http_method,
'uri' => $uri,
];
}
}
if(empty($route['permission'])) {
continue;
}
$resources[] = [
'http_method' => FrontRoutes::$http_methods[$route['http_method']],
'uri' => $route['permission'],
];
}
}
$user_open_apis = $this->getOpenResourceList('user_open_api');
return array_merge($resources, $user_open_apis);
}
public function getOpenResourceList($field = 'open_api')
{
$open_apis = CommonConfig::query()->where([
'namespace' => 'system',
'name' => 'permissions',
])->value('value')[$field] ?? [];
$data = [];
foreach($open_apis as $route) {
[$http_method, $uri] = explode("::", $route, 2);
$data[] = compact('http_method', 'uri');
}
return $data;
}
public function getResourceDispatcher($user_id = 0, $auth_type = FrontRoutes::RESOURCE_OPEN)
{
$cache_key = $this->getPermissionCacheKey($user_id);
$options = [
'routeParser' => 'FastRoute\\RouteParser\\Std',
'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased',
'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased',
'routeCollector' => 'FastRoute\\RouteCollector',
];
if(!$dispatch_data = json_decode(Redis::get($cache_key), true)) {
/** @var RouteCollector $routeCollector */
$route_collector = new $options['routeCollector'](new $options['routeParser'], new $options['dataGenerator']);
$this->processUserResource($route_collector, $user_id, $auth_type);
$dispatch_data = $route_collector->getData();
if(!empty($dispatch_data)) {
Redis::setex($cache_key, json_encode($dispatch_data), 86400);
}
}
return new $options['dispatcher']($dispatch_data);
}
protected function processUserResource(RouteCollector $r, $user_id, $auth_type)
{
$resources = $auth_type == FrontRoutes::RESOURCE_OPEN ? $this->getOpenResourceList() : $this->getUserResource($user_id);
$route_keys = [];
foreach($resources as $resource) {
if(!$resource['uri']) {
continue;
}
$route_key = "{$resource['http_method']}::{$resource['uri']}";
if(in_array($route_key, $route_keys)) {
continue;
}
$route_keys[] = $route_key;
$r->addRoute($resource['http_method'], $resource['uri'], '');
}
}
public function hasPermission($uri, $method = 'GET')
{
$auth_service = make(AuthService::class);
// 用户为超级管理员
if($auth_service->isSupperAdmin()) {
return true;
}
$user = $auth_service->user();
$dispatcher = $this->getResourceDispatcher($user['id'] ?? 0, FrontRoutes::RESOURCE_NEED_AUTH);
$route_info = $dispatcher->dispatch($method, $uri);
return $route_info[0] === $dispatcher::FOUND;
}
public function isOpen($uri, $method)
{
$routes = container(DispatcherFactory::class)
->getDispatcher('http')
->dispatch($method, $uri);
if($routes[0] !== Dispatcher::FOUND) {
return false;
}
if($routes[1] instanceof Handler) {
if(is_string($routes[1]->callback)) {
[$controller, $action] = $this->prepareHandler($routes[1]->callback);
} else {
[$controller, $action] = [
$routes[1]->callback[0],
$routes[1]->callback[1],
];
}
} else {
[$controller, $action] = $this->prepareHandler($routes[1]);
}
$controllerInstance = container($controller);
if (in_array($action, $controllerInstance->open_resources)) {
return true;
}
// 获取开放的资源
$dispatcher = $this->getResourceDispatcher();
$route_info = $dispatcher->dispatch($method, $uri);
return $route_info[0] === $dispatcher::FOUND;
}
public function can($uri, $method)
{
if($this->isOpen($uri, $method)) {
return true;
}
if($this->hasPermission($uri, $method)) {
return true;
}
return false;
}
public function getPermissionCacheKey($user_id = 0, $force = false)
{
$cache_key = 'hyperf_admin_permission_cache:key_map';
$new_key = "hyperf_admin_permission_cache:" . md5(time() . Str::random(6));
$cache_value = !$force ? json_decode(Redis::get($cache_key), true) : [];
if(!isset($cache_value[$user_id])) {
$cache_value[$user_id] = $new_key;
Redis::set($cache_key, json_encode($cache_value));
}
return $cache_value[$user_id];
}
protected function prepareHandler($handler): array
{
if(is_string($handler)) {
if(strpos($handler, '@') !== false) {
return explode('@', $handler);
}
return explode('::', $handler);
}
if(is_array($handler) && isset($handler[0], $handler[1])) {
return $handler;
}
throw new \RuntimeException('Handler not exist.');
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace HyperfAdmin\Admin\Service;
use HyperfAdmin\Admin\Model\User;
class UserService
{
public function getUser($filter)
{
if (!is_array($filter)) {
$where = ['id' => (int)$filter];
} else {
$where = $filter;
}
$user = make(User::class)->where2query($where)->select([
'id',
'username',
'realname',
'mobile',
'email',
'status',
'is_admin',
'avatar',
'roles',
])->first();
if (!$user) {
return false;
}
return $user->toArray();
}
public function batchGetUser(array $ids, $status = YES)
{
$users = User::query()->whereIn('id', $ids)->where('status', $status)->select([
'id',
'username',
'realname',
'mobile',
'email',
'status',
'is_admin',
'avatar',
'roles',
])->get();
if (!$users) {
return false;
}
return $users->toArray();
}
public function findUserOrCreate($sso_user_info)
{
$where = empty($sso_user_info['mobile']) ? [
'username' => $sso_user_info['name'],
] : [
'username' => [$sso_user_info['name'], $sso_user_info['mobile']],
];
$user_info = $this->getUser($where);
if ($user_info) {
return $user_info;
}
$data = [
'username' => $sso_user_info['mobile'],
'mobile' => $sso_user_info['mobile'],
'avatar' => $sso_user_info['avatar'] ?? '',
'password' => '',
'status' => YES,
'realname' => $sso_user_info['name'] ?? '',
'login_time' => date("Y-m-d H:i:s"),
'login_ip' => '',
];
$user = new User($data);
$user->save();
return $user->toArray();
}
}

View File

@@ -0,0 +1,57 @@
<?php
return [
'databases' => [
'hyperf_admin' => db_complete([
'host' => env('HYPERF_ADMIN_DB_HOST'),
'database' => env('HYPERF_ADMIN_DB_NAME', 'hyperf_admin'),
'username' => env('HYPERF_ADMIN_DB_USER'),
'password' => env('HYPERF_ADMIN_DB_PWD'),
]),
],
'init_routes' => [
__DIR__ . '/routes.php'
],
'user_info_cache_prefix' => 'hyperf_admin_userinfo_',
'admin_cookie_name' => 'hyperf_admin_token',
// 脚手架预置权限
'scaffold_permissions' => [
'list' => [
'label' => '列表',
'type' => 2,
'permission' => 'GET::/*/info,GET::/*/list,GET::/*/list.json,GET::/*/childs/{id:\d+},GET::/*/act,GET::/*/notice',
],
'create' =>[
'label' => '新建',
'path' => '/form',
'type' => 1,
'permission' =>'GET::/*/form,GET::/*/form.json,POST::/*/form',
],
'edit' =>[
'label' => '编辑',
'path' => '/:id',
'type' => 1,
'permission' => 'GET::/*/{id:\d+},GET::/*/{id:\d+}.json,POST::/*/{id:\d+},GET::/*/newversion/{id:\d+}/{last_ver_id:\d+}',
],
'rowchange' =>[
'label' => '行编辑',
'type' => 2,
'permission' => 'POST::/*/rowchange/{id:\d+}'
],
'delete' => [
'label' => '删除',
'type' => 2,
'permission' => 'POST::/*/delete',
],
'import' => [
'label' => '导入',
'type' => 2,
'permission' => 'POST::/*/import',
],
'export' => [
'label' => '导出',
'type' => 2,
'permission' => 'POST::/*/export',
],
],
];

View File

@@ -0,0 +1,40 @@
<?php
use Hyperf\HttpServer\Router\Router;
use HyperfAdmin\Admin\Controller\CommonConfigController;
use HyperfAdmin\Admin\Controller\MenuController;
use HyperfAdmin\Admin\Controller\RoleController;
use HyperfAdmin\Admin\Controller\SystemController;
use HyperfAdmin\Admin\Controller\UploadController;
use HyperfAdmin\Admin\Controller\UserController;
Router::addGroup('/upload', function () {
Router::post('/image', [UploadController::class, 'image']);
Router::get('/ossprivateurl', [UploadController::class, 'privateFileUrl']);
});
register_route('/user', UserController::class, function ($controller) {
Router::get('/menu', [$controller, 'menu']);
Router::get('/roles', [$controller, 'roles']);
Router::post('/login', [$controller, 'login']);
Router::post('/logout', [$controller, 'logout']);
Router::get('/export', [$controller, 'export']);
Router::get('/exports', [$controller, 'exportTasks']);
Router::post('/exports/retry/{id:\d+}', [$controller, 'exportTasksRetry']);
Router::get('/exportLimit', [$controller, 'exportLimit']);
});
register_route('/role', RoleController::class);
register_route('/menu', MenuController::class, function ($controller) {
Router::get('/tree', [$controller, 'menuTree']);
Router::get('/getOpenApis', [$controller, 'getOpenApis']);
Router::post('/permission/clear', [$controller, 'clearPermissionCache']);
});
register_route('/cconf', CommonConfigController::class, function ($controller) {
Router::get('/detail/{key:[a-zA-Z-_0-1]+}', [$controller, 'detail']);
Router::post('/detail/{key:[a-zA-Z-_0-1]+}', [$controller, 'saveDetail']);
});
Router::get('/system/config', [SystemController::class, 'config']);

View File

@@ -0,0 +1,34 @@
<?php
use HyperfAdmin\Admin\Service\OperatorLogService;
use HyperfAdmin\Admin\Service\AuthService;
use HyperfAdmin\Admin\Service\PermissionService;
if (!function_exists('log_operator')) {
function log_operator($model, $action, $ids, $remark = '', $options = [], $user_id = 0)
{
return make(OperatorLogService::class)->write($model, $action, $ids, $remark, $options, $user_id);
}
}
if (! function_exists('auth')) {
function auth(?string $field = null)
{
$auth = container(AuthService::class);
if (is_null($field)) {
return $auth;
}
return $auth->get($field);
}
}
if (! function_exists('permission')) {
function permission(?string $uri = null, string $method = 'POST')
{
$permission = make(PermissionService::class);
if (is_null($uri)) {
return $permission;
}
return $permission->can($uri, strtoupper($method));
}
}

View File

@@ -0,0 +1,30 @@
{
"name": "rock-admin/alert-manager",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "daodao97",
"email": "daodao97@foxmail.com"
}
],
"require": {
"hyperf/async-queue": "^1.1",
"hyperf/process": "^1.1",
"rock-admin/rule-engine": "~0.0.1"
},
"autoload": {
"psr-4": {
"Rock\\AlertManager\\": "./src"
},
"files": [
"./src/func.php"
]
},
"extra": {
"hyperf": {
"config": "Rock\\AlertManager\\ConfigProvider"
}
},
"description": ""
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace HyperfAdmin\AlertManager;
use Hyperf\AsyncQueue\Job;
use HyperfAdmin\BaseUtils\Log;
use HyperfAdmin\RuleEngine\BooleanOperation;
use HyperfAdmin\RuleEngine\Context\Context;
use HyperfAdmin\RuleEngine\Context\TimeContext;
class AlertJob extends Job
{
public $params;
public function __construct($params)
{
// 这里最好是普通数据,不要使用携带 IO 的对象,比如 PDO 对象
$this->params = $params;
}
public function handle()
{
$params = $this->params;
$filter = make(AlertRules::class)->get();
$logger = Log::get('alert_manager');
if($filter) {
$context = (new Context())->register(new TimeContext())
->setCustomContext($params['extra'] ?? []);
$bo = new BooleanOperation($context);
try {
$ret = $bo->execute($filter);
} catch (\Exception $exception) {
$logger->error('rule compare filed', compact('exception', 'filter'));
}
}
if(!isset($ret) || !$ret) {
$ret = [
'alert' => true,
];
}
$params = array_overlay($ret, $params);
if(!$ret['alert']) {
return;
}
$robots = make(AlertRobots::class)->get();
if(!isset($robots[$params['robot_name']])) {
$logger->warning(sprintf('not support group [%s]', $params['robot_name']));
return;
}
$key = sprintf('alert_manager:frequency:%s:%s', $params['robot_name'], date('YmdHi'));
$params['webhook'] = $robots[$params['robot_name']]['webhook'];
$webhook = $params['webhook'] ?? '';
$type = $params['type'] ?? 'text';
$message = $params['message'] ?? '';
$receivers = $params['receivers'] ?? '';
$method = 'sendText';
switch($type) {
case 'markdown':
case 'md':
$method = 'sendMarkdown';
break;
}
$send = make(DingTalkRobot::class, ['webhook' => $webhook])->$method($message, $receivers);
if(!$send) {
$logger->error('alert_manager send fail', compact('params'));
}
}
}

View File

@@ -0,0 +1,23 @@
<?php
class AlertMessage
{
public $type;
public $message;
public $receivers;
public $webhook;
public $title;
public function __construct(array $params)
{
foreach($params as $key => $val) {
if(property_exists($this, $key)) {
$this->{$key} = $val;
}
}
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace HyperfAdmin\AlertManager;
use Hyperf\AsyncQueue\Process\ConsumerProcess;
class AlertQueueConsumer extends ConsumerProcess
{
public $queue = 'alert_manager';
public function isEnable(): bool
{
return config('alert_manager.enable', false);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace HyperfAdmin\AlertManager;
use HyperfAdmin\BaseUtils\Redis\Redis;
class AlertRobots
{
private $key = 'alert_manager:robots';
public function get()
{
$rules = Redis::get($this->key);
if($rules) {
return json_decode($rules, true);
}
return [];
}
public function set(array $value)
{
return Redis::set($this->key, json_encode($value));
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace HyperfAdmin\AlertManager;
use HyperfAdmin\BaseUtils\Redis\Redis;
class AlertRules
{
private $key = 'alert_manager:rules';
public function get()
{
$rules = Redis::get($this->key);
if($rules) {
return json_decode($rules, true);
}
return [];
}
public function set(array $rules)
{
return Redis::set($this->key, json_encode($rules));
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace HyperfAdmin\AlertManager;
use Hyperf\AsyncQueue\Driver\DriverFactory;
use Hyperf\AsyncQueue\Driver\DriverInterface;
class AlertService
{
/**
* @var DriverInterface
*/
protected $driver;
public function __construct(DriverFactory $driverFactory)
{
$this->driver = $driverFactory->get('default');
}
/**
* 生产消息.
*
* @param $params 数据
* @param int $delay 延时时间 单位秒
*
* @return bool
*/
public function push($params, int $delay = 0): bool
{
// 这里的 `ExampleJob` 会被序列化存到 Redis 中,所以内部变量最好只传入普通数据
// 同理,如果内部使用了注解 @Value 会把对应对象一起序列化,导致消息体变大。
// 所以这里也不推荐使用 `make` 方法来创建 `Job` 对象。
return $this->driver->push(new AlertJob($params), $delay);
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace HyperfAdmin\AlertManager;
class ConfigProvider
{
public function __invoke(): array
{
return [
'redis' => [
'alert_manager' => [
'host' => env('REDIS_ALERT_MANAGER_HOST', 'localhost'),
'auth' => env('REDIS_ALERT_MANAGER_AUTH', null),
'port' => (int)env('REDIS_ALERT_MANAGER_PORT', 6379),
'db' => (int)env('REDIS_ALERT_MANAGER_DB', 0),
'pool' => [
'min_connections' => 1,
'max_connections' => 10,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
'max_idle_time' => (float)env('REDIS_ALERT_MANAGER_MAX_IDLE_TIME', 60),
],
],
],
'async_queue' => [
'alert_manager' => [
'driver' => \Hyperf\AsyncQueue\Driver\RedisDriver::class,
'redis' => [
'pool' => 'alert_manager',
],
'channel' => 'alert_manager',
'timeout' => 2,
'retry_seconds' => 5,
'handle_timeout' => 10,
'processes' => 1,
'concurrent' => [
'limit' => 5,
],
],
],
'commands' => [],
'dependencies' => [],
'processes' => [
AlertQueueConsumer::class,
],
'listeners' => [],
'publish' => [],
];
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace HyperfAdmin\AlertManager;
use mysql_xdevapi\Exception;
use HyperfAdmin\BaseUtils\Guzzle;
use HyperfAdmin\BaseUtils\Log;
class DingTalkRobot implements SenderInterface
{
protected $webhook;
public function __construct(string $webhook)
{
if(!$webhook) {
throw new Exception('webhook is invalide');
}
$this->webhook = $webhook;
}
public function sendText($message, $at = 'all')
{
$params = [
'msgtype' => 'text',
'text' => [
'content' => $message,
],
];
return $this->send($params, $at);
}
public function sendMarkdown($message, $at = 'all')
{
$params = [
'msgtype' => 'markdown',
'markdown' => [
'text' => $message,
'title' => '告警信息',
],
];
return $this->send($params, $at);
}
public function send($params, $at = 'all')
{
if($at == 'all') {
$params['at'] = ['isAtAll' => true];
}
if(is_array($at)) {
$params['at'] = ['atMobiles' => $at];
}
$ret = Guzzle::post($this->webhook, $params);
if($ret['errcode'] ?? -1 !== 0) {
Log::get('alert_manager')
->error(sprintf('alert_manager send message filed'), compact('params', 'ret'));
return false;
}
return true;
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace HyperfAdmin\AlertManager;
interface SenderInterface
{
public function sendText($message, $at = 'all');
public function sendMarkdown($message, $at = 'all');
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
use Hyperf\AsyncQueue\Driver\DriverFactory;
use HyperfAdmin\AlertManager\AlertJob;
function alert_message($message)
{
return container(DriverFactory::class)->get('alert_manager')->push(new AlertJob($message));
}

View File

@@ -0,0 +1,78 @@
{
"name": "rock-admin/base-utils",
"type": "project",
"license": "MIT",
"authors": [
{
"name": "daodao97",
"email": "daodao97@foxmail.com"
}
],
"require": {
"php": ">=7.2",
"ext-json": "*",
"ext-swoole": ">=4.4",
"ext-yaml": "*",
"aliyuncs/oss-sdk-php": "^2.3",
"box/spout": "^3.1",
"hyperf/amqp": "^1.1",
"hyperf/cache": "~1.1.0",
"hyperf/command": "~1.1.0",
"hyperf/config": "~1.1.0",
"hyperf/constants": "^1.1",
"hyperf/database": "^1.1",
"hyperf/db-connection": "~1.1.0",
"hyperf/filesystem": "^1.1",
"hyperf/framework": "~1.1.0",
"hyperf/guzzle": "~1.1.0",
"hyperf/http-server": "~1.1.0",
"hyperf/logger": "~1.1.0",
"hyperf/memory": "~1.1.0",
"hyperf/metric": "^1.1",
"hyperf/nsq": "^1.1",
"hyperf/process": "~1.1.0",
"hyperf/redis": "~1.1.0",
"hyperf/snowflake": "^1.1",
"yadakhov/insert-on-duplicate-key": "^1.2",
"ext-pdo": "*"
},
"require-dev": {
"swoft/swoole-ide-helper": "^4.2",
"phpstan/phpstan": "^0.11.2",
"hyperf/devtool": "~1.1.0",
"hyperf/testing": "~1.1.0",
"daodao97/hyperf-watch": "dev-master",
"symfony/var-dumper": "^5.0"
},
"suggest": {
"ext-openssl": "Required to use HTTPS.",
"ext-json": "Required to use JSON.",
"ext-pdo": "Required to use MySQL Client.",
"ext-pdo_mysql": "Required to use MySQL Client.",
"ext-redis": "Required to use Redis Client."
},
"autoload": {
"psr-4": {
"Rock\\BaseUtils\\": "src/"
},
"files": [
"src/Helper/constants.php",
"src/Helper/common.php",
"src/Helper/array.php",
"src/Helper/system.php"
]
},
"autoload-dev": {
"psr-4": {
"HyperfTest\\": "./tests/"
}
},
"config": {
"sort-packages": false
},
"extra": {
"hyperf": {
"config": "Rock\\BaseUtils\\ConfigProvider"
}
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace HyperfAdmin\BaseUtils;
/**
* AKSK 模式鉴权
*/
class AKSK
{
private $access_key;
private $secret_key;
public function __construct($access_key, $secret_key)
{
$this->access_key = $access_key;
$this->secret_key = $secret_key;
}
public function token($method, $path, $host, $query, $content_type, $body)
{
$data = '';
if(!empty($path)) {
$data = $method . ' ' . $path;
}
if(!empty($query)) {
$data .= '?' . $query;
}
$data .= "\nHost: " . $host;
if(!empty($content_type)) {
$data .= "\nContent-Type: " . $content_type;
}
$data .= "\n\n";
if(!empty($body)) {
$data .= $body;
}
$sign = $this->sign($this->secret_key, $data);
return 'ha ' . $this->access_key . ':' . $sign;
}
private function digest($secret, $data)
{
return hash_hmac('sha1', $data, $secret, true);
}
private function sign($secret, $data)
{
return urlsafe_b64encode($this->digest($secret, $data));
}
}

View File

@@ -0,0 +1,150 @@
<?php
namespace HyperfAdmin\BaseUtils;
use OSS\OssClient;
class AliyunOSS
{
public $access_key;
public $access_key_secret;
public $endpoint;
public $bucket;
public $host;
public $cdn;
public $default_ttl = 60;
public $default_bytes = 1048576; // 默认1M
public $max_bytes = 10485760; // 最大10M
/** @var OssClient */
public $client;
const ACL_PRIVATE = 'private';
public function __construct($bucket = 'default')
{
$config = config('storager.' . $bucket);
$this->access_key = $config['access_key'];
$this->access_key_secret = $config['access_key_secret'];
$this->endpoint = $config['endpoint'];
$this->bucket = $config['bucket'];
$this->host = $config['host'];
$this->cdn = $config['cdn'];
$this->client = new OssClient($this->access_key, $this->access_key_secret, $this->endpoint);
}
/**
* @param string $object
* @param string $file 本地文件名, 包含完整路径
*
* @throws \OSS\Core\OssException
*/
public function uploadFile($object, $file, $options = [])
{
return $this->client->uploadFile($this->bucket, $object, $file, $options);
}
public function uploadPrivateFile($object, $file_path, $options = [])
{
$ok = $this->uploadFile($object, $file_path, $options);
if(!$ok) {
return false;
}
return $this->setAcl($object, self::ACL_PRIVATE);
}
public function setAcl($object, $acl = 'default')
{
if($acl !== 'default') {
return $this->client->putObjectAcl($this->bucket, $object, $acl);
}
return true;
}
public function getSignUrl($object, $timeout = 60)
{
return $this->client->signUrl($this->bucket, $object, $timeout);
}
/**
* 下载到本地文件
*
* @link https://help.aliyun.com/document_detail/88494.html
* 直接获取文件内容
* @link https://help.aliyun.com/document_detail/88495.html
*
* @param string $object
* @param string $file 本地文件名, 包含完整路径, 不传则直接获取文件内容
*
* @throws \OSS\Core\OssException
*/
public function getFile($object, $file = null)
{
$options = [];
if($file) {
$options = [
OssClient::OSS_FILE_DOWNLOAD => $file,
];
}
return $this->client->getObject($this->bucket, $object, $options);
}
private function gmtISO8601($time)
{
$dtStr = date("c", $time);
$mydatetime = new \DateTime($dtStr);
$expiration = $mydatetime->format(\DateTime::ISO8601);
$pos = strpos($expiration, '+');
$expiration = substr($expiration, 0, $pos);
return $expiration . "Z";
}
public function getPolicy($config = [])
{
$expire = $config['expire'] ?? $this->default_ttl;
$max_size = $config['max_size'] ?? $this->default_bytes;
$dir = $config['dir'] ?? '';
$end = time() + $expire;
$expiration = $this->gmtISO8601($end);
//最大文件大小.用户可以自己设置
$conditions[] = [
'content-length-range',
0,
$max_size,
];
//表示用户上传的数据,必须是以$dir开始, 不然上传会失败,这一步不是必须项,只是为了安全起见,防止用户通过policy上传到别人的目录
if($dir) {
$conditions[] = [
'starts-with',
'$key',
rtrim($dir, '/') . '/',
];
}
$base64_policy = base64_encode(json_encode([
'expiration' => $expiration,
'conditions' => $conditions,
]));
$signature = base64_encode(hash_hmac('sha1', $base64_policy, $this->access_key_secret, true));
return [
'OSSAccessKeyId' => $this->access_key,
'host' => 'http://' . $this->host,
'policy' => $base64_policy,
'Signature' => $signature,
'expire' => $end,
'dir' => $dir, //这个参数是设置用户上传指定的前缀
'cdn' => $this->cdn ? rtrim($this->cdn, '/') : '',
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace HyperfAdmin\BaseUtils;
use Monolog\Formatter\LineFormatter;
class ColorLineFormatter extends LineFormatter
{
public $level_color_map = [
'ERROR' => "\e[0;31m{log_str}\e[0m",
'INFO' => "\e[0;32m{log_str}\e[0m",
'WARNING' => "\e[0;33m{log_str}\e[0m",
];
public function format(array $record)
{
$log = parent::format($record);
return $this->logColor($record['level_name'], $log);
}
public function logColor($level_name, $log)
{
$color_format = $this->level_color_map[$level_name] ?? '{log_str}';
return str_replace('{log_str}', $log, $color_format);
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace HyperfAdmin\BaseUtils;
use Dotenv\Dotenv;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Crontab\LoggerInterface;
use Hyperf\HttpServer\Router\DispatcherFactory;
use Hyperf\Utils\Str;
use Monolog\Formatter\JsonFormatter;
use Monolog\Logger;
use HyperfAdmin\BaseUtils\Exception\HttpExceptionHandler;
use HyperfAdmin\BaseUtils\Listener\BootAppConfListener as HABootAppConfListener;
use HyperfAdmin\BaseUtils\Listener\DbQueryExecutedListener;
use HyperfAdmin\BaseUtils\Listener\FetchModeListener;
use HyperfAdmin\BaseUtils\Middleware\CorsMiddleware;
use HyperfAdmin\BaseUtils\Middleware\HttpLogMiddleware;
class ConfigProvider
{
public function __invoke(): array
{
if (is_dev()) {
$logger_default = [
'handler' => [
'class' => HAStreamHandler::class,
'constructor' => [
'level' => Logger::INFO,
'stream' => 'php://stdout',
],
],
'formatter' => [
'class' => ColorLineFormatter::class,
'constructor' => [
'format' => "%datetime%||%channel%||%level_name%||%message%||%context%||%extra%\n",
'allowInlineLineBreaks' => true,
'includeStacktraces' => true,
],
],
];
} else {
$logger_default = [
'handler' => [
'class' => RotatingFileHandler::class,
'constructor' => [
'filename' => BASE_PATH . '/runtime/logs/app.log',
'maxFiles' => 1,
'level' => Logger::INFO,
],
],
'formatter' => [
'class' => JsonFormatter::class,
'constructor' => [],
],
];
}
return [
'commands' => [],
'dependencies' => [
// 终端彩色日志
StdoutLoggerInterface::class => StdoutLoggerFactory::class,
// 处理 routes 分文件路由
DispatcherFactory::class => RoutesDispatcher::class,
],
'listeners' => [
HABootAppConfListener::class,
DbQueryExecutedListener::class,
FetchModeListener::class,
],
'annotations' => [
'scan' => [
'paths' => [
__DIR__,
],
],
],
'logger' => [
'default' => $logger_default,
],
'middlewares' => [
'http' => [
CorsMiddleware::class,
HttpLogMiddleware::class,
],
],
'redis' => [
'metric' => [
'host' => env('REDIS_ALERT_MANAGER_HOST', 'localhost'),
'auth' => env('REDIS_ALERT_MANAGER_AUTH', null),
'port' => (int)env('REDIS_ALERT_MANAGER_PORT', 6379),
'db' => (int)env('REDIS_ALERT_MANAGER_DB', 0),
'pool' => [
'min_connections' => 1,
'max_connections' => 10,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
'max_idle_time' => (float)env('REDIS_ALERT_MANAGER_MAX_IDLE_TIME', 60),
],
],
],
'init_routes' => [
__DIR__ . '/config/routes.php',
],
'exceptions' => [
'handler' => [
'http' => [
HttpExceptionHandler::class
],
],
],
'publish' => [],
];
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace HyperfAdmin\BaseUtils\Constants;
use Hyperf\Constants\AbstractConstants;
use Hyperf\Constants\Annotation\Constants;
/**
* @Constants
*/
class Consts extends AbstractConstants
{
const DEFAULT_MODEL_NAMESPACE = 'App\\Model';
const YES = 1;
const NO = 0;
const DATETIME_FORMAT = 'Y-m-d H:i:s.u';
const VERSION_MIN = '1.6.1';
const PLATFORM_WECHAT = 2;
const PLATFORM_WEB = 3;
const PLATFORM_WXA = 4;
const PLATFORM_IOS = 30;
const PLATFORM_ANDROID = 50;
const PLATFORM_QTT = 110;
public static $platforms = [
self::PLATFORM_WECHAT => 'wechat',
self::PLATFORM_WEB => 'web',
self::PLATFORM_WXA => 'mapp',
self::PLATFORM_IOS => 'ios',
self::PLATFORM_ANDROID => 'android',
self::PLATFORM_QTT => 'qtt',
];
}

View File

@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace HyperfAdmin\BaseUtils\Constants;
use Hyperf\Constants\AbstractConstants;
use Hyperf\Constants\Annotation\Constants;
/**
* @Constants
* @method static string getMessage(int $code)
*/
class ErrorCode extends AbstractConstants
{
/**
* @Message("Fail")
*/
const FAIL = -1;
/**
* @Message("ok")
*/
const CODE_SUCC = 0;
/**
* @Message("err other")
*/
const CODE_ERR_OTHER = 1;
/**
* @Message("用户不存在")
*/
const CODE_ERR_AUTH = 100;
/**
* @Message("err auth")
*/
const CODE_ERR_AUTH_USERNAME = 101;
/**
* @Message("err auth")
*/
const CODE_ERR_AUTH_PASSWORD = 102;
/**
* @Message("err auth")
*/
const CODE_ERR_AUTH_DISABLE = 103;
/**
* @Message("err auth")
*/
const CODE_ERR_AUTH_VERIFICATION_CODE = 104;
/**
* @Message("err auth")
*/
const CODE_ERR_AUTH_SECOND_VERIFICATION = 105;
/**
* @Message("err auth")
*/
const CODE_ERR_DUPLICATE = 201;
/**
* @Message("err auth")
*/
const CODE_ERR_REDIRECT = 302;
/**
* @Message("err auth")
*/
const CODE_ERR_UNAUTHORIZED = 401;
/**
* @Message("err auth")
*/
const CODE_ERR_DENY = 403;
/**
* @Message("err auth")
*/
const CODE_ERR_NOT_FOUND = 404;
/**
* @Message("err auth")
*/
const CODE_ERR_SYSTEM = 500;
/**
* @Message("err auth")
*/
const CODE_ERR_SERVER = 502;
/**
* @Message("参数错误")
*/
const CODE_ERR_PARAM = 600;
/**
* @Message("err auth")
*/
const CODE_ERR_PARAM_MISSING = 601;
/**
* @Message("err auth")
*/
const CODE_ERR_PARAM_INVALID = 602;
/**
* @Message("err auth")
*/
const CODE_ERR_PARAM_EXPIRE = 603;
/**
* @Message("err auth")
*/
const CODE_ERR_PARAM_SIGNATURE = 604;
/**
* @Message("重新登录")
*/
const CODE_LOGIN = 401100;
/**
* @Message("权限不足")
*/
const CODE_NO_AUTH = 403100;
}

View File

@@ -0,0 +1,49 @@
<?php
namespace HyperfAdmin\BaseUtils\Excel;
use Box\Spout\Common\Type;
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
class ExcelReader
{
private $sheets;
public function __construct($path)
{
$extension = get_extension($path);
switch($extension) {
case Type::CSV:
/** @var \Box\Spout\Reader\CSV\Reader $reader */ $reader = ReaderEntityFactory::createCSVReader();
break;
case Type::XLSX:
/** @var \Box\Spout\Reader\XLSX\Reader $reader */ $reader = ReaderEntityFactory::createXLSXReader();
break;
case Type::ODS:
/** @var \Box\Spout\Reader\ODS\Reader $reader */ $reader = ReaderEntityFactory::createODSReader();
break;
}
$reader->open($path);
$iterator = $reader->getSheetIterator();
/** @var \Box\Spout\Reader\XLSX\Sheet $sheet */
foreach($iterator as $sheet) {
$this->sheets[] = $sheet;
}
}
public function readLine($sheet_index = 0)
{
$sheet = $this->sheets[$sheet_index] ?? null;
if(!$sheet) {
return false;
}
foreach($sheet->getRowIterator() as $row) {
$cells = $row->getCells();
$row = [];
/** @var \Box\Spout\Common\Entity\Cell $cell */
foreach($cells as $cell) {
$row[] = $cell->getValue();
}
yield $row;
}
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace HyperfAdmin\BaseUtils\Excel;
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
use Box\Spout\Writer\Common\Creator\WriterFactory;
class ExcelWriter
{
private $writer;
public function __construct($path)
{
$extension = get_extension($path);
$this->writer = WriterFactory::createFromType($extension);
$this->writer = $this->writer->openToFile($path);
}
public function addRows($rows)
{
$rows = WriterEntityFactory::createRowFromArray($rows);
$this->writer->addRow($rows);
}
public function close()
{
$this->writer->close();;
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace HyperfAdmin\BaseUtils\Exception;
use Hyperf\Di\Annotation\Inject;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Psr\Http\Message\ResponseInterface;
use HyperfAdmin\BaseUtils\Constants\ErrorCode;
use HyperfAdmin\BaseUtils\Log;
use Throwable;
class HttpExceptionHandler extends ExceptionHandler
{
/**
* @Inject()
* @var \Hyperf\HttpServer\Contract\ResponseInterface
*/
protected $response;
public function handle(Throwable $throwable, ResponseInterface $response)
{
Log::get('http.exception')->error($throwable->getCode(), [
'trace' => (string)$throwable,
]);
if (is_production()) {
return $this->response(ErrorCode::CODE_ERR_SYSTEM, '服务器内部错误');
}
return $this->response($throwable->getCode() ?: ErrorCode::CODE_ERR_SYSTEM, (string)$throwable);
}
/**
* 抽取函数方便子类重写response结构
*
* @param int $code
* @param string $msg
*
* @return ResponseInterface
*/
protected function response($code, $msg)
{
return $this->response->json([
'code' => $code,
'msg' => $msg,
]);
}
public function isValid(Throwable $throwable): bool
{
return true;
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace HyperfAdmin\BaseUtils;
use GuzzleHttp\Client;
use Hyperf\Guzzle\ClientFactory;
class Guzzle
{
/**
* @param array $config
*
* @return Client
*/
public static function create(array $config = [])
{
// 如果在协程环境下创建,则会自动使用协程版的 Handler非协程环境下无改变
return container(ClientFactory::class)->create($config);
}
public static function get($url, $query = [], $header = [])
{
return self::request('get', $url, $query, $header);
}
public static function post($url, $params = [], $header = [])
{
return self::request('post', $url, $params, $header);
}
public static function request($method, $api, $params = [], $headers = [])
{
if(!$api) {
Log::get('api_request')->warning('api is empty');
return false;
}
$client = self::create([
'timeout' => $headers['timeout'] ?? 10.0,
]);
$method = strtoupper($method);
$options = [];
$headers['charset'] = $headers['charset'] ?? 'UTF-8';
$options['headers'] = $headers;
if($method == 'GET' && $params) {
$options['query'] = $params;
}
if($method == 'POST') {
$options['headers']['Content-Type'] = $headers['Content-Type'] ?? 'application/json';
if($options['headers']['Content-Type'] == 'application/json' && $params) {
$options['body'] = \GuzzleHttp\json_encode($params ? $params : (object)[]);
}
if($options['headers']['Content-Type'] == 'application/x-www-form-urlencoded' && $params) {
$options['form_params'] = $params;
}
}
try {
$request = $client->request($method, $api, $options);
$code = $request->getStatusCode();
$content = $request->getBody()->getContents();
$content = my_json_decode($content);
Log::get('api_request')->info($api, [
'method' => $method,
'code' => $code,
'options' => $options,
'content' => $content,
]);
return $content;
} catch (\GuzzleHttp\Exception\GuzzleException $e) {
Log::get('api_request')->error($api, [
'method' => $method,
'options' => $options,
'exception' => (string)$e,
]);
return false;
}
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace HyperfAdmin\BaseUtils;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
class HAStreamHandler extends StreamHandler
{
/**
* 修复handler写日志判断级别问题bug
*/
public function isHandling(array $record): bool
{
$level_code = Logger::toMonologLevel($record['level']);
return $level_code >= $this->level;
}
}

View File

@@ -0,0 +1,326 @@
<?php
if(!function_exists('array_group_k2k')) {
function array_group_k2k(array $items, $key1, $key2 = null)
{
$map = [];
foreach($items as $item) {
$map[$item[$key1]][] = $key2 ? $item[$key2] : $item;
}
return $map;
}
}
if(!function_exists('array_group_by')) {
function array_group_by(array $arr, $key)
{
$grouped = [];
foreach($arr as $value) {
$grouped[$value[$key]][] = $value;
}
// Recursively build a nested grouping if more parameters are supplied
// Each grouped array value is grouped according to the next sequential key
if(func_num_args() > 2) {
$args = func_get_args();
foreach($grouped as $key => $value) {
$parms = array_merge([$value], array_slice($args, 2, func_num_args()));
$grouped[$key] = call_user_func_array('array_group_by', $parms);
}
}
return $grouped;
}
}
if(!function_exists('array_node_append')) {
function array_node_append($list, $key, $append_key, $callable)
{
$kws = array_column($list, $key);
$ret = $callable($kws);
foreach($list as &$item) {
$item[$append_key] = $ret[$item[$key]] ?? '';
unset($item);
}
return $list;
}
}
if(!function_exists('array_map_recursive')) {
function array_map_recursive(callable $func, array $data)
{
$result = [];
foreach($data as $key => $val) {
$result[$key] = is_array($val) ? array_map_recursive($func, $val) : call($func, [$val]);
}
return $result;
}
}
if(!function_exists('array_copy')) {
function array_copy($arr, $keys = [])
{
if(!$keys) {
return $arr;
}
$new = [];
foreach($keys as $index => $key) {
$new_key = is_string($index) ? $index : $key;
isset($arr[$key]) && $new[$new_key] = $arr[$key];
}
return $new;
}
}
if(!function_exists('array_sort_by_key_length')) {
function array_sort_by_key_length($arr, $sort_order = SORT_DESC)
{
$keys = array_map('strlen', array_keys($arr));
array_multisort($keys, $sort_order, $arr);
return $arr;
}
}
if(!function_exists('array_sort_by_value_length')) {
function array_sort_by_value_length($arr, $sort_order = SORT_DESC)
{
$keys = array_map('strlen', $arr);
array_multisort($keys, $sort_order, $arr);
return $arr;
}
}
if(!function_exists('array_to_kv')) {
function array_to_kv($arr, $as_key_column, $as_value_column)
{
$new = [];
foreach($arr as $item) {
$new[$item[$as_key_column]] = $item[$as_value_column];
}
return $new;
}
}
if(!function_exists('array_flat')) {
/**
* 将数组递归展开至深度为1的新数组
*
* @param $arr
* @param bool $keep_key
*
* @return array|mixed
*/
function array_flat($arr, $keep_key = true)
{
$newArr = [];
if($keep_key) {
foreach($arr as $key => $item) {
if(is_array($item)) {
$newArr = $newArr + array_flat($item, true);
} else {
$newArr[$key] = $item;
}
}
} else {
foreach($arr as $item) {
if(is_array($item)) {
$newArr = array_merge($newArr, array_flat($item));
} else {
$newArr[] = $item;
}
}
}
return $newArr;
}
}
if(!function_exists('array_depth')) {
function array_depth(array $array)
{
$max_depth = 1;
foreach($array as $value) {
if(is_array($value)) {
$depth = array_depth($value) + 1;
if($depth > $max_depth) {
$max_depth = $depth;
}
}
}
return $max_depth;
}
}
if(!function_exists('array_merge_node')) {
function array_merge_node($arr, $node, $key)
{
$box = [];
foreach($arr as $each) {
$box[$each[$key] . '-'] = $each;
}
if(isset($node[0])) {
foreach($node as $n) {
$box[$n[$key] . '-'] = $n;
}
} else {
$box[$node[$key] . '-'] = $node;
}
return array_values($box);
}
}
if(!function_exists('array_change_v2k')) {
/**
* 将二维数组二维某列的key值作为一维的key
*
* @param array $arr 原始数组
* @param string $column key
*/
function array_change_v2k(&$arr, $column)
{
if(empty($arr)) {
return;
}
$new_arr = [];
foreach($arr as $val) {
$new_arr[$val[$column]] = $val;
}
$arr = $new_arr;
}
}
if(!function_exists('array_group')) {
function array_group($arr, $key)
{
$tmp = [];
foreach($arr as $item) {
$tmp[$item[$key]][] = $item;
}
return $tmp;
}
}
if(!function_exists('array_last')) {
function array_last($arr)
{
return $arr[count($arr) - 1];
}
}
if(!function_exists('array_split')) {
/**
* 将数组切割成几份
*
* @param array $arr 需要分割的数组
* @param int $chunk_count 需要分割成几份
*
* @return array
*/
function array_split(array $arr, int $chunk_count)
{
$total = count($arr);
if($chunk_count >= $total) {
return array_chunk($arr, 1);
}
$remainder = array_splice($arr, 0, $total % $chunk_count);
$chunks = array_chunk($arr, (int)($total / $chunk_count));
$i = 0;
while($remainder) {
array_push($chunks[$i++], array_shift($remainder));
}
return $chunks;
}
}
if(!function_exists('array_get_by_keys')) {
function array_get_by_keys($arr, $keys = [])
{
if(!$keys) {
return $arr;
}
$tmp = [];
foreach($keys as $key) {
$tmp[$key] = $arr[$key] ?? null;
}
return $tmp;
}
}
if(!function_exists('array_remove')) {
function array_remove($arr, $del)
{
if(($key = array_search($del, $arr)) !== false) {
unset($arr[$key]);
}
return array_merge($arr);
}
}
if(!function_exists('array_get_node')) {
function array_get_node($key, $arr = [])
{
$path = explode('.', $key);
foreach($path as $key) {
$key = trim($key);
if(empty($arr) || !isset($arr[$key])) {
return null;
}
$arr = $arr[$key];
}
return $arr;
}
}
if(!function_exists('array_remove_keys_not_in')) {
function array_remove_keys_not_in($arr, $keys)
{
return array_remove_keys($arr, array_diff(array_keys($arr), $keys));
}
}
if(!function_exists('array_remove_keys')) {
function array_remove_keys($arr, $keys)
{
foreach($keys as $key) {
unset($arr[$key]);
}
return $arr;
}
}
if(!function_exists('array_overlay')) {
/**
* 数组合并: 使用 $source 中的值覆盖 $target 中的值
*
* @param array $source
* @param array $target
*
* @return array
*/
function array_overlay($source, $target)
{
if(!is_array($source)) {
return $target;
}
foreach($source as $key => $val) {
if(!is_array($val) || !isset($target[$key]) || !is_array($target[$key])) {
$target[$key] = $val;
} else {
$target[$key] = array_overlay($val, $target[$key]);
}
}
return $target;
}
}

View File

@@ -0,0 +1,810 @@
<?php
use GuzzleHttp\Client;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Hyperf\Logger\LoggerFactory;
use Hyperf\Snowflake\IdGeneratorInterface;
use Hyperf\Utils\ApplicationContext;
use Hyperf\Utils\Arr;
if (!function_exists('generate_tree')) {
function generate_tree(array $array, $pid_key = 'pid', $id_key = 'id', $children_key = 'children', $callback = null)
{
if (!$array) {
return [];
}
//第一步 构造数据
$items = [];
foreach ($array as $value) {
if ($callback && is_callable($callback)) {
$callback($value);
}
$items[$value[$id_key]] = $value;
}
//第二部 遍历数据 生成树状结构
$tree = [];
foreach ($items as $key => $value) {
//如果pid这个节点存在
if (isset($items[$value[$pid_key]])) {
$items[$value[$pid_key]][$children_key][] = &$items[$key];
} else {
$tree[] = &$items[$key];
}
}
return $tree;
}
}
if (!function_exists('generate_checkbox_tree')) {
function generate_checkbox_tree(array $array, array $checked_arr = [], $pid_key = 'pid', $id_key = 'id', $label_key = 'label')
{
$parents = [];
//第一步 构造数据
$items = [];
foreach ($array as $value) {
$items[$value[$id_key]] = [
'label' => $value[$label_key],
'value' => $value[$id_key],
];
if ($value[$pid_key] > 0) {
$parents[$value[$id_key]] = $value[$pid_key];
} else {
$items[$value[$id_key]]['checkAll'] = false;
$items[$value[$id_key]]['isIndeterminate'] = false;
$items[$value[$id_key]]['checkList'] = in_array($value[$id_key], $checked_arr) ? [$value[$id_key]] : [];
}
}
//第二部 遍历数据 生成树状结构
$tree = [];
foreach ($items as $key => $value) {
$pid = $parents[$value['value']] ?? 0;
//如果pid这个节点存在
if (isset($items[$pid])) {
$items[$pid]['options'][] = &$items[$key];
if (in_array($key, $checked_arr)) {
$items[$pid]['checkList'][] = $key;
$items[$pid]['isIndeterminate'] = true;
if (count($items[$pid]['checkList']) - 1 == count($items[$pid]['options'])) {
$items[$pid]['checkAll'] = true;
$items[$pid]['isIndeterminate'] = false;
}
}
} else {
$tree[] = &$items[$key];
}
}
return $tree;
}
}
if (!function_exists('data_desensitization')) {
/**
* 数据脱敏
*
* @param string $string 需要脱敏值
* @param int $first_length 保留前n位
* @param int $last_length 保留后n位
* @param string $re 脱敏替代符号
*
* @return bool|string
* 例子:
* data_desensitization('18811113683', 3, 4); //188****3683
* data_desensitization('王富贵', 0, 1); //**贵
*/
function data_desensitization($string, $first_length = 0, $last_length = 0, $re = '*')
{
if (empty($string) || $first_length < 0 || $last_length < 0) {
return $string;
}
$str_length = mb_strlen($string, 'utf-8');
$first_str = mb_substr($string, 0, $first_length, 'utf-8');
$last_str = mb_substr($string, -$last_length, $last_length, 'utf-8');
if ($str_length <= 2 && $first_length > 0) {
$replace_length = $str_length - $first_length;
return $first_str . str_repeat($re, $replace_length > 0 ? $replace_length : 0);
} elseif ($str_length <= 2 && $first_length == 0) {
$replace_length = $str_length - $last_length;
return str_repeat($re, $replace_length > 0 ? $replace_length : 0) . $last_str;
} elseif ($str_length > 2) {
$replace_length = $str_length - $first_length - $last_length;
return $first_str . str_repeat("*", $replace_length > 0 ? $replace_length : 0) . $last_str;
}
if (empty($string)) {
return $string;
}
}
}
if (!function_exists('yuan2fen')) {
/**
* 转换价格到元, 保留 2 位小数
*
* @param $yuan
*
* @return string
*/
function yuan2fen($yuan)
{
return (int)round((float)$yuan * 100);
}
}
if (!function_exists('fen2yuan')) {
/**
* 转换价格到分
*
* @param $fen
*
* @return int
*/
function fen2yuan($fen)
{
return sprintf('%.2f', (int)$fen / 100);
}
}
// 请谨慎使用该函数,确保输入的目录的正确性
if (!function_exists('rmdir_recursive')) {
function rmdir_recursive($dir)
{
if (!$dir || $dir === '/' || $dir === '.') {
return false;
}
$it = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);
$it = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
foreach ($it as $file) {
if ($file->isDir()) {
rmdir($file->getPathname());
} else {
unlink($file->getPathname());
}
}
rmdir($dir);
}
}
if (!function_exists('get_img_ratio')) {
function get_img_ratio($img)
{
[$width, $height] = getimagesize($img);
return number_format($width / $height, 2);
}
}
if (!function_exists('encrypt')) {
function encrypt($txt, $key = 'mengtui')
{
$chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-=+";
//$nh = rand(0,64);
$nh = strlen($txt) % 65;
$ch = $chars[$nh];
$mdKey = md5($key . $ch);
$mdKey = substr($mdKey, $nh % 8, $nh % 8 + 7);
$txt = base64_encode($txt);
$tmp = '';
$i = 0;
$j = 0;
$k = 0;
for ($i = 0; $i < strlen($txt); $i++) {
$k = $k == strlen($mdKey) ? 0 : $k;
$j = ($nh + strpos($chars, $txt[$i]) + ord($mdKey[$k++])) % 64;
$tmp .= $chars[$j];
}
return str_replace(['+', '='], ['_', '.'], $ch . $tmp);
}
}
if (!function_exists('decrypt')) {
function decrypt($txt, $key = 'mengtui')
{
$txt = str_replace(['_', '.'], ['+', '='], $txt);
$chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-=+";
$ch = $txt[0];
$nh = strpos($chars, $ch);
$mdKey = md5($key . $ch);
$mdKey = substr($mdKey, $nh % 8, $nh % 8 + 7);
$txt = substr($txt, 1);
$tmp = '';
$i = 0;
$j = 0;
$k = 0;
for ($i = 0; $i < strlen($txt); $i++) {
$k = $k == strlen($mdKey) ? 0 : $k;
$j = strpos($chars, $txt[$i]) - $nh - ord($mdKey[$k++]);
while ($j < 0) {
$j += 64;
}
$tmp .= $chars[$j];
}
return base64_decode($tmp);
}
}
if (!function_exists('is_valid_date')) {
function is_valid_date($date_str)
{
return $date_str !== '0000-00-00 00:00:00';
}
}
if (!function_exists('str_var_replace')) {
function str_var_replace($str, $data)
{
preg_match_all('/{([\s\S]*?)}/', $str, $match);
$values = [];
$vars = [];
foreach (($match && $match[1] ? $match[1] : []) as $item) {
$vars[] = '{' . $item . '}';
$values[$item] = Arr::get($data, $item);
}
return str_replace($vars, $values, $str);
}
}
if (!function_exists('convert_memory')) {
function convert_memory($size)
{
$unit = ['b', 'kb', 'mb', 'gb', 'tb', 'pb'];
return @round($size / pow(1024, ($i = floor(log($size, 1024)))), 2) . ' ' . $unit[$i];
}
}
if (!function_exists('read_file')) {
function read_file($file)
{
$f = fopen($file, 'r');
try {
while ($line = fgets($f)) {
yield $line;
}
} finally {
is_resource($f) && fclose($f);
}
}
}
if (!function_exists('get_extension')) {
function get_extension($file)
{
return substr(strrchr($file, '.'), 1);
}
}
if (!function_exists('get_class_method_params_name')) {
function get_class_method_params_name($object, $method)
{
$ref = new \ReflectionMethod($object, $method);
$params_name = [];
foreach ($ref->getParameters() as $item) {
$params_name[] = $item->getName();
}
return $params_name;
}
}
if (!function_exists('tree_2_paths')) {
function tree_2_paths($tree, $pre_key = '', $id_key = 'value', $children_key = 'children')
{
$arr_paths = [];
foreach ($tree as $node) {
$now_key = $pre_key ? $pre_key . '-' . $node[$id_key] : $node[$id_key];
if (!empty($node['children']) && is_array($node['children'])) {
$arr = tree_2_paths($node['children'], $now_key, $id_key, $children_key);
$arr_paths = array_merge($arr_paths, $arr);
} else {
$arr_paths[$now_key] = $node[$id_key];
}
}
return $arr_paths;
}
}
if (!function_exists('getFilePath')) {
/**
* 生成csv文件的路径名,如果文件夹不存在,生成相应路径文件夹
*
* @param int $id 如marketing_task_id,user_group_id
* @param string $relative_path 生成的文件相对路径
* @param int $mode 生成的文件目录的权限
* @param string $type_name 生成的文件的类型名,作更详细的区分
* @param bool $is_file_name 是否需要返回文件名
* @param string $filename 文件名
*
* @return mixed 组装好的文件绝对路径
*/
function getFilePath($id, $relative_path = '/runtime', $mode = 0755, $type_name = '', $is_file_name = false, $filename = '')
{
$env = config('app_env');
$filename = $filename ?: sprintf('%s_%s_%s_%s.csv', date('YmdHis'), $env, $id, $type_name);
$path = BASE_PATH . $relative_path;
if ((!file_exists($path)) && (!mkdir($path, $mode, true))) {
return false;
}
if ($is_file_name) {
return ['filename' => $filename, 'path' => $path . '/' . $filename];
}
return $path . '/' . $filename;
}
}
if (!function_exists('http_build_url')) {
function http_build_url($url_arr)
{
$new_url = $url_arr['scheme'] . "://" . $url_arr['host'];
if (!empty($url_arr['port'])) {
$new_url = $new_url . ":" . $url_arr['port'];
}
$new_url = $new_url . $url_arr['path'];
if (!empty($url_arr['query'])) {
$new_url = $new_url . "?" . $url_arr['query'];
}
if (!empty($url_arr['fragment'])) {
$new_url = $new_url . "#" . $url_arr['fragment'];
}
return $new_url;
}
}
if (!function_exists('replace_url_query')) {
function replace_url_query($url, array $query)
{
$parse = parse_url($url);
parse_str($parse['query'], $p);
$parse['query'] = urldecode(http_build_query(array_merge($p, $query)));
return http_build_url($parse);
}
}
if (!function_exists('container')) {
function container(string $id = '')
{
$container = ApplicationContext::getContainer();
if (!$id) {
return $container;
}
return $container->get($id);
}
}
if (!function_exists('id_gen')) {
function id_gen()
{
return container(IdGeneratorInterface::class)->generate();
}
}
if (!function_exists('id_degen')) {
function id_degen(int $id)
{
return container(IdGeneratorInterface::class)->degenerate($id);
}
}
if (!function_exists('format_time')) {
function format_time($time)
{
$output = '';
foreach (
[
86400 => '天',
3600 => '小时',
60 => '分',
1 => '秒',
] as $key => $value
) {
if ($time >= $key) {
$output .= floor($time / $key) . $value;
}
$time %= $key;
}
return $output;
}
}
if (!function_exists('my_json_decode')) {
function my_json_decode($json, $default = [])
{
if (!$json) {
return $default;
}
$json = preg_replace('@//[^"]+?$@mui', '', $json);
$json = preg_replace('@^\s*//.*?$@mui', '', $json);
$json = $json ? @json_decode($json, true) : $default;
if (is_null($json)) {
$json = $default;
}
return $json;
}
}
if (!function_exists('is_real_array')) {
/**
* 检测是否是一个真实的类C的索引数组
*/
function is_real_array($arr)
{
if (!is_array($arr)) {
return false;
}
$n = count($arr);
for ($i = 0; $i < $n; $i++) {
if (!isset($arr[$i])) {
return false;
}
}
return true;
}
}
if (!function_exists('is_map_array')) {
/**
* 关联数组
*/
function is_map_array($arr)
{
if (!is_array($arr)) {
return false;
}
$keys = array_keys($arr);
foreach ($keys as $item) {
if (is_numeric($item)) {
return false;
}
}
return true;
}
}
if (!function_exists('get_millisecond')) {
function get_millisecond()
{
return round(microtime(true) * 1000);
}
}
if (!function_exists('is_production')) {
function is_production()
{
$env = env('ENV', '');
return $env == 'production' || $env == 'prod';
}
}
if (!function_exists('is_staging')) {
/**
* @return bool
*/
function is_staging()
{
$env = env('ENV', '');
return $env == 'staging' || $env == 'pre';
}
}
if (!function_exists('is_test')) {
function is_test()
{
return env('ENV') == 'test';
}
}
if (!function_exists('is_dev')) {
function is_dev()
{
return env('ENV') == 'dev';
}
}
if (!function_exists('generate_random_str')) {
function generate_random_str($length = 12, $prefix = '')
{
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$random_str = '';
for ($i = 0; $i < $length; $i++) {
$random_str .= $characters[rand(0, strlen($characters) - 1)];
}
return trim($prefix) . $random_str;
}
}
if (!function_exists('get_dir_filename')) {
function get_dir_filename($dir, $extension = '', $full_path = false)
{
$handler = opendir($dir);
$files = [];
while (($filename = readdir($handler)) !== false) {
$filter_extension = $extension === '' ? true : strpos($filename, $extension);
if (!($filename !== "." && $filename !== ".." && $filter_extension)) {
continue;
}
if ($full_path) {
$files[] = realpath($dir) . '/' . $filename;
} else {
$files[] = $filename;
}
}
closedir($handler);
return $files;
}
}
if (!function_exists('sp_encrypt')) {
function sp_encrypt($plaintext, $key)
{
$key = substr(sha1($key, true), 0, 16);
$iv = openssl_random_pseudo_bytes(16);
$ciphertext = openssl_encrypt($plaintext, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
$ciphertext_base64 = urlsafe_b64encode($iv . $ciphertext);
return $ciphertext_base64;
}
}
if (!function_exists('sp_decrypt')) {
function sp_decrypt($ciphertext_base64, $key)
{
$key = substr(sha1($key, true), 0, 16);
$ciphertext_dec = urlsafe_b64decode($ciphertext_base64);
$iv_size = 16;
$iv_dec = substr($ciphertext_dec, 0, $iv_size);
$ciphertext_dec = substr($ciphertext_dec, $iv_size);
$plaintext_dec = openssl_decrypt($ciphertext_dec, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv_dec);
return $plaintext_dec;
}
}
if (!function_exists('urlsafe_b64encode')) {
function urlsafe_b64encode($string)
{
$data = base64_encode($string);
$data = str_replace([
'+',
'/',
// '=',
], [
'-',
'_',
// '',
], $data);
return $data;
}
}
if (!function_exists('urlsafe_b64decode')) {
function urlsafe_b64decode($string)
{
$data = str_replace([
'-',
'_',
], [
'+',
'/',
], $string);
$mod4 = strlen($data) % 4;
if ($mod4) {
$data .= substr('====', $mod4);
}
return base64_decode($data);
}
}
if (!function_exists('csv_big_num')) {
function csv_big_num($num)
{
if (!is_numeric($num)) {
return $num;
}
return "{$num}\t";
}
}
if (!function_exists('num_zone')) {
function num_zone($num1, $num2)
{
$min = min($num1, $num2);
$max = max($num1, $num2);
return $max == $min ? $min : ($min . ' ~ ' . $max);
}
}
if (!function_exists('logger')) {
function logger()
{
return container(LoggerFactory::class);
}
}
if (!function_exists('request')) {
function request()
{
return container(RequestInterface::class);
}
}
if (!function_exists('response')) {
function response()
{
return container(ResponseInterface::class);
}
}
if (!function_exists('cookie')) {
/**
* 快捷方式,返回 request 相关 cookie
*
* @param string $key
*
* @return mixed
*/
function cookie(string $key = '')
{
if (!ApplicationContext::hasContainer()) {
return [];
}
$cookies = container(RequestInterface::class)->getCookieParams();
if (empty($key)) {
return $cookies;
}
return $cookies[$key] ?? '';
}
}
if (!function_exists('request_header')) {
/**
* 快捷方式,返回 request 相关 header
*
* @param string $key
*
* @return mixed
*/
function request_header(string $key = '')
{
if (!ApplicationContext::hasContainer()) {
return [];
}
$headers = container(RequestInterface::class)->getHeaders();
if (empty($key)) {
return $headers;
}
return $headers[$key] ?? '';
}
}
if (!function_exists('download')) {
/**
* 下载文件
*
* @param string $url
* @param string $file_path
* @param array $http_options
*
* @return bool
*/
function download($url, $file_path, $http_options = [])
{
try {
$client = make(Client::class);
$client->get($url, array_merge([
'verify' => false,
'decode_content' => false,
'timeout' => 600,
'sink' => $file_path,
], $http_options));
return file_exists($file_path);
} catch (\Throwable $exception) {
logger()->get('download')->error((string)$exception);
return false;
}
}
}
if (!function_exists('runtime_path')) {
function runtime_path($path)
{
return BASE_PATH . DIRECTORY_SEPARATOR . 'runtime' . DIRECTORY_SEPARATOR . $path;
}
}
if (!function_exists('is_cli')) {
/**
* 判断是否是命令行环境
*
* @return bool
*/
function is_cli()
{
return PHP_SAPI === 'cli';
}
}
if (!function_exists('xml2array')) {
function xml2array($xml_string, $key = '')
{
if (strpos($xml_string, '<') === false) {
return [];
}
$array = (array)@simplexml_load_string($xml_string, 'SimpleXMLElement', LIBXML_NOCDATA);
if (!$key) {
return $array;
}
return array_get_node($key, $array);
}
}
if (!function_exists('get_week_day_by_timestamp')) {
//获取星期几
function get_week_day_by_timestamp($timestamp)
{
if (!$timestamp) {
return '';
}
static $weeks = [
'天',
'一',
'二',
'三',
'四',
'五',
'六',
];
return '星期' . $weeks[date('w', $timestamp)];
}
}
if (!function_exists('now')) {
/**
* 获取当前时间
*
* @param string $format
*
* @return false|string
*/
function now($format = 'Y-m-d H:i:s')
{
return date($format);
}
}
if (!function_exists('is_json_str')) {
function is_json_str($string)
{
json_decode($string);
return (json_last_error() == JSON_ERROR_NONE);
}
}

View File

@@ -0,0 +1,6 @@
<?php
defined('DAY') ?: define('DAY', 86400);
defined('HOUR') ?: define('HOUR', 3600);
defined('MINUTE') ?: define('MINUTE', 60);
defined('YES') ?: define('YES', 1);
defined('NO') ?: define('NO', 0);

View File

@@ -0,0 +1,211 @@
<?php
use Hyperf\ExceptionHandler\Formatter\FormatterInterface;
use Hyperf\HttpServer\Router\DispatcherFactory;
use Hyperf\HttpServer\Router\Router;
use Hyperf\Server\ServerFactory;
use Hyperf\Utils\Str;
use OSS\Core\OssException;
use HyperfAdmin\BaseUtils\AliyunOSS;
use HyperfAdmin\BaseUtils\Guzzle;
use HyperfAdmin\BaseUtils\Log;
if (file_exists(BASE_PATH . '/.env')) {
Dotenv\Dotenv::create([BASE_PATH])->load();
}
if(!function_exists('server')) {
function server()
{
return container(ServerFactory::class);
}
}
if(!function_exists('swoole_server')) {
/**
* @return \Swoole\Server
*/
function swoole_server()
{
return server()->getServer()->getServer();
}
}
if(!function_exists('dispatcher')) {
/**
* @param $server_name
*
* @return \FastRoute\Dispatcher
*/
function dispatcher(string $server_name = 'http')
{
return container(DispatcherFactory::class)->getDispatcher($server_name);
}
}
if(!function_exists('register_route')) {
function register_route($prefix, $controller, $callable = null)
{
Router::addGroup($prefix, function () use ($controller, $callable) {
Router::get('/list.json', [$controller, 'info']);
Router::get('/form.json', [$controller, 'form']);
Router::get('/{id:\d+}.json', [$controller, 'edit']);
Router::get('/info', [$controller, 'info']);
Router::get('/form', [$controller, 'form']);
Router::get('/{id:\d+}', [$controller, 'edit']);
Router::get('/list', [$controller, 'list']);
Router::post('/form', [$controller, 'save']);
Router::post('/delete', [$controller, 'delete']);
Router::post('/batchdel', [$controller, 'batchDelete']);
Router::post('/{id:\d+}', [$controller, 'save']);
Router::post('/rowchange/{id:\d+}', [$controller, 'rowChange']);
Router::get('/childs/{id:\d+}', [$controller, 'getTreeNodeChilds']);
Router::get('/newversion/{id:\d+}/{last_ver_id:\d+}', [
$controller,
'newVersion',
]);
Router::post('/export', [$controller, 'export']);
Router::get('/act', [$controller, 'act']);
Router::post('/import', [$controller, 'import']);
is_callable($callable) && $callable($controller);
});
}
}
if(!function_exists('move_local_file_to_oss')) {
function move_local_file_to_oss($local_file_path, $oss_file_path, $private = false, $bucket = 'aliyuncs')
{
/** @var AliyunOSS $oss */
$oss = make(AliyunOSS::class, ['bucket' => $bucket]);
try {
$method = $private ? 'uploadPrivateFile' : 'uploadFile';
$oss->$method($oss_file_path, $local_file_path);
$file_path = config('storager.aliyuncs.cdn') . '/' . $oss_file_path;
if($private) {
$file_path = oss_private_url($oss_file_path, MINUTE * 5, $bucket);
}
return [
'file_path' => $file_path,
'path' => 'oss/' . $oss_file_path,
];
} catch (OssException $exception) {
Log::get('move_local_file_to_oss')->error($exception->getMessage());
return false;
}
}
}
if(!function_exists('oss_private_url')) {
function oss_private_url($oss_file_path, $timeout = 60, $bucket = 'aliyuncs')
{
/** @var AliyunOSS $oss */
$oss = make(AliyunOSS::class, ['bucket' => $bucket]);
$key = preg_replace('@^oss/@', '', $oss_file_path);
try {
return str_replace('-internal', '', $oss->getSignUrl($key, $timeout));
} catch (OssException $exception) {
Log::get('oss_private_url')->error($exception->getMessage());
return false;
}
}
}
if(!function_exists('call_self_api')) {
function call_self_api($api, $params = [], $method = 'GET')
{
$headers = [
'X-Real-IP' => '127.0.0.1',
];
$info = Guzzle::request($method, "http://127.0.0.1:" . config('server.servers.0.port') . $api, $params, $headers);
return $info['payload'] ?? [];
}
}
if(!function_exists('select_options')) {
function select_options($api, array $kws)
{
$ret = [];
$chunk = array_chunk($kws, 100);
foreach($chunk as $part) {
$ret = array_merge($ret, call_self_api($api, ['kw' => implode(',', $part)]));
}
return $ret;
}
}
if(!function_exists('process_list_filter')) {
function process_list_filter($processes, $rule)
{
if(!$rule) {
return $processes;
}
if($ignore = $rule['ignore'] ?? false) {
if(is_string($ignore) && $ignore === 'all') {
$processes = [];
}
if(is_array($ignore)) {
$processes = array_filter($processes, function ($item) use ($ignore) {
return !Str::startsWith($item, array_map(function ($each) {
return Str::replaceLast('*', '', $each);
}, $ignore));
});
}
}
if($active = $rule['active'] ?? []) {
$processes = array_merge($processes, $active);
}
return $processes;
}
}
if(!function_exists('get_sub_dir')) {
function get_sub_dir($dir, $exclude = [])
{
$paths = [];
$dirs = \Symfony\Component\Finder\Finder::create()
->in($dir)
->depth('<1')
->exclude((array)$exclude)
->directories();
/** @var SplFileInfo $dir */
foreach($dirs as $dir) {
$paths[] = $dir->getRealPath();
}
return $paths;
}
}
if(!function_exists('db_complete')) {
function db_complete(array $conf)
{
return array_overlay($conf, [
'port' => 3306,
'driver' => 'mysql',
'options' => [
PDO::ATTR_STRINGIFY_FETCHES => false,
],
'pool' => [
'min_connections' => 1,
'max_connections' => 20,
'connect_timeout' => 10.0,
'wait_timeout' => 100,
'heartbeat' => -1,
'max_idle_time' => (float)env('DB_MAX_IDLE_TIME', 60),
],
]);
}
}
if (!function_exists('format_exception')) {
function format_exception($throwable)
{
return make(FormatterInterface::class)->format($throwable);
}
}

132
src/base-utils/src/JWT.php Normal file
View File

@@ -0,0 +1,132 @@
<?php
/**
* JWT
*/
namespace HyperfAdmin\BaseUtils;
class JWT
{
//头部
private static $header = [
'alg' => 'HS256', //生成signature的算法
'typ' => 'JWT', //类型
];
//使用HMAC生成信息摘要时所使用的密钥
private static $key = 'ha-jwt';
/**
* 获取jwt token
*
* @param array $payload jwt载荷 格式如下非必须
* [
* 'iss'=>'jwt_admin', //该JWT的签发者
* 'iat'=>time(), //签发时间
* 'exp'=>time()+7200, //过期时间
* 'nbf'=>time()+60, //该时间之前不接收处理该Token
* 'sub'=>'www.xxx.com', //面向的用户
* 'jti'=>md5(uniqid('JWT').time()) //该Token唯一标识
* ]
*
* @return bool|string
*/
public static function token(array $payload)
{
if(!is_array($payload)) {
return false;
}
$base64_header = self::base64UrlEncode(json_encode(self::$header, JSON_UNESCAPED_UNICODE));
$base64_payload = self::base64UrlEncode(json_encode($payload, JSON_UNESCAPED_UNICODE));
$token = $base64_header . '.' . $base64_payload . '.' . self::signature($base64_header . '.' . $base64_payload, self::$key, self::$header['alg']);
return $token;
}
/**
* 验证token是否有效,默认验证exp,nbf,iat时间
*
* @param string $token 需要验证的token
*
* @return bool|string
*/
public static function verifyToken(string $token)
{
$tokens = explode('.', $token);
if(count($tokens) != 3) {
return false;
}
[$base64_header, $base64_payload, $sign] = $tokens;
//获取jwt算法
$base64_decode_header = json_decode(self::base64UrlDecode($base64_header), true);
if(empty($base64_decode_header['alg'])) {
return false;
}
//签名验证
if(self::signature($base64_header . '.' . $base64_payload, self::$key, $base64_decode_header['alg']) !== $sign) {
return false;
}
$payload = json_decode(self::base64UrlDecode($base64_payload), true);
//签发时间大于当前服务器时间验证失败
if(isset($payload['iat']) && $payload['iat'] > time()) {
return false;
}
//过期时间小于当前服务器时间验证失败
if(isset($payload['exp']) && $payload['exp'] < time()) {
return false;
}
//该nbf时间之前不接收处理该Token
if(isset($payload['nbf']) && $payload['nbf'] > time()) {
return false;
}
return $payload;
}
/**
* base64UrlEncode https://jwt.io/ 中base64UrlEncode编码实现
*
* @param string $input 需要编码的字符串
*
* @return string
*/
private static function base64UrlEncode(string $input)
{
return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
}
/**
* base64UrlEncode https://jwt.io/ 中base64UrlEncode解码实现
*
* @param string $input 需要解码的字符串
*
* @return bool|string
*/
private static function base64UrlDecode(string $input)
{
$remainder = strlen($input) % 4;
if($remainder) {
$add_len = 4 - $remainder;
$input .= str_repeat('=', $add_len);
}
return base64_decode(strtr($input, '-_', '+/'));
}
/**
* HMACSHA256签名 https://jwt.io/ 中HMACSHA256签名实现
*
* @param string $input 为base64UrlEncode(header).".".base64UrlEncode(payload)
* @param string $key
* @param string $alg 算法方式
*
* @return mixed
*/
private static function signature(string $input, string $key, string $alg = 'HS256')
{
$alg_config = [
'HS256' => 'sha256',
];
return self::base64UrlEncode(hash_hmac($alg_config[$alg], $input, $key, true));
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace HyperfAdmin\BaseUtils\Listener;
use Hyperf\Database\Model\Builder;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Framework\Event\BootApplication;
use HyperfAdmin\BaseUtils\Log;
class BootAppConfListener implements ListenerInterface
{
public function listen(): array
{
return [
BootApplication::class,
];
}
public function process(object $event)
{
Builder::macro('getAsArray', function () {
/** @var \Hyperf\Database\Query\Builder $this */
$ret = $this->get();
return $ret ? $ret->toArray() : [];
});
Builder::macro('firstAsArray', function () {
/** @var \Hyperf\Database\Query\Builder $this */
$ret = $this->first();
return $ret ? $ret->toArray() : [];
});
set_error_handler(function ($level, $message, $file = '', $line = 0, $context = []) {
if(error_reporting() & $level) {
$exception = new \ErrorException($message, 0, $level, $file, $line);
Log::get('php_output_error')->error((string)$exception);
}
});
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace HyperfAdmin\BaseUtils\Listener;
use Hyperf\Database\Events\QueryExecuted;
use Hyperf\Event\Contract\ListenerInterface;
use HyperfAdmin\BaseUtils\Log;
class DbQueryExecutedListener implements ListenerInterface
{
public function listen(): array
{
return [
QueryExecuted::class,
];
}
/**
* @param object $event
*/
public function process(object $event)
{
if($event instanceof QueryExecuted) {
if(is_production()) {
$sql = $event->sql;
} else {
$sql_with_placeholders = str_replace(['%', '?'], [
'%%',
'%s',
], $event->sql);
$bindings = $event->connection->prepareBindings($event->bindings);
$pdo = $event->connection->getPdo();
$sql = vsprintf($sql_with_placeholders, array_map([
$pdo,
'quote',
], $bindings));
}
// 获取sql类型
$sql_type = $this->getSqlType($sql);
$name_map = [
'select' => function ($sql_str) {
preg_match('/from\s+(\w+)\s?.*/', $sql_str, $match);
return $match[1] ?? '';
},
'update' => function ($sql_str) {
preg_match('/update\s+(\w+)\s.*/', $sql_str, $match);
return $match[1] ?? '';
},
'delete' => function ($sql_str) {
preg_match('/delete\s+(\w+)\s.*/', $sql_str, $match);
return $match[1] ?? '';
},
'insert' => function ($sql_str) {
preg_match('/insert into\s+(\w+)\s.*/', $sql_str, $match);
return $match[1] ?? '';
},
];
$table_name = isset($name_map[$sql_type]) ? $name_map[$sql_type](strtolower(str_replace('`', '', $sql))) : '';
Log::get('sql')->info($event->connectionName, [
'database' => $event->connection->getDatabaseName(),
'type' => $sql_type,
'table' => $table_name,
'use_time' => $event->time,
'sql' => $sql,
]);
}
}
/**
* 获取sql类型
*
* @param string $sql
*
* @return string
*/
protected function getSqlType(string $sql): string
{
$type_map = [
'select' => 'select',
'update' => 'update',
'delete' => 'delete',
'insert' => 'insert into',
// more...
];
$sql_type = strtolower(explode(' ', trim($sql))[0] ?? '');
if(isset($type_map[$sql_type])) {
return (string)$sql_type;
}
// 下面原来逻辑不变
foreach($type_map as $type => $ident) {
if(stripos($sql, $ident) !== false) {
$sql_type = $type;
break;
}
}
return (string)$sql_type;
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace HyperfAdmin\BaseUtils\Listener;
use Hyperf\Database\Events\StatementPrepared;
use Hyperf\Event\Contract\ListenerInterface;
use PDO;
class FetchModeListener implements ListenerInterface
{
public function listen(): array
{
return [
StatementPrepared::class,
];
}
public function process(object $event)
{
if($event instanceof StatementPrepared) {
$event->statement->setFetchMode(PDO::FETCH_ASSOC);
}
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace HyperfAdmin\BaseUtils;
use Hyperf\Logger\LoggerFactory;
class Log
{
public static function get(string $name, $group = 'default')
{
return container(LoggerFactory::class)->get($name, $group);
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace HyperfAdmin\BaseUtils\Middleware;
use Hyperf\Utils\Context;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class CorsMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = Context::get(ResponseInterface::class);
$response = $response->withHeader('Access-Control-Allow-Origin', config('cors.origin', $request->getHeader('origin')[0] ?? '*'))
->withHeader('Access-Control-Allow-Credentials', 'true')
->withHeader('Access-Control-Allow-Headers', config('cors.allow_headers', 'Origin,X-Requested-With,Content-Type,Accept,X-HTTP-Method-Override,Cookie,X-Real-Ip'));
Context::set(ResponseInterface::class, $response);
if ($request->getMethod() == 'OPTIONS') {
return $response;
}
return $handler->handle($request);
}
}

Some files were not shown because too many files have changed in this diff Show More