mirror of
http://124.126.16.154:8888/singularity/hyperf-admin.git
synced 2026-01-15 01:55:06 +08:00
feat: hyperf-admin init
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.idea
|
||||
composer.lock
|
||||
/vendor/
|
||||
22
README.md
Normal file
22
README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
`HyperfAdmin`是前后端分离的后台管理系统, 前端基于`vue`的 `vue-admin-template`, 针对后台业务`列表`, `表单`等场景封装了大量业务组件, 后端基于`hyperf`实现, 整体思路是后端定义页面渲染规则, 前端页面渲染时首先拉取配置, 然后组件根据具体配置完成页面渲染, 方便开发者仅做少量的配置工作就能完成常见的`CRUD`工作, 同时支持自定义组件和自定义页面, 以开发更为复杂的页面.
|
||||
|
||||
[详细文档](https://hyperf-admin.github.io/)
|
||||
|
||||

|
||||
|
||||
前端为`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
102
composer.json
Normal 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
0
docs/.nojekyll
Normal file
34
docs/README.md
Normal file
34
docs/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
`hyperf-admin`是前后端分离的后台管理系统, 前端基于`vue`的 `vue-admin-template`, 针对后台业务`列表`, `表单`等场景封装了大量业务组件, 后端基于`hyperf`实现, 整体思路是后端定义页面渲染规则, 前端页面渲染时首先拉取配置, 然后组件根据具体配置完成页面渲染, 方便开发者仅做少量的配置工作就能完成常见的`CRUD`工作, 同时支持自定义组件和自定义页面, 以开发更为复杂的页面.
|
||||
|
||||

|
||||
|
||||
前端为`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
12
docs/_coverpage.md
Normal file
@@ -0,0 +1,12 @@
|
||||

|
||||
|
||||
# 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
0
docs/_navbar.md
Normal file
21
docs/_sidebar.md
Normal file
21
docs/_sidebar.md
Normal 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)
|
||||
|
||||
38
docs/backend/common-config.md
Normal file
38
docs/backend/common-config.md
Normal file
@@ -0,0 +1,38 @@
|
||||
通用配置在只需要使用表单搜集信息, 没有过多业务逻辑和校验规则时使用, 可以无需开发, 完成表单的定义和使用
|
||||
|
||||
1. 定义表单 http://localhost:9528/system/#/cconf/list, 此处表单规则同控制器中的form定义
|
||||
|
||||

|
||||
|
||||
2. 投放表单, 创建 `/***/cconf_{1中的表单名称}` 即可通过该路由访问
|
||||
|
||||

|
||||
|
||||
3. 访问 http://localhost:9528/hyperf/#/lucky/cconf_lucky_round
|
||||
|
||||

|
||||
|
||||
数据存储位置`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`获取相应的提交数据
|
||||
7
docs/backend/components/cron-center.md
Normal file
7
docs/backend/components/cron-center.md
Normal 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)
|
||||
97
docs/backend/components/data-focus.md
Normal file
97
docs/backend/components/data-focus.md
Normal 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
|
||||
11
docs/backend/components/dev-tools.md
Normal file
11
docs/backend/components/dev-tools.md
Normal file
@@ -0,0 +1,11 @@
|
||||
## 代码自动生成工具
|
||||
|
||||
针对 数据库, `Model`, `Controller` 通用模型, 可以使用后台提供的`代码生成工具`来初始化大部分代码.
|
||||
|
||||

|
||||
|
||||
连接池对应`config.autoload.databases` 中配置的可用链接, 选择好`连接池`, `数据库`, `表` 后下方表单会根据表结构字段渲染, 完成具体字段的配置, 点击提交.
|
||||
|
||||

|
||||
|
||||
相应的`Modle`, `Controller` 便已创建成功.
|
||||
430
docs/backend/form.md
Normal file
430
docs/backend/form.md
Normal 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",
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
示例效果:
|
||||
|
||||

|
||||
|
||||
### 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,
|
||||
],
|
||||
],
|
||||
],
|
||||
```
|
||||
|
||||
示例效果:
|
||||
|
||||

|
||||
|
||||
### 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
78
docs/backend/functions.md
Normal 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
571
docs/backend/list.md
Normal file
@@ -0,0 +1,571 @@
|
||||

|
||||
|
||||
```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"></i>
|
||||
|
||||
### image
|
||||
|
||||
**渲染条件**: `'type' => 'image'`,数据为数组时可以渲染多张图片
|
||||
|
||||
**效果展示**:
|
||||
|
||||
#### extrude
|
||||
|
||||
**用法**:
|
||||
|
||||
```php
|
||||
[
|
||||
"field" => "field_1",
|
||||
"type" => 'extrude',
|
||||
"render" => function($field_value, $row) {
|
||||
// return "<优惠券|balck|yellow>*****"; 单个
|
||||
// or 支持多个
|
||||
return [
|
||||
"<优惠券{replace_field}|balck|yellow>*****{replace_field}"
|
||||
]; // 格式 <文字|背景色|文字色>, 支持前端变量替换
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**效果展示**:
|
||||
|
||||
### tag
|
||||
|
||||
**用法**:
|
||||
|
||||
```php
|
||||
[
|
||||
"field" => "field_1",
|
||||
"options" => [
|
||||
0 => '禁用',
|
||||
1 => '启用',
|
||||
],
|
||||
"enum" => [ // tag 样式, 参见 https://element.eleme.io/#/zh-CN/component/tag
|
||||
0 => 'info',
|
||||
1 => 'success'
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
**效果展示**:
|
||||
|
||||
### 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"
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
**效果展示**:
|
||||
|
||||
### 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]
|
||||
]
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
**效果展示**:
|
||||
|
||||
### progress
|
||||
|
||||
**用法**:
|
||||
|
||||
```php
|
||||
[
|
||||
'field' => 'sync_progress',
|
||||
'title' => '同步进度',
|
||||
'type' => 'progress',
|
||||
// 'props' => []
|
||||
]
|
||||
```
|
||||
|
||||
> props里是progress组件的属性配置,请参考[Progress 进度条](https://element.eleme.cn/#/zh-CN/component/progress#attributes)
|
||||
|
||||
**效果展示**:
|
||||
|
||||
|
||||
## 搜索项
|
||||
|
||||
```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],
|
||||
],
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
**效果展示**:
|
||||
|
||||
## 操作按钮
|
||||
|
||||
支持 列操作按钮, 批量操作按钮, 列表页顶部按钮,前端组件为`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
408
docs/backend/scaffold.md
Normal 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;}
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
```
|
||||
132
docs/backend/super-button.md
Normal file
132
docs/backend/super-button.md
Normal 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
BIN
docs/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
1
docs/frontend/chart.md
Normal file
1
docs/frontend/chart.md
Normal file
@@ -0,0 +1 @@
|
||||
图表
|
||||
1
docs/frontend/form.md
Normal file
1
docs/frontend/form.md
Normal file
@@ -0,0 +1 @@
|
||||
表单
|
||||
1
docs/frontend/list.md
Normal file
1
docs/frontend/list.md
Normal file
@@ -0,0 +1 @@
|
||||
图表
|
||||
37
docs/guide/desc.md
Normal file
37
docs/guide/desc.md
Normal file
@@ -0,0 +1,37 @@
|
||||
`hyperf-admin`是前后端分离的后台管理系统, 前端基于`vue`的 `vue-admin-template`, 针对后台业务`列表`, `表单`等场景封装了大量业务组件, 后端基于`hyperf`实现, 整体思路是后端定义页面渲染规则, 前端页面渲染时首先拉取配置, 然后组件根据具体配置完成页面渲染, 方便开发者仅做少量的配置工作就能完成常见的`CRUD`工作, 同时支持自定义组件和自定义页面, 以开发更为复杂的页面.
|
||||
|
||||
### 架构
|
||||
|
||||

|
||||
|
||||
前端为`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
119
docs/guide/dev_example.md
Normal 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`
|
||||
|
||||

|
||||
|
||||
选择好相应的表后, 点击提交, 此时工具已经帮我们创建好相应的`app/Controller/StudentScoreController.php`和`app/Model/Test/StudentScore.php`
|
||||
|
||||
3. 添加目录和菜单
|
||||
|
||||

|
||||
|
||||
注册路由
|
||||
|
||||
```php
|
||||
// config/routes.php
|
||||
register_route('/student_score', StudentScoreController::class);
|
||||
```
|
||||
|
||||
此时我们也已经完成了基础的`CRUD`开发
|
||||
|
||||

|
||||
|
||||
哦对了, 还有各种筛选条件呢? 也很简单, 在 `scaffoldOptions` 中增加 `filter`配置即可
|
||||
|
||||
```php
|
||||
public function scaffoldOptions()
|
||||
{
|
||||
return [
|
||||
'filter' => [
|
||||
'grade', 'class', 'subject', 'name%',
|
||||
'score|分数' => [
|
||||
'type' => 'input-range',
|
||||
'select_type' => 'between'
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
还有, 大家别忘了, 需求中还要去可以按页签显示, 改怎么办呢, 这个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',
|
||||
],
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
至此我们已经完成了绝大部分的功能开发, 如果使用熟练, 我们应该能在十分钟内完成整个功能的前后端开发, 而且还支持复杂的前端效果.
|
||||
|
||||
?> 当然`hyperf-admin`还支持更多复杂的功能, 快快用你明亮的眼睛去发现他吧.
|
||||
39
docs/guide/install.md
Normal file
39
docs/guide/install.md
Normal 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
63
docs/index.html
Normal 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 + ') '
|
||||
|
||||
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
BIN
docs/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
83
docs/ws.js
Normal file
83
docs/ws.js
Normal 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 there’s 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
3
src/admin/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.idea
|
||||
composer.lock
|
||||
vendor
|
||||
31
src/admin/composer.json
Normal file
31
src/admin/composer.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/admin/src/ConfigProvider.php
Normal file
36
src/admin/src/ConfigProvider.php
Normal 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
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
83
src/admin/src/Controller/AdminAbstractController.php
Normal file
83
src/admin/src/Controller/AdminAbstractController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
126
src/admin/src/Controller/CommonConfigController.php
Normal file
126
src/admin/src/Controller/CommonConfigController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
147
src/admin/src/Controller/LogController.php
Normal file
147
src/admin/src/Controller/LogController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
572
src/admin/src/Controller/MenuController.php
Normal file
572
src/admin/src/Controller/MenuController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
198
src/admin/src/Controller/RoleController.php
Normal file
198
src/admin/src/Controller/RoleController.php
Normal 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;
|
||||
}
|
||||
}
|
||||
29
src/admin/src/Controller/SystemController.php
Normal file
29
src/admin/src/Controller/SystemController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
58
src/admin/src/Controller/UploadController.php
Normal file
58
src/admin/src/Controller/UploadController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
325
src/admin/src/Controller/UserController.php
Normal file
325
src/admin/src/Controller/UserController.php
Normal 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
|
||||
]);
|
||||
}
|
||||
}
|
||||
33
src/admin/src/Crontab/ExportTask.php
Normal file
33
src/admin/src/Crontab/ExportTask.php
Normal 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;
|
||||
}
|
||||
}
|
||||
29
src/admin/src/Install/InstallCommand.php
Normal file
29
src/admin/src/Install/InstallCommand.php
Normal 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');
|
||||
}
|
||||
}
|
||||
44
src/admin/src/Install/UpdateCommand.php
Normal file
44
src/admin/src/Install/UpdateCommand.php
Normal 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');
|
||||
|
||||
}
|
||||
}
|
||||
141
src/admin/src/Install/install.sql
Normal file
141
src/admin/src/Install/install.sql
Normal 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 '是否启用表单:0,否;1,是',
|
||||
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='通用操作日志';
|
||||
54
src/admin/src/Middleware/AuthMiddleware.php
Normal file
54
src/admin/src/Middleware/AuthMiddleware.php
Normal 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);
|
||||
}
|
||||
}
|
||||
147
src/admin/src/Middleware/PermissionMiddleware.php
Normal file
147
src/admin/src/Middleware/PermissionMiddleware.php
Normal 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;
|
||||
}
|
||||
}
|
||||
48
src/admin/src/Model/CommonConfig.php
Normal file
48
src/admin/src/Model/CommonConfig.php
Normal 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 => '是',
|
||||
];
|
||||
}
|
||||
54
src/admin/src/Model/ExportTasks.php
Normal file
54
src/admin/src/Model/ExportTasks.php
Normal 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; // 导出最大条数
|
||||
}
|
||||
102
src/admin/src/Model/FrontRoutes.php
Normal file
102
src/admin/src/Model/FrontRoutes.php
Normal 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);
|
||||
}
|
||||
}
|
||||
96
src/admin/src/Model/GlobalConfig.php
Normal file
96
src/admin/src/Model/GlobalConfig.php
Normal 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);
|
||||
}
|
||||
}
|
||||
23
src/admin/src/Model/OperatorLog.php
Normal file
23
src/admin/src/Model/OperatorLog.php
Normal 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', // 客户端地址
|
||||
];
|
||||
}
|
||||
43
src/admin/src/Model/RequestLog.php
Normal file
43
src/admin/src/Model/RequestLog.php
Normal 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();
|
||||
}
|
||||
}
|
||||
49
src/admin/src/Model/Role.php
Normal file
49
src/admin/src/Model/Role.php
Normal 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');
|
||||
}
|
||||
}
|
||||
28
src/admin/src/Model/RoleMenu.php
Normal file
28
src/admin/src/Model/RoleMenu.php
Normal 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',
|
||||
];
|
||||
}
|
||||
85
src/admin/src/Model/User.php
Normal file
85
src/admin/src/Model/User.php
Normal 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;
|
||||
}
|
||||
}
|
||||
29
src/admin/src/Model/UserRole.php
Normal file
29
src/admin/src/Model/UserRole.php
Normal 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',
|
||||
];
|
||||
}
|
||||
76
src/admin/src/Model/Version.php
Normal file
76
src/admin/src/Model/Version.php
Normal 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;
|
||||
}
|
||||
}
|
||||
195
src/admin/src/Model/Versionable.php
Normal file
195
src/admin/src/Model/Versionable.php
Normal 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();
|
||||
}
|
||||
}
|
||||
70
src/admin/src/Service/AuthService.php
Normal file
70
src/admin/src/Service/AuthService.php
Normal 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);
|
||||
}
|
||||
}
|
||||
49
src/admin/src/Service/CommonConfig.php
Normal file
49
src/admin/src/Service/CommonConfig.php
Normal 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 ?: [];
|
||||
}
|
||||
}
|
||||
197
src/admin/src/Service/ExportService.php
Normal file
197
src/admin/src/Service/ExportService.php
Normal 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();
|
||||
}
|
||||
}
|
||||
67
src/admin/src/Service/GlobalConfig.php
Normal file
67
src/admin/src/Service/GlobalConfig.php
Normal 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');
|
||||
}
|
||||
}
|
||||
95
src/admin/src/Service/Menu.php
Normal file
95
src/admin/src/Service/Menu.php
Normal 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);
|
||||
}
|
||||
}
|
||||
92
src/admin/src/Service/OperatorLogService.php
Normal file
92
src/admin/src/Service/OperatorLogService.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
388
src/admin/src/Service/PermissionService.php
Normal file
388
src/admin/src/Service/PermissionService.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
79
src/admin/src/Service/UserService.php
Normal file
79
src/admin/src/Service/UserService.php
Normal 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();
|
||||
}
|
||||
}
|
||||
57
src/admin/src/config/config.php
Normal file
57
src/admin/src/config/config.php
Normal 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',
|
||||
],
|
||||
],
|
||||
];
|
||||
40
src/admin/src/config/routes.php
Normal file
40
src/admin/src/config/routes.php
Normal 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']);
|
||||
34
src/admin/src/funcs/common.php
Normal file
34
src/admin/src/funcs/common.php
Normal 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));
|
||||
}
|
||||
}
|
||||
30
src/alert-manager/composer.json
Normal file
30
src/alert-manager/composer.json
Normal 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": ""
|
||||
}
|
||||
77
src/alert-manager/src/AlertJob.php
Normal file
77
src/alert-manager/src/AlertJob.php
Normal 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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
23
src/alert-manager/src/AlertMessage.php
Normal file
23
src/alert-manager/src/AlertMessage.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/alert-manager/src/AlertQueueConsumer.php
Normal file
15
src/alert-manager/src/AlertQueueConsumer.php
Normal 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);
|
||||
}
|
||||
}
|
||||
24
src/alert-manager/src/AlertRobots.php
Normal file
24
src/alert-manager/src/AlertRobots.php
Normal 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));
|
||||
}
|
||||
}
|
||||
24
src/alert-manager/src/AlertRules.php
Normal file
24
src/alert-manager/src/AlertRules.php
Normal 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));
|
||||
}
|
||||
}
|
||||
35
src/alert-manager/src/AlertService.php
Normal file
35
src/alert-manager/src/AlertService.php
Normal 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);
|
||||
}
|
||||
}
|
||||
51
src/alert-manager/src/ConfigProvider.php
Normal file
51
src/alert-manager/src/ConfigProvider.php
Normal 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' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
63
src/alert-manager/src/DingTalkRobot.php
Normal file
63
src/alert-manager/src/DingTalkRobot.php
Normal 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;
|
||||
}
|
||||
}
|
||||
9
src/alert-manager/src/SenderInterface.php
Normal file
9
src/alert-manager/src/SenderInterface.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
namespace HyperfAdmin\AlertManager;
|
||||
|
||||
interface SenderInterface
|
||||
{
|
||||
public function sendText($message, $at = 'all');
|
||||
|
||||
public function sendMarkdown($message, $at = 'all');
|
||||
}
|
||||
10
src/alert-manager/src/func.php
Normal file
10
src/alert-manager/src/func.php
Normal 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));
|
||||
}
|
||||
78
src/base-utils/composer.json
Normal file
78
src/base-utils/composer.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/base-utils/src/AKSK.php
Normal file
50
src/base-utils/src/AKSK.php
Normal 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));
|
||||
}
|
||||
}
|
||||
150
src/base-utils/src/AliyunOSS.php
Normal file
150
src/base-utils/src/AliyunOSS.php
Normal 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, '/') : '',
|
||||
];
|
||||
}
|
||||
}
|
||||
27
src/base-utils/src/ColorLineFormatter.php
Normal file
27
src/base-utils/src/ColorLineFormatter.php
Normal 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);
|
||||
}
|
||||
}
|
||||
117
src/base-utils/src/ConfigProvider.php
Normal file
117
src/base-utils/src/ConfigProvider.php
Normal 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' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
42
src/base-utils/src/Constants/Consts.php
Normal file
42
src/base-utils/src/Constants/Consts.php
Normal 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',
|
||||
];
|
||||
}
|
||||
128
src/base-utils/src/Constants/ErrorCode.php
Normal file
128
src/base-utils/src/Constants/ErrorCode.php
Normal 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;
|
||||
}
|
||||
49
src/base-utils/src/Excel/ExcelReader.php
Normal file
49
src/base-utils/src/Excel/ExcelReader.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/base-utils/src/Excel/ExcelWriter.php
Normal file
28
src/base-utils/src/Excel/ExcelWriter.php
Normal 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();;
|
||||
}
|
||||
}
|
||||
54
src/base-utils/src/Exception/HttpExceptionHandler.php
Normal file
54
src/base-utils/src/Exception/HttpExceptionHandler.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
79
src/base-utils/src/Guzzle.php
Normal file
79
src/base-utils/src/Guzzle.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/base-utils/src/HAStreamHandler.php
Normal file
18
src/base-utils/src/HAStreamHandler.php
Normal 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;
|
||||
}
|
||||
}
|
||||
326
src/base-utils/src/Helper/array.php
Normal file
326
src/base-utils/src/Helper/array.php
Normal 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;
|
||||
}
|
||||
}
|
||||
810
src/base-utils/src/Helper/common.php
Normal file
810
src/base-utils/src/Helper/common.php
Normal 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);
|
||||
}
|
||||
}
|
||||
6
src/base-utils/src/Helper/constants.php
Normal file
6
src/base-utils/src/Helper/constants.php
Normal 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);
|
||||
211
src/base-utils/src/Helper/system.php
Normal file
211
src/base-utils/src/Helper/system.php
Normal 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
132
src/base-utils/src/JWT.php
Normal 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));
|
||||
}
|
||||
}
|
||||
39
src/base-utils/src/Listener/BootAppConfListener.php
Normal file
39
src/base-utils/src/Listener/BootAppConfListener.php
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
103
src/base-utils/src/Listener/DbQueryExecutedListener.php
Normal file
103
src/base-utils/src/Listener/DbQueryExecutedListener.php
Normal 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;
|
||||
}
|
||||
}
|
||||
24
src/base-utils/src/Listener/FetchModeListener.php
Normal file
24
src/base-utils/src/Listener/FetchModeListener.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/base-utils/src/Log.php
Normal file
12
src/base-utils/src/Log.php
Normal 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);
|
||||
}
|
||||
}
|
||||
25
src/base-utils/src/Middleware/CorsMiddleware.php
Normal file
25
src/base-utils/src/Middleware/CorsMiddleware.php
Normal 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
Reference in New Issue
Block a user