项目初始化

This commit is contained in:
2025-09-22 18:52:07 +08:00
commit c4fca82443
5192 changed files with 866921 additions and 0 deletions

14
.bowerrc Normal file
View File

@@ -0,0 +1,14 @@
{
"directory": "public/assets/libs",
"ignoredDependencies": [
"es6-promise",
"file-saver",
"html2canvas",
"jspdf",
"jspdf-autotable",
"pdfmake"
],
"scripts":{
"postinstall": "node bower-cleanup.js"
}
}

11
.env.sample Normal file
View File

@@ -0,0 +1,11 @@
[app]
debug = false
trace = false
[database]
hostname = 127.0.0.1
database = fastadmin
username = root
password = root
hostport = 3306
prefix = fa_

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
/runtime/*
/public/uploads/*
.idea
*.log
.env

0
.htaccess Normal file
View File

4
.npmrc Normal file
View File

@@ -0,0 +1,4 @@
# 使用自定义镜像源
registry=http://mirrors.tencent.com/npm/
#关闭SSL验证
strict-ssl=false

139
Gruntfile.js Normal file
View File

@@ -0,0 +1,139 @@
module.exports = function (grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
copy: {
main: {
files: []
}
}
});
var build = function (module, type, callback) {
var config = {
compile: {
options: type === 'js' ? {
optimizeCss: "standard",
optimize: "uglify", //可使用uglify|closure|none
preserveLicenseComments: true,
removeCombined: false,
baseUrl: "./public/assets/js/", //JS文件所在的基础目录
name: "require-" + module, //来源文件,不包含后缀
out: "./public/assets/js/require-" + module + ".min.js" //目标文件
} : {
optimizeCss: "default",
optimize: "uglify", //可使用uglify|closure|none
cssIn: "./public/assets/css/" + module + ".css", //CSS文件所在的基础目录
out: "./public/assets/css/" + module + ".min.css" //目标文件
}
}
};
var content = grunt.file.read("./public/assets/js/require-" + module + ".js"),
pattern = /^require\.config\(\{[\r\n]?[\n]?(.*?)[\r\n]?[\n]?}\);/is;
var matches = content.match(pattern);
if (matches) {
if (type === 'js') {
var data = matches[1].replaceAll(/(urlArgs|baseUrl):(.*)\n/gi, '');
const parse = require('parse-config-file'), fs = require('fs');
require('jsonminify');
data = JSON.minify("{\n" + data + "\n}");
let options = parse(data);
options.paths.tableexport = "empty:";
Object.assign(config.compile.options, options);
}
let requirejs = require("./application/admin/command/Min/r");
try {
requirejs.optimize(config.compile.options, function (buildResponse) {
// var contents = require('fs').readFileSync(config.compile.options.out, 'utf8');
callback();
}, function (err) {
console.error(err);
callback();
});
} catch (err) {
console.error(err);
callback();
}
}
};
// 加载 "copy" 插件
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.registerTask('frontend:js', 'build frontend js', function () {
var done = this.async();
build('frontend', 'js', done);
});
grunt.registerTask('backend:js', 'build backend js', function () {
var done = this.async();
build('backend', 'js', done);
});
grunt.registerTask('frontend:css', 'build frontend css', function () {
var done = this.async();
build('frontend', 'css', done);
});
grunt.registerTask('backend:css', 'build frontend css', function () {
var done = this.async();
build('backend', 'css', done);
});
// 注册部署JS和CSS任务
grunt.registerTask('deploy', 'deploy', function () {
const fs = require('fs');
const path = require("path")
const nodeModulesDir = path.resolve(__dirname, "./node_modules");
const getAllFiles = function (dirPath, arrayOfFiles) {
files = fs.readdirSync(dirPath)
arrayOfFiles = arrayOfFiles || []
files.forEach(function (file) {
if (fs.statSync(dirPath + "/" + file).isDirectory()) {
arrayOfFiles = getAllFiles(dirPath + "/" + file, arrayOfFiles)
} else {
arrayOfFiles.push(path.join(__dirname, dirPath, "/", file))
}
});
return arrayOfFiles
};
const mainPackage = grunt.config.get('pkg');
let dists = mainPackage.dists || [];
let files = [];
// 兼容旧版本bower使用的目录
let specialKey = {
'fastadmin-bootstraptable': 'bootstrap-table',
'sortablejs': 'Sortable',
'tableexport.jquery.plugin': 'tableExport.jquery.plugin',
};
Object.keys(dists).forEach(key => {
let src = ["**/*LICENSE*", "**/*license*"];
src = src.concat(Array.isArray(dists[key]) ? dists[key] : [dists[key]]);
files.push({expand: true, cwd: nodeModulesDir + "/" + key, src: src, dest: 'public/assets/libs/' + (specialKey[key] || key) + "/"});
});
// 兼容bower历史路径文件
files = [...files,
{src: nodeModulesDir + "/toastr/build/toastr.min.css", dest: "public/assets/libs/toastr/toastr.min.css"},
{src: nodeModulesDir + "/bootstrap-slider/dist/css/bootstrap-slider.css", dest: "public/assets/libs/bootstrap-slider/slider.css"},
{expand: true, cwd: nodeModulesDir + "/bootstrap-slider/dist", src: ["*.js"], dest: "public/assets/libs/bootstrap-slider/"}
]
grunt.config.set('copy.main.files', files);
grunt.task.run("copy:main");
});
// 注册默认任务
grunt.registerTask('default', ['deploy', 'frontend:js', 'backend:js', 'frontend:css', 'backend:css']);
};

191
LICENSE Normal file
View File

@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "{}" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright 07 Karson
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

93
README.md Normal file
View File

@@ -0,0 +1,93 @@
FastAdmin是一款基于ThinkPHP+Bootstrap的极速后台开发框架。
## 主要特性
* 基于`Auth`验证的权限管理系统
* 支持无限级父子级权限继承,父级的管理员可任意增删改子级管理员及权限设置
* 支持单管理员多角色
* 支持管理子级数据或个人数据
* 强大的一键生成功能
* 一键生成CRUD,包括控制器、模型、视图、JS、语言包、菜单、回收站等
* 一键压缩打包JS和CSS文件一键CDN静态资源部署
* 一键生成控制器菜单和规则
* 一键生成API接口文档
* 完善的前端功能组件开发
* 基于`AdminLTE`二次开发
* 基于`Bootstrap`开发自适应手机、平板、PC
* 基于`RequireJS`进行JS模块管理按需加载
* 基于`Less`进行样式开发
* 强大的插件扩展功能,在线安装卸载升级插件
* 通用的会员模块和API模块
* 共用同一账号体系的Web端会员中心权限验证和API接口会员权限验证
* 二级域名部署支持,同时域名支持绑定到应用插件
* 多语言支持,服务端及客户端支持
* 支持大文件分片上传、剪切板粘贴上传、拖拽上传,进度条显示,图片上传前压缩
* 支持表格固定列、固定表头、跨页选择、Excel导出、模板渲染等功能
* 强大的第三方应用模块支持([CMS](https://www.fastadmin.net/store/cms.html)、[CRM](https://www.fastadmin.net/store/facrm.html)、[企业网站管理系统](https://www.fastadmin.net/store/ldcms.html)、[知识库文档系统](https://www.fastadmin.net/store/knowbase.html)、[在线投票系统](https://www.fastadmin.net/store/vote.html)、[B2C商城](https://www.fastadmin.net/store/shopro.html)、[B2B2C商城](https://www.fastadmin.net/store/wanlshop.html))
* 整合第三方短信接口(阿里云、腾讯云短信)
* 无缝整合第三方云存储(七牛云、阿里云OSS、腾讯云存储、又拍云)功能,支持云储存分片上传
* 第三方富文本编辑器支持(Summernote、百度编辑器)
* 第三方登录(QQ、微信、微博)整合
* 第三方支付(微信、支付宝)无缝整合微信支持PC端扫码支付
* 丰富的插件应用市场
## 安装使用
https://doc.fastadmin.net
## 在线演示
https://demo.fastadmin.net
用户名admin
 123456
提 示:演示站数据无法进行修改,请下载源码安装体验全部功能
## 界面截图
![控制台](https://images.gitee.com/uploads/images/2020/0929/202947_8db2d281_10933.gif "控制台")
## 问题反馈
在使用中有任何问题,请使用以下联系方式联系我们
问答社区: https://ask.fastadmin.net
Github: https://github.com/fastadminnet/fastadmin
Gitee: https://gitee.com/fastadminnet/fastadmin
## 特别鸣谢
感谢以下的项目,排名不分先后
ThinkPHPhttp://www.thinkphp.cn
AdminLTEhttps://adminlte.io
Bootstraphttp://getbootstrap.com
jQueryhttp://jquery.com
Bootstrap-tablehttps://github.com/wenzhixin/bootstrap-table
Nice-validator: https://validator.niceue.com
SelectPage: https://github.com/TerryZ/SelectPage
Layer: https://layuion.com/layer/
DropzoneJS: https://www.dropzonejs.com
## 版权信息
FastAdmin遵循Apache2开源协议发布并提供免费使用。
本项目包含的第三方源码和二进制文件之版权信息另行标注。
版权所有Copyright © 2017-2024 by FastAdmin (https://www.fastadmin.net)
All rights reserved。

1
addons/.gitkeep Normal file
View File

@@ -0,0 +1 @@

1
addons/.htaccess Normal file
View File

@@ -0,0 +1 @@
deny from all

1
addons/crontab/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.addonrc

View File

@@ -0,0 +1,80 @@
<?php
namespace addons\crontab;
use app\common\library\Menu;
use think\Addons;
use think\Loader;
/**
* 定时任务
*/
class Crontab extends Addons
{
/**
* 插件安装方法
* @return bool
*/
public function install()
{
$menu = [
[
'name' => 'general/crontab',
'title' => '定时任务',
'icon' => 'fa fa-tasks',
'remark' => '按照设定的时间进行任务的执行,目前支持三种任务:请求URL、执行SQL、执行Shell。',
'sublist' => [
['name' => 'general/crontab/index', 'title' => '查看'],
['name' => 'general/crontab/add', 'title' => '添加'],
['name' => 'general/crontab/edit', 'title' => '编辑 '],
['name' => 'general/crontab/del', 'title' => '删除'],
['name' => 'general/crontab/multi', 'title' => '批量更新'],
]
]
];
Menu::create($menu, 'general');
return true;
}
/**
* 插件卸载方法
* @return bool
*/
public function uninstall()
{
Menu::delete('general/crontab');
return true;
}
/**
* 插件启用方法
*/
public function enable()
{
Menu::enable('general/crontab');
}
/**
* 插件禁用方法
*/
public function disable()
{
Menu::disable('general/crontab');
}
/**
* 添加命名空间
*/
public function appInit()
{
//添加命名空间
if (!class_exists('\Cron\CronExpression')) {
Loader::addNamespace('Cron', ADDON_PATH . 'crontab' . DS . 'library' . DS . 'Cron' . DS);
}
if (!class_exists('\Jenner\SimpleFork\Pool')) {
Loader::addNamespace('Jenner\SimpleFork', ADDON_PATH . 'crontab' . DS . 'library' . DS . 'SimpleFork' . DS);
}
}
}

19
addons/crontab/config.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
return [
[
'name' => 'mode',
'title' => '执行模式',
'type' => 'select',
'content' => [
'single' => '单进程,阻塞',
'pcntl' => '子进程无阻塞需支持pcntl不支持时自动切换为单进程',
],
'value' => 'pcntl',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
];

View File

@@ -0,0 +1,125 @@
<?php
namespace addons\crontab\controller;
use addons\crontab\model\Crontab;
use Cron\CronExpression;
use fast\Http;
use think\Controller;
use think\Db;
use think\Exception;
use think\Log;
/**
* 定时任务接口
*
* 以Crontab方式每分钟定时执行,且只可以Cli方式运行
* @internal
*/
class Autotask extends Controller
{
/**
* 初始化方法,最前且始终执行
*/
public function _initialize()
{
// 只可以以cli方式执行
if (!$this->request->isCli()) {
$this->error('Autotask script only work at client!');
}
parent::_initialize();
// 清除错误
error_reporting(0);
// 设置永不超时
set_time_limit(0);
}
/**
* 执行定时任务
*/
public function index()
{
$withPcntl = false;
$pool = null;
$config = get_addon_config('crontab');
$mode = $config['mode'] ?? 'pcntl';
if ($mode == 'pcntl' && function_exists('pcntl_fork')) {
$withPcntl = true;
$pool = new \Jenner\SimpleFork\Pool();
}
$time = time();
$logDir = LOG_PATH . 'crontab' . DS;
if (!is_dir($logDir)) {
mkdir($logDir, 0755);
}
//筛选未过期且未完成的任务
$crontabList = Crontab::where('status', '=', 'normal')->order('weigh DESC,id DESC')->select();
$execTime = time();
foreach ($crontabList as $crontab) {
$update = [];
$execute = false;
if ($time < $crontab['begintime']) {
//任务未开始
continue;
}
if ($crontab['maximums'] && $crontab['executes'] > $crontab['maximums']) {
//任务已超过最大执行次数
$update['status'] = 'completed';
} else {
if ($crontab['endtime'] > 0 && $time > $crontab['endtime']) {
//任务已过期
$update['status'] = 'expired';
} else {
//重复执行
//如果未到执行时间则继续循环
$cron = CronExpression::factory($crontab['schedule']);
if (!$cron->isDue() || ($crontab['executetime'] && date("YmdHi", $execTime) === date("YmdHi", $crontab['executetime']))) {
continue;
}
$execute = true;
}
}
// 如果允许执行
if ($execute) {
$update['executetime'] = $time;
$update['executes'] = $crontab['executes'] + 1;
$update['status'] = ($crontab['maximums'] > 0 && $update['executes'] >= $crontab['maximums']) ? 'completed' : 'normal';
}
// 如果需要更新状态
if (!$update) {
continue;
}
// 更新状态
$crontab->save($update);
Db::connect()->close();
// 将执行放在后面是为了避免超时导致多次执行
if (!$execute) {
continue;
}
$runnable = new \addons\crontab\library\CommandRunnable($crontab);
if ($withPcntl) {
$process = new \Jenner\SimpleFork\Process($runnable);
$name = $crontab['title'];
$pool->execute($process);
} else {
$runnable->run();
}
}
if ($withPcntl && $pool) {
$pool->wait();
}
return "Execute completed!\n";
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace addons\crontab\controller;
use think\addons\Controller;
class Index extends Controller
{
public function index()
{
$this->error("当前插件暂无前台页面");
}
}

10
addons/crontab/info.ini Normal file
View File

@@ -0,0 +1,10 @@
name = crontab
title = 定时任务管理
intro = 便捷的后台定时任务管理
author = FastAdmin
website = https://www.fastadmin.net
version = 1.1.3
state = 1
url = /addons/crontab
license = regular
licenseto = 101612

View File

@@ -0,0 +1,40 @@
CREATE TABLE IF NOT EXISTS `__PREFIX__crontab` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
`type` varchar(10) NOT NULL DEFAULT '' COMMENT '事件类型',
`title` varchar(100) NOT NULL DEFAULT '' COMMENT '事件标题',
`content` text NOT NULL COMMENT '事件内容',
`schedule` varchar(100) NOT NULL DEFAULT '' COMMENT 'Crontab格式',
`sleep` tinyint(1) UNSIGNED NOT NULL DEFAULT '0' COMMENT '延迟秒数执行',
`maximums` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '最大执行次数 0为不限',
`executes` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '已经执行的次数',
`createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
`updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
`begintime` bigint(16) DEFAULT NULL COMMENT '开始时间',
`endtime` bigint(16) DEFAULT NULL COMMENT '结束时间',
`executetime` bigint(16) DEFAULT NULL COMMENT '最后执行时间',
`weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重',
`status` enum('completed','expired','hidden','normal') NOT NULL DEFAULT 'normal' COMMENT '状态',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='定时任务表';
BEGIN;
INSERT INTO `__PREFIX__crontab` (`id`, `type`, `title`, `content`, `schedule`, `sleep`, `maximums`, `executes`, `createtime`, `updatetime`, `begintime`, `endtime`, `executetime`, `weigh`, `status`) VALUES
(1, 'url', '请求百度', 'https://www.baidu.com', '* * * * *', 0, 0, 0, 1497070825, 1501253101, 1483200000, 1830268800, 1501253101, 1, 'normal'),
(2, 'sql', '查询一条SQL', 'SELECT 1;', '* * * * *', 0, 0, 0, 1497071095, 1501253101, 1483200000, 1830268800, 1501253101, 2, 'normal');
COMMIT;
CREATE TABLE IF NOT EXISTS `__PREFIX__crontab_log` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`crontab_id` int(10) DEFAULT NULL COMMENT '任务ID',
`executetime` bigint(16) DEFAULT NULL COMMENT '执行时间',
`completetime` bigint(16) DEFAULT NULL COMMENT '结束时间',
`content` text COMMENT '执行结果',
`processid` int(10) NULL DEFAULT 0 COMMENT '进程ID',
`status` enum('success','failure', 'inprogress') DEFAULT 'failure' COMMENT '状态',
PRIMARY KEY (`id`),
KEY `crontab_id` (`crontab_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='定时任务日志表';
-- 1.1.0 --
ALTER TABLE `__PREFIX__crontab_log` ADD `processid` INT(10) NULL DEFAULT 0 COMMENT '进程ID' AFTER `content`, CHANGE `status` `status` ENUM('success','failure','inprogress') CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'failure' COMMENT '状态';

View File

@@ -0,0 +1,130 @@
<?php
namespace addons\crontab\library;
use fast\Http;
use think\Config;
use think\Db;
class CommandRunnable implements \Jenner\SimpleFork\Runnable
{
protected $connect = null;
protected $crontab = null;
public function __construct($crontab)
{
$this->crontab = $crontab;
}
public function run()
{
$processId = getmypid();
//这里需要强制重连数据库,使用已有的连接会报2014错误
$this->connect = Db::connect([], true);
$this->connect->execute("SELECT 1");
$message = '';
$result = false;
$this->crontabLog = null;
$log = [
'crontab_id' => $this->crontab['id'],
'executetime' => time(),
'completetime' => null,
'content' => '',
'processid' => $processId,
'status' => 'inprogress',
];
$this->connect->name("crontab_log")->insert($log);
$this->crontabLogId = $this->connect->getLastInsID();
try {
if ($this->crontab['type'] == 'url') {
if (substr($this->crontab['content'], 0, 1) == "/") {
// 本地项目URL
$message = shell_exec('php ' . ROOT_PATH . 'public/index.php ' . $this->crontab['content']);
$result = (bool)$message;
} else {
$arr = explode(" ", $this->crontab['content']);
$url = $arr[0];
$params = $arr[1] ?? '';
$method = $arr[2] ?? 'POST';
try {
// 远程异步调用URL
$ret = Http::sendRequest($url, $params, $method);
$result = $ret['ret'];
$message = $ret['msg'];
} catch (\Exception $e) {
$message = $e->getMessage();
}
}
} elseif ($this->crontab['type'] == 'sql') {
$ret = $this->sql($this->crontab['content']);
$result = $ret['ret'];
$message = $ret['msg'];
} elseif ($this->crontab['type'] == 'shell') {
// 执行Shell
$message = shell_exec($this->crontab['content']);
$result = !is_null($message);
}
} catch (\Exception $e) {
$message = $e->getMessage();
}
//设定任务完成
$this->connect->name("crontab_log")->where('id', $this->crontabLogId)->update(['content' => $message, 'completetime' => time(), 'status' => $result ? 'success' : 'failure']);
}
/**
* 执行SQL语句
*/
protected function sql($sql)
{
// 执行SQL
$sqlquery = str_replace('__PREFIX__', config('database.prefix'), $sql);
$sqls = preg_split("/;[ \t]{0,}\n/i", $sqlquery);
$result = false;
$message = '';
$this->connect->startTrans();
try {
foreach ($sqls as $key => $val) {
if (trim($val) == '' || substr($val, 0, 2) == '--' || substr($val, 0, 2) == '/*') {
continue;
}
$message .= "\nSQL:{$val}\n";
$val = rtrim($val, ';');
if (preg_match("/^(select|explain)(.*)/i ", $val)) {
$count = $this->connect->execute($val);
if ($count > 0) {
$resultlist = Db::query($val);
} else {
$resultlist = [];
}
$message .= "Total:{$count}\n";
$j = 1;
foreach ($resultlist as $m => $n) {
$message .= "\n";
$message .= "Row:{$j}\n";
foreach ($n as $k => $v) {
$message .= "{$k}{$v}\n";
}
$j++;
}
} else {
$count = $this->connect->getPdo()->exec($val);
$message = "Affected rows:{$count}";
}
}
$this->connect->commit();
$result = true;
} catch (\PDOException $e) {
$message = $e->getMessage();
$this->connect->rollback();
$result = false;
}
return ['ret' => $result, 'msg' => $message];
}
}

View File

@@ -0,0 +1,252 @@
<?php
namespace Cron;
/**
* Abstract CRON expression field
*/
abstract class AbstractField implements FieldInterface
{
/**
* Full range of values that are allowed for this field type
* @var array
*/
protected $fullRange = [];
/**
* Literal values we need to convert to integers
* @var array
*/
protected $literals = [];
/**
* Start value of the full range
* @var integer
*/
protected $rangeStart;
/**
* End value of the full range
* @var integer
*/
protected $rangeEnd;
public function __construct()
{
$this->fullRange = range($this->rangeStart, $this->rangeEnd);
}
/**
* Check to see if a field is satisfied by a value
*
* @param string $dateValue Date value to check
* @param string $value Value to test
*
* @return bool
*/
public function isSatisfied($dateValue, $value)
{
if ($this->isIncrementsOfRanges($value)) {
return $this->isInIncrementsOfRanges($dateValue, $value);
} elseif ($this->isRange($value)) {
return $this->isInRange($dateValue, $value);
}
return $value == '*' || $dateValue == $value;
}
/**
* Check if a value is a range
*
* @param string $value Value to test
*
* @return bool
*/
public function isRange($value)
{
return strpos($value, '-') !== false;
}
/**
* Check if a value is an increments of ranges
*
* @param string $value Value to test
*
* @return bool
*/
public function isIncrementsOfRanges($value)
{
return strpos($value, '/') !== false;
}
/**
* Test if a value is within a range
*
* @param string $dateValue Set date value
* @param string $value Value to test
*
* @return bool
*/
public function isInRange($dateValue, $value)
{
$parts = array_map('trim', explode('-', $value, 2));
return $dateValue >= $parts[0] && $dateValue <= $parts[1];
}
/**
* Test if a value is within an increments of ranges (offset[-to]/step size)
*
* @param string $dateValue Set date value
* @param string $value Value to test
*
* @return bool
*/
public function isInIncrementsOfRanges($dateValue, $value)
{
$chunks = array_map('trim', explode('/', $value, 2));
$range = $chunks[0];
$step = isset($chunks[1]) ? $chunks[1] : 0;
// No step or 0 steps aren't cool
if (is_null($step) || '0' === $step || 0 === $step) {
return false;
}
// Expand the * to a full range
if ('*' == $range) {
$range = $this->rangeStart . '-' . $this->rangeEnd;
}
// Generate the requested small range
$rangeChunks = explode('-', $range, 2);
$rangeStart = $rangeChunks[0];
$rangeEnd = isset($rangeChunks[1]) ? $rangeChunks[1] : $rangeStart;
if ($rangeStart < $this->rangeStart || $rangeStart > $this->rangeEnd || $rangeStart > $rangeEnd) {
throw new \OutOfRangeException('Invalid range start requested');
}
if ($rangeEnd < $this->rangeStart || $rangeEnd > $this->rangeEnd || $rangeEnd < $rangeStart) {
throw new \OutOfRangeException('Invalid range end requested');
}
if ($step > ($rangeEnd - $rangeStart) + 1) {
throw new \OutOfRangeException('Step cannot be greater than total range');
}
$thisRange = range($rangeStart, $rangeEnd, $step);
return in_array($dateValue, $thisRange);
}
/**
* Returns a range of values for the given cron expression
*
* @param string $expression The expression to evaluate
* @param int $max Maximum offset for range
*
* @return array
*/
public function getRangeForExpression($expression, $max)
{
$values = array();
if ($this->isRange($expression) || $this->isIncrementsOfRanges($expression)) {
if (!$this->isIncrementsOfRanges($expression)) {
list ($offset, $to) = explode('-', $expression);
$stepSize = 1;
}
else {
$range = array_map('trim', explode('/', $expression, 2));
$stepSize = isset($range[1]) ? $range[1] : 0;
$range = $range[0];
$range = explode('-', $range, 2);
$offset = $range[0];
$to = isset($range[1]) ? $range[1] : $max;
}
$offset = $offset == '*' ? 0 : $offset;
for ($i = $offset; $i <= $to; $i += $stepSize) {
$values[] = $i;
}
sort($values);
}
else {
$values = array($expression);
}
return $values;
}
protected function convertLiterals($value)
{
if (count($this->literals)) {
$key = array_search($value, $this->literals);
if ($key !== false) {
return $key;
}
}
return $value;
}
/**
* Checks to see if a value is valid for the field
*
* @param string $value
* @return bool
*/
public function validate($value)
{
$value = $this->convertLiterals($value);
// All fields allow * as a valid value
if ('*' === $value) {
return true;
}
// You cannot have a range and a list at the same time
if (strpos($value, ',') !== false && strpos($value, '-') !== false) {
return false;
}
if (strpos($value, '/') !== false) {
list($range, $step) = explode('/', $value);
return $this->validate($range) && filter_var($step, FILTER_VALIDATE_INT);
}
if (strpos($value, '-') !== false) {
if (substr_count($value, '-') > 1) {
return false;
}
$chunks = explode('-', $value);
$chunks[0] = $this->convertLiterals($chunks[0]);
$chunks[1] = $this->convertLiterals($chunks[1]);
if ('*' == $chunks[0] || '*' == $chunks[1]) {
return false;
}
return $this->validate($chunks[0]) && $this->validate($chunks[1]);
}
// Validate each chunk of a list individually
if (strpos($value, ',') !== false) {
foreach (explode(',', $value) as $listItem) {
if (!$this->validate($listItem)) {
return false;
}
}
return true;
}
// We should have a numeric by now, so coerce this into an integer
if (filter_var($value, FILTER_VALIDATE_INT) !== false) {
$value = (int) $value;
}
return in_array($value, $this->fullRange, true);
}
}

View File

@@ -0,0 +1,402 @@
<?php
namespace Cron;
use DateTime;
use DateTimeImmutable;
use DateTimeZone;
use Exception;
use InvalidArgumentException;
use RuntimeException;
/**
* CRON expression parser that can determine whether or not a CRON expression is
* due to run, the next run date and previous run date of a CRON expression.
* The determinations made by this class are accurate if checked run once per
* minute (seconds are dropped from date time comparisons).
*
* Schedule parts must map to:
* minute [0-59], hour [0-23], day of month, month [1-12|JAN-DEC], day of week
* [1-7|MON-SUN], and an optional year.
*
* @link http://en.wikipedia.org/wiki/Cron
*/
class CronExpression
{
const MINUTE = 0;
const HOUR = 1;
const DAY = 2;
const MONTH = 3;
const WEEKDAY = 4;
const YEAR = 5;
/**
* @var array CRON expression parts
*/
private $cronParts;
/**
* @var FieldFactory CRON field factory
*/
private $fieldFactory;
/**
* @var int Max iteration count when searching for next run date
*/
private $maxIterationCount = 1000;
/**
* @var array Order in which to test of cron parts
*/
private static $order = array(self::YEAR, self::MONTH, self::DAY, self::WEEKDAY, self::HOUR, self::MINUTE);
/**
* Factory method to create a new CronExpression.
*
* @param string $expression The CRON expression to create. There are
* several special predefined values which can be used to substitute the
* CRON expression:
*
* `@yearly`, `@annually` - Run once a year, midnight, Jan. 1 - 0 0 1 1 *
* `@monthly` - Run once a month, midnight, first of month - 0 0 1 * *
* `@weekly` - Run once a week, midnight on Sun - 0 0 * * 0
* `@daily` - Run once a day, midnight - 0 0 * * *
* `@hourly` - Run once an hour, first minute - 0 * * * *
* @param FieldFactory $fieldFactory Field factory to use
*
* @return CronExpression
*/
public static function factory($expression, FieldFactory $fieldFactory = null)
{
$mappings = array(
'@yearly' => '0 0 1 1 *',
'@annually' => '0 0 1 1 *',
'@monthly' => '0 0 1 * *',
'@weekly' => '0 0 * * 0',
'@daily' => '0 0 * * *',
'@hourly' => '0 * * * *'
);
if (isset($mappings[$expression])) {
$expression = $mappings[$expression];
}
return new static($expression, $fieldFactory ?: new FieldFactory());
}
/**
* Validate a CronExpression.
*
* @param string $expression The CRON expression to validate.
*
* @return bool True if a valid CRON expression was passed. False if not.
* @see \Cron\CronExpression::factory
*/
public static function isValidExpression($expression)
{
try {
self::factory($expression);
} catch (InvalidArgumentException $e) {
return false;
}
return true;
}
/**
* Parse a CRON expression
*
* @param string $expression CRON expression (e.g. '8 * * * *')
* @param FieldFactory $fieldFactory Factory to create cron fields
*/
public function __construct($expression, FieldFactory $fieldFactory)
{
$this->fieldFactory = $fieldFactory;
$this->setExpression($expression);
}
/**
* Set or change the CRON expression
*
* @param string $value CRON expression (e.g. 8 * * * *)
*
* @return CronExpression
* @throws \InvalidArgumentException if not a valid CRON expression
*/
public function setExpression($value)
{
$this->cronParts = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY);
if (count($this->cronParts) < 5) {
throw new InvalidArgumentException(
$value . ' is not a valid CRON expression'
);
}
foreach ($this->cronParts as $position => $part) {
$this->setPart($position, $part);
}
return $this;
}
/**
* Set part of the CRON expression
*
* @param int $position The position of the CRON expression to set
* @param string $value The value to set
*
* @return CronExpression
* @throws \InvalidArgumentException if the value is not valid for the part
*/
public function setPart($position, $value)
{
if (!$this->fieldFactory->getField($position)->validate($value)) {
throw new InvalidArgumentException(
'Invalid CRON field value ' . $value . ' at position ' . $position
);
}
$this->cronParts[$position] = $value;
return $this;
}
/**
* Set max iteration count for searching next run dates
*
* @param int $maxIterationCount Max iteration count when searching for next run date
*
* @return CronExpression
*/
public function setMaxIterationCount($maxIterationCount)
{
$this->maxIterationCount = $maxIterationCount;
return $this;
}
/**
* Get a next run date relative to the current date or a specific date
*
* @param string|\DateTime $currentTime Relative calculation date
* @param int $nth Number of matches to skip before returning a
* matching next run date. 0, the default, will return the current
* date and time if the next run date falls on the current date and
* time. Setting this value to 1 will skip the first match and go to
* the second match. Setting this value to 2 will skip the first 2
* matches and so on.
* @param bool $allowCurrentDate Set to TRUE to return the current date if
* it matches the cron expression.
* @param null|string $timeZone Timezone to use instead of the system default
*
* @return \DateTime
* @throws \RuntimeException on too many iterations
*/
public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timeZone = null)
{
return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate, $timeZone);
}
/**
* Get a previous run date relative to the current date or a specific date
*
* @param string|\DateTime $currentTime Relative calculation date
* @param int $nth Number of matches to skip before returning
* @param bool $allowCurrentDate Set to TRUE to return the
* current date if it matches the cron expression
* @param null|string $timeZone Timezone to use instead of the system default
*
* @return \DateTime
* @throws \RuntimeException on too many iterations
* @see \Cron\CronExpression::getNextRunDate
*/
public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timeZone = null)
{
return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate, $timeZone);
}
/**
* Get multiple run dates starting at the current date or a specific date
*
* @param int $total Set the total number of dates to calculate
* @param string|\DateTime $currentTime Relative calculation date
* @param bool $invert Set to TRUE to retrieve previous dates
* @param bool $allowCurrentDate Set to TRUE to return the
* current date if it matches the cron expression
* @param null|string $timeZone Timezone to use instead of the system default
*
* @return array Returns an array of run dates
*/
public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false, $timeZone = null)
{
$matches = array();
for ($i = 0; $i < max(0, $total); $i++) {
try {
$matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate, $timeZone);
} catch (RuntimeException $e) {
break;
}
}
return $matches;
}
/**
* Get all or part of the CRON expression
*
* @param string $part Specify the part to retrieve or NULL to get the full
* cron schedule string.
*
* @return string|null Returns the CRON expression, a part of the
* CRON expression, or NULL if the part was specified but not found
*/
public function getExpression($part = null)
{
if (null === $part) {
return implode(' ', $this->cronParts);
} elseif (array_key_exists($part, $this->cronParts)) {
return $this->cronParts[$part];
}
return null;
}
/**
* Helper method to output the full expression.
*
* @return string Full CRON expression
*/
public function __toString()
{
return $this->getExpression();
}
/**
* Determine if the cron is due to run based on the current date or a
* specific date. This method assumes that the current number of
* seconds are irrelevant, and should be called once per minute.
*
* @param string|\DateTime $currentTime Relative calculation date
* @param null|string $timeZone Timezone to use instead of the system default
*
* @return bool Returns TRUE if the cron is due to run or FALSE if not
*/
public function isDue($currentTime = 'now', $timeZone = null)
{
if (is_null($timeZone)) {
$timeZone = date_default_timezone_get();
}
if ('now' === $currentTime) {
$currentDate = date('Y-m-d H:i');
$currentTime = strtotime($currentDate);
} elseif ($currentTime instanceof DateTime) {
$currentDate = clone $currentTime;
// Ensure time in 'current' timezone is used
$currentDate->setTimezone(new DateTimeZone($timeZone));
$currentDate = $currentDate->format('Y-m-d H:i');
$currentTime = strtotime($currentDate);
} elseif ($currentTime instanceof DateTimeImmutable) {
$currentDate = DateTime::createFromFormat('U', $currentTime->format('U'));
$currentDate->setTimezone(new DateTimeZone($timeZone));
$currentDate = $currentDate->format('Y-m-d H:i');
$currentTime = strtotime($currentDate);
} else {
$currentTime = new DateTime($currentTime);
$currentTime->setTime($currentTime->format('H'), $currentTime->format('i'), 0);
$currentDate = $currentTime->format('Y-m-d H:i');
$currentTime = $currentTime->getTimeStamp();
}
try {
return $this->getNextRunDate($currentDate, 0, true)->getTimestamp() == $currentTime;
} catch (Exception $e) {
return false;
}
}
/**
* Get the next or previous run date of the expression relative to a date
*
* @param string|\DateTime $currentTime Relative calculation date
* @param int $nth Number of matches to skip before returning
* @param bool $invert Set to TRUE to go backwards in time
* @param bool $allowCurrentDate Set to TRUE to return the
* current date if it matches the cron expression
* @param string|null $timeZone Timezone to use instead of the system default
*
* @return \DateTime
* @throws \RuntimeException on too many iterations
*/
protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $allowCurrentDate = false, $timeZone = null)
{
if (is_null($timeZone)) {
$timeZone = date_default_timezone_get();
}
if ($currentTime instanceof DateTime) {
$currentDate = clone $currentTime;
} elseif ($currentTime instanceof DateTimeImmutable) {
$currentDate = DateTime::createFromFormat('U', $currentTime->format('U'));
$currentDate->setTimezone($currentTime->getTimezone());
} else {
$currentDate = new DateTime($currentTime ?: 'now');
$currentDate->setTimezone(new DateTimeZone($timeZone));
}
$currentDate->setTime($currentDate->format('H'), $currentDate->format('i'), 0);
$nextRun = clone $currentDate;
$nth = (int) $nth;
// We don't have to satisfy * or null fields
$parts = array();
$fields = array();
foreach (self::$order as $position) {
$part = $this->getExpression($position);
if (null === $part || '*' === $part) {
continue;
}
$parts[$position] = $part;
$fields[$position] = $this->fieldFactory->getField($position);
}
// Set a hard limit to bail on an impossible date
for ($i = 0; $i < $this->maxIterationCount; $i++) {
foreach ($parts as $position => $part) {
$satisfied = false;
// Get the field object used to validate this part
$field = $fields[$position];
// Check if this is singular or a list
if (strpos($part, ',') === false) {
$satisfied = $field->isSatisfiedBy($nextRun, $part);
} else {
foreach (array_map('trim', explode(',', $part)) as $listPart) {
if ($field->isSatisfiedBy($nextRun, $listPart)) {
$satisfied = true;
break;
}
}
}
// If the field is not satisfied, then start over
if (!$satisfied) {
$field->increment($nextRun, $invert, $part);
continue 2;
}
}
// Skip this match if needed
if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) {
$this->fieldFactory->getField(0)->increment($nextRun, $invert, isset($parts[0]) ? $parts[0] : null);
continue;
}
return $nextRun;
}
// @codeCoverageIgnoreStart
throw new RuntimeException('Impossible CRON expression');
// @codeCoverageIgnoreEnd
}
}

View File

@@ -0,0 +1,131 @@
<?php
namespace Cron;
use DateTime;
/**
* Day of month field. Allows: * , / - ? L W
*
* 'L' stands for "last" and specifies the last day of the month.
*
* The 'W' character is used to specify the weekday (Monday-Friday) nearest the
* given day. As an example, if you were to specify "15W" as the value for the
* day-of-month field, the meaning is: "the nearest weekday to the 15th of the
* month". So if the 15th is a Saturday, the trigger will fire on Friday the
* 14th. If the 15th is a Sunday, the trigger will fire on Monday the 16th. If
* the 15th is a Tuesday, then it will fire on Tuesday the 15th. However if you
* specify "1W" as the value for day-of-month, and the 1st is a Saturday, the
* trigger will fire on Monday the 3rd, as it will not 'jump' over the boundary
* of a month's days. The 'W' character can only be specified when the
* day-of-month is a single day, not a range or list of days.
*
* @author Michael Dowling <mtdowling@gmail.com>
*/
class DayOfMonthField extends AbstractField
{
protected $rangeStart = 1;
protected $rangeEnd = 31;
/**
* Get the nearest day of the week for a given day in a month
*
* @param int $currentYear Current year
* @param int $currentMonth Current month
* @param int $targetDay Target day of the month
*
* @return \DateTime Returns the nearest date
*/
private static function getNearestWeekday($currentYear, $currentMonth, $targetDay)
{
$tday = str_pad($targetDay, 2, '0', STR_PAD_LEFT);
$target = DateTime::createFromFormat('Y-m-d', "$currentYear-$currentMonth-$tday");
$currentWeekday = (int) $target->format('N');
if ($currentWeekday < 6) {
return $target;
}
$lastDayOfMonth = $target->format('t');
foreach (array(-1, 1, -2, 2) as $i) {
$adjusted = $targetDay + $i;
if ($adjusted > 0 && $adjusted <= $lastDayOfMonth) {
$target->setDate($currentYear, $currentMonth, $adjusted);
if ($target->format('N') < 6 && $target->format('m') == $currentMonth) {
return $target;
}
}
}
}
public function isSatisfiedBy(DateTime $date, $value)
{
// ? states that the field value is to be skipped
if ($value == '?') {
return true;
}
$fieldValue = $date->format('d');
// Check to see if this is the last day of the month
if ($value == 'L') {
return $fieldValue == $date->format('t');
}
// Check to see if this is the nearest weekday to a particular value
if (strpos($value, 'W')) {
// Parse the target day
$targetDay = substr($value, 0, strpos($value, 'W'));
// Find out if the current day is the nearest day of the week
return $date->format('j') == self::getNearestWeekday(
$date->format('Y'),
$date->format('m'),
$targetDay
)->format('j');
}
return $this->isSatisfied($date->format('d'), $value);
}
public function increment(DateTime $date, $invert = false)
{
if ($invert) {
$date->modify('previous day');
$date->setTime(23, 59);
} else {
$date->modify('next day');
$date->setTime(0, 0);
}
return $this;
}
/**
* @inheritDoc
*/
public function validate($value)
{
$basicChecks = parent::validate($value);
// Validate that a list don't have W or L
if (strpos($value, ',') !== false && (strpos($value, 'W') !== false || strpos($value, 'L') !== false)) {
return false;
}
if (!$basicChecks) {
if ($value === 'L') {
return true;
}
if (preg_match('/^(.*)W$/', $value, $matches)) {
return $this->validate($matches[1]);
}
return false;
}
return $basicChecks;
}
}

View File

@@ -0,0 +1,170 @@
<?php
namespace Cron;
use DateTime;
use InvalidArgumentException;
/**
* Day of week field. Allows: * / , - ? L #
*
* Days of the week can be represented as a number 0-7 (0|7 = Sunday)
* or as a three letter string: SUN, MON, TUE, WED, THU, FRI, SAT.
*
* 'L' stands for "last". It allows you to specify constructs such as
* "the last Friday" of a given month.
*
* '#' is allowed for the day-of-week field, and must be followed by a
* number between one and five. It allows you to specify constructs such as
* "the second Friday" of a given month.
*/
class DayOfWeekField extends AbstractField
{
protected $rangeStart = 0;
protected $rangeEnd = 7;
protected $nthRange;
protected $literals = [1 => 'MON', 2 => 'TUE', 3 => 'WED', 4 => 'THU', 5 => 'FRI', 6 => 'SAT', 7 => 'SUN'];
public function __construct()
{
$this->nthRange = range(1, 5);
parent::__construct();
}
public function isSatisfiedBy(DateTime $date, $value)
{
if ($value == '?') {
return true;
}
// Convert text day of the week values to integers
$value = $this->convertLiterals($value);
$currentYear = $date->format('Y');
$currentMonth = $date->format('m');
$lastDayOfMonth = $date->format('t');
// Find out if this is the last specific weekday of the month
if (strpos($value, 'L')) {
$weekday = str_replace('7', '0', substr($value, 0, strpos($value, 'L')));
$tdate = clone $date;
$tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth);
while ($tdate->format('w') != $weekday) {
$tdateClone = new DateTime();
$tdate = $tdateClone
->setTimezone($tdate->getTimezone())
->setDate($currentYear, $currentMonth, --$lastDayOfMonth);
}
return $date->format('j') == $lastDayOfMonth;
}
// Handle # hash tokens
if (strpos($value, '#')) {
list($weekday, $nth) = explode('#', $value);
if (!is_numeric($nth)) {
throw new InvalidArgumentException("Hashed weekdays must be numeric, {$nth} given");
} else {
$nth = (int) $nth;
}
// 0 and 7 are both Sunday, however 7 matches date('N') format ISO-8601
if ($weekday === '0') {
$weekday = 7;
}
$weekday = $this->convertLiterals($weekday);
// Validate the hash fields
if ($weekday < 0 || $weekday > 7) {
throw new InvalidArgumentException("Weekday must be a value between 0 and 7. {$weekday} given");
}
if (!in_array($nth, $this->nthRange)) {
throw new InvalidArgumentException("There are never more than 5 or less than 1 of a given weekday in a month, {$nth} given");
}
// The current weekday must match the targeted weekday to proceed
if ($date->format('N') != $weekday) {
return false;
}
$tdate = clone $date;
$tdate->setDate($currentYear, $currentMonth, 1);
$dayCount = 0;
$currentDay = 1;
while ($currentDay < $lastDayOfMonth + 1) {
if ($tdate->format('N') == $weekday) {
if (++$dayCount >= $nth) {
break;
}
}
$tdate->setDate($currentYear, $currentMonth, ++$currentDay);
}
return $date->format('j') == $currentDay;
}
// Handle day of the week values
if (strpos($value, '-')) {
$parts = explode('-', $value);
if ($parts[0] == '7') {
$parts[0] = '0';
} elseif ($parts[1] == '0') {
$parts[1] = '7';
}
$value = implode('-', $parts);
}
// Test to see which Sunday to use -- 0 == 7 == Sunday
$format = in_array(7, str_split($value)) ? 'N' : 'w';
$fieldValue = $date->format($format);
return $this->isSatisfied($fieldValue, $value);
}
public function increment(DateTime $date, $invert = false)
{
if ($invert) {
$date->modify('-1 day');
$date->setTime(23, 59, 0);
} else {
$date->modify('+1 day');
$date->setTime(0, 0, 0);
}
return $this;
}
/**
* @inheritDoc
*/
public function validate($value)
{
$basicChecks = parent::validate($value);
if (!$basicChecks) {
// Handle the # value
if (strpos($value, '#') !== false) {
$chunks = explode('#', $value);
$chunks[0] = $this->convertLiterals($chunks[0]);
if (parent::validate($chunks[0]) && is_numeric($chunks[1]) && in_array($chunks[1], $this->nthRange)) {
return true;
}
}
if (preg_match('/^(.*)L$/', $value, $matches)) {
return $this->validate($matches[1]);
}
return false;
}
return $basicChecks;
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Cron;
use InvalidArgumentException;
/**
* CRON field factory implementing a flyweight factory
* @link http://en.wikipedia.org/wiki/Cron
*/
class FieldFactory
{
/**
* @var array Cache of instantiated fields
*/
private $fields = array();
/**
* Get an instance of a field object for a cron expression position
*
* @param int $position CRON expression position value to retrieve
*
* @return FieldInterface
* @throws InvalidArgumentException if a position is not valid
*/
public function getField($position)
{
if (!isset($this->fields[$position])) {
switch ($position) {
case 0:
$this->fields[$position] = new MinutesField();
break;
case 1:
$this->fields[$position] = new HoursField();
break;
case 2:
$this->fields[$position] = new DayOfMonthField();
break;
case 3:
$this->fields[$position] = new MonthField();
break;
case 4:
$this->fields[$position] = new DayOfWeekField();
break;
default:
throw new InvalidArgumentException(
$position . ' is not a valid position'
);
}
}
return $this->fields[$position];
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Cron;
use DateTime;
/**
* CRON field interface
*/
interface FieldInterface
{
/**
* Check if the respective value of a DateTime field satisfies a CRON exp
*
* @param DateTime $date DateTime object to check
* @param string $value CRON expression to test against
*
* @return bool Returns TRUE if satisfied, FALSE otherwise
*/
public function isSatisfiedBy(DateTime $date, $value);
/**
* When a CRON expression is not satisfied, this method is used to increment
* or decrement a DateTime object by the unit of the cron field
*
* @param DateTime $date DateTime object to change
* @param bool $invert (optional) Set to TRUE to decrement
*
* @return FieldInterface
*/
public function increment(DateTime $date, $invert = false);
/**
* Validates a CRON expression for a given field
*
* @param string $value CRON expression value to validate
*
* @return bool Returns TRUE if valid, FALSE otherwise
*/
public function validate($value);
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Cron;
use DateTime;
use DateTimeZone;
/**
* Hours field. Allows: * , / -
*/
class HoursField extends AbstractField
{
protected $rangeStart = 0;
protected $rangeEnd = 23;
public function isSatisfiedBy(DateTime $date, $value)
{
return $this->isSatisfied($date->format('H'), $value);
}
public function increment(DateTime $date, $invert = false, $parts = null)
{
// Change timezone to UTC temporarily. This will
// allow us to go back or forwards and hour even
// if DST will be changed between the hours.
if (is_null($parts) || $parts == '*') {
$timezone = $date->getTimezone();
$date->setTimezone(new DateTimeZone('UTC'));
if ($invert) {
$date->modify('-1 hour');
} else {
$date->modify('+1 hour');
}
$date->setTimezone($timezone);
$date->setTime($date->format('H'), $invert ? 59 : 0);
return $this;
}
$parts = strpos($parts, ',') !== false ? explode(',', $parts) : array($parts);
$hours = array();
foreach ($parts as $part) {
$hours = array_merge($hours, $this->getRangeForExpression($part, 23));
}
$current_hour = $date->format('H');
$position = $invert ? count($hours) - 1 : 0;
if (count($hours) > 1) {
for ($i = 0; $i < count($hours) - 1; $i++) {
if ((!$invert && $current_hour >= $hours[$i] && $current_hour < $hours[$i + 1]) ||
($invert && $current_hour > $hours[$i] && $current_hour <= $hours[$i + 1])) {
$position = $invert ? $i : $i + 1;
break;
}
}
}
$hour = $hours[$position];
if ((!$invert && $date->format('H') >= $hour) || ($invert && $date->format('H') <= $hour)) {
$date->modify(($invert ? '-' : '+') . '1 day');
$date->setTime($invert ? 23 : 0, $invert ? 59 : 0);
}
else {
$date->setTime($hour, $invert ? 59 : 0);
}
return $this;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Cron;
use DateTime;
/**
* Minutes field. Allows: * , / -
*/
class MinutesField extends AbstractField
{
protected $rangeStart = 0;
protected $rangeEnd = 59;
public function isSatisfiedBy(DateTime $date, $value)
{
return $this->isSatisfied($date->format('i'), $value);
}
public function increment(DateTime $date, $invert = false, $parts = null)
{
if (is_null($parts)) {
if ($invert) {
$date->modify('-1 minute');
} else {
$date->modify('+1 minute');
}
return $this;
}
$parts = strpos($parts, ',') !== false ? explode(',', $parts) : array($parts);
$minutes = array();
foreach ($parts as $part) {
$minutes = array_merge($minutes, $this->getRangeForExpression($part, 59));
}
$current_minute = $date->format('i');
$position = $invert ? count($minutes) - 1 : 0;
if (count($minutes) > 1) {
for ($i = 0; $i < count($minutes) - 1; $i++) {
if ((!$invert && $current_minute >= $minutes[$i] && $current_minute < $minutes[$i + 1]) ||
($invert && $current_minute > $minutes[$i] && $current_minute <= $minutes[$i + 1])) {
$position = $invert ? $i : $i + 1;
break;
}
}
}
if ((!$invert && $current_minute >= $minutes[$position]) || ($invert && $current_minute <= $minutes[$position])) {
$date->modify(($invert ? '-' : '+') . '1 hour');
$date->setTime($date->format('H'), $invert ? 59 : 0);
}
else {
$date->setTime($date->format('H'), $minutes[$position]);
}
return $this;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Cron;
use DateTime;
/**
* Month field. Allows: * , / -
*/
class MonthField extends AbstractField
{
protected $rangeStart = 1;
protected $rangeEnd = 12;
protected $literals = [1 => 'JAN', 2 => 'FEB', 3 => 'MAR', 4 => 'APR', 5 => 'MAY', 6 => 'JUN', 7 => 'JUL',
8 => 'AUG', 9 => 'SEP', 10 => 'OCT', 11 => 'NOV', 12 => 'DEC'];
public function isSatisfiedBy(DateTime $date, $value)
{
$value = $this->convertLiterals($value);
return $this->isSatisfied($date->format('m'), $value);
}
public function increment(DateTime $date, $invert = false)
{
if ($invert) {
$date->modify('last day of previous month');
$date->setTime(23, 59);
} else {
$date->modify('first day of next month');
$date->setTime(0, 0);
}
return $this;
}
}

View File

@@ -0,0 +1,183 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/11/3
* Time: 14:37
*/
namespace Jenner\SimpleFork;
/**
* processes' pool
*
* @package Jenner\SimpleFork
*/
abstract class AbstractPool
{
/**
* process list
*
* @var Process[]
*/
protected $processes = array();
/**
* get process by pid
*
* @param $pid
* @return null|Process
*/
public function getProcessByPid($pid)
{
foreach ($this->processes as $process) {
if ($process->getPid() == $pid) {
return $process;
}
}
return null;
}
/**
* shutdown sub process and no wait. it is dangerous,
* maybe the sub process is working.
*/
public function shutdownForce()
{
$this->shutdown(SIGKILL);
}
/**
* shutdown all process
*
* @param int $signal
*/
public function shutdown($signal = SIGTERM)
{
foreach ($this->processes as $process) {
if ($process->isRunning()) {
$process->shutdown(true, $signal);
}
}
}
/**
* if all processes are stopped
*
* @return bool
*/
public function isFinished()
{
foreach ($this->processes as $process) {
if (!$process->isStopped()) {
return false;
}
}
return true;
}
/**
* waiting for the sub processes to exit
*
* @param bool|true $block if true the parent process will be blocked until all
* sub processes exit. else it will check if there are processes that had been exited once and return.
* @param int $sleep when $block is true, it will check sub processes every $sleep minute
*/
public function wait($block = true, $sleep = 100)
{
do {
foreach ($this->processes as $process) {
if (!$process->isRunning()) {
continue;
}
}
usleep($sleep);
} while ($block && $this->aliveCount() > 0);
}
/**
* get the count of running processes
*
* @return int
*/
public function aliveCount()
{
$count = 0;
foreach ($this->processes as $process) {
if ($process->isRunning()) {
$count++;
}
}
return $count;
}
/**
* get process by name
*
* @param string $name process name
* @return Process|null
*/
public function getProcessByName($name)
{
foreach ($this->processes as $process) {
if ($process->name() == $name) {
return $process;
}
}
return null;
}
/**
* remove process by name
*
* @param string $name process name
* @throws \RuntimeException
*/
public function removeProcessByName($name)
{
foreach ($this->processes as $key => $process) {
if ($process->name() == $name) {
if ($process->isRunning()) {
throw new \RuntimeException("can not remove a running process");
}
unset($this->processes[$key]);
}
}
}
/**
* remove exited process
*/
public function removeExitedProcess()
{
foreach ($this->processes as $key => $process) {
if ($process->isStopped()) {
unset($this->processes[$key]);
}
}
}
/**
* return process count
*
* @return int
*/
public function count()
{
return count($this->processes);
}
/**
* get all processes
*
* @return Process[]
*/
public function getProcesses()
{
return $this->processes;
}
}

View File

@@ -0,0 +1,53 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/12
* Time: 14:59
*/
namespace Jenner\SimpleFork\Cache;
/**
* cache for processes shared variables
*
* @package Jenner\SimpleFork\Cache
*/
interface CacheInterface
{
/**
* get var
*
* @param $key
* @param null $default
* @return bool|mixed
*/
public function get($key, $default = null);
/**
* set var
*
* @param $key
* @param null $value
* @return
*/
public function set($key, $value);
/**
* has var ?
*
* @param $key
* @return bool
*/
public function has($key);
/**
* delete var
*
* @param $key
* @return bool
*/
public function delete($key);
}

View File

@@ -0,0 +1,263 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2016/6/22
* Time: 16:18
*/
namespace Jenner\SimpleFork\Cache;
class FileCache implements CacheInterface
{
/**
* 缓存目录
* @var
*/
private $cache_dir;
/**
* @param string $cache_dir
* @throws \Exception
*/
public function __construct($cache_dir)
{
$this->cache_dir = $cache_dir;
if (!is_dir($cache_dir)) {
$make_dir_result = mkdir($cache_dir, 0755, true);
if ($make_dir_result === false) throw new \Exception('Cannot create the cache directory');
}
}
/**
* get value by key, and check if it is expired
* @param string $key
* @param string $default
* @return mixed
*/
public function get($key, $default = null)
{
$cache_data = $this->getItem($key);
if ($cache_data === false || !is_array($cache_data)) return $default;
return $cache_data['data'];
}
/**
* 添加或覆盖一个key
* @param string $key
* @param mixed $value
* @param int $expire expire time in seconds
* @return mixed
*/
public function set($key, $value, $expire = 0)
{
return $this->setItem($key, $value, time(), $expire);
}
/**
* 设置包含元数据的信息
* @param $key
* @param $value
* @param $time
* @param $expire
* @return bool
*/
private function setItem($key, $value, $time, $expire)
{
$cache_file = $this->createCacheFile($key);
if ($cache_file === false) return false;
$cache_data = array('data' => $value, 'time' => $time, 'expire' => $expire);
$cache_data = serialize($cache_data);
$put_result = file_put_contents($cache_file, $cache_data);
if ($put_result === false) return false;
return true;
}
/**
* 创建缓存文件
* @param $key
* @return bool|string
*/
private function createCacheFile($key)
{
$cache_file = $this->path($key);
if (!file_exists($cache_file)) {
$directory = dirname($cache_file);
if (!is_dir($directory)) {
$make_dir_result = mkdir($directory, 0755, true);
if ($make_dir_result === false) return false;
}
$create_result = touch($cache_file);
if ($create_result === false) return false;
}
return $cache_file;
}
/**
* 判断Key是否存在
* @param $key
* @return mixed
*/
public function has($key)
{
$value = $this->get($key);
if ($value === false) return false;
return true;
}
/**
* 加法递增
* @param $key
* @param int $value
* @return mixed
*/
public function increment($key, $value = 1)
{
$item = $this->getItem($key);
if ($item === false) {
$set_result = $this->set($key, $value);
if ($set_result === false) return false;
return $value;
}
$check_expire = $this->checkExpire($item);
if ($check_expire === false) return false;
$item['data'] += $value;
$result = $this->setItem($key, $item['data'], $item['time'], $item['expire']);
if ($result === false) return false;
return $item['data'];
}
/**
* 减法递增
* @param $key
* @param int $value
* @return mixed
*/
public function decrement($key, $value = 1)
{
$item = $this->getItem($key);
if ($item === false) {
$value = 0 - $value;
$set_result = $this->set($key, $value);
if ($set_result === false) return false;
return $value;
}
$check_expire = $this->checkExpire($item);
if ($check_expire === false) return false;
$item['data'] -= $value;
$result = $this->setItem($key, $item['data'], $item['time'], $item['expire']);
if ($result === false) return false;
return $item['data'];
}
/**
* 删除一个key同事会删除缓存文件
* @param $key
* @return boolean
*/
public function delete($key)
{
$cache_file = $this->path($key);
if (file_exists($cache_file)) {
$unlink_result = unlink($cache_file);
if ($unlink_result === false) return false;
}
return true;
}
/**
* 清楚所有缓存
* @return mixed
*/
public function flush()
{
return $this->delTree($this->cache_dir);
}
/**
* 递归删除目录
* @param $dir
* @return bool
*/
function delTree($dir)
{
$files = array_diff(scandir($dir), array('.', '..'));
foreach ($files as $file) {
(is_dir("$dir/$file")) ? $this->delTree("$dir/$file") : unlink("$dir/$file");
}
return rmdir($dir);
}
/**
* 根据key获取缓存文件路径
*
* @param string $key
* @return string
*/
protected function path($key)
{
$parts = array_slice(str_split($hash = md5($key), 2), 0, 2);
return $this->cache_dir . '/' . implode('/', $parts) . '/' . $hash;
}
/**
* 获取含有元数据的信息
* @param $key
* @return bool|mixed|string
*/
protected function getItem($key)
{
$cache_file = $this->path($key);
if (!file_exists($cache_file) || !is_readable($cache_file)) {
return false;
}
$data = file_get_contents($cache_file);
if (empty($data)) return false;
$cache_data = unserialize($data);
if ($cache_data === false) {
return false;
}
$check_expire = $this->checkExpire($cache_data);
if ($check_expire === false) {
$this->delete($key);
return false;
}
return $cache_data;
}
/**
* 检查key是否过期
* @param $cache_data
* @return bool
*/
protected function checkExpire($cache_data)
{
$time = time();
$is_expire = intval($cache_data['expire']) !== 0 && (intval($cache_data['time']) + intval($cache_data['expire']) < $time);
if ($is_expire) return false;
return true;
}
}

View File

@@ -0,0 +1,126 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/20
* Time: 15:14
*/
namespace Jenner\SimpleFork\Cache;
/**
* redis cache
*
* @package Jenner\SimpleFork\Cache
*/
class RedisCache implements CacheInterface
{
/**
* @var \Redis
*/
protected $redis;
protected $prefix;
/**
* @param string $host
* @param int $port
* @param int $database
* @param string $prefix
*/
public function __construct(
$host = '127.0.0.1',
$port = 6379,
$database = 0,
$prefix = 'simple-fork'
)
{
$this->redis = new \Redis();
$connection_result = $this->redis->connect($host, $port);
if (!$connection_result) {
throw new \RuntimeException('can not connect to the redis server');
}
if ($database != 0) {
$select_result = $this->redis->select($database);
if (!$select_result) {
throw new \RuntimeException('can not select the database');
}
}
if (empty($prefix)) {
throw new \InvalidArgumentException('prefix can not be empty');
}
$this->prefix = $prefix;
}
/**
* close redis connection
*/
public function __destruct()
{
$this->close();
}
/**
* close the connection
*/
public function close()
{
$this->redis->close();
}
/**
* get var
*
* @param $key
* @param null $default
* @return bool|string|null
*/
public function get($key, $default = null)
{
$result = $this->redis->hGet($this->prefix, $key);
if ($result !== false) return $result;
return $default;
}
/**
* set var
*
* @param $key
* @param null $value
* @return boolean
*/
public function set($key, $value)
{
return $this->redis->hSet($this->prefix, $key, $value);
}
/**
* has var ?
*
* @param $key
* @return bool
*/
public function has($key)
{
return $this->redis->hExists($this->prefix, $key);
}
/**
* delete var
*
* @param $key
* @return bool
*/
public function delete($key)
{
if ($this->redis->hDel($this->prefix, $key) > 0) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,176 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/12
* Time: 15:00
*/
namespace Jenner\SimpleFork\Cache;
/**
* shared memory cache
*
* @package Jenner\SimpleFork\Cache
*/
class SharedMemory implements CacheInterface
{
/**
* holds shared memory resource
* @var resource
*/
protected $shm;
/**
* shared memory ipc key
* @var string
*/
protected $client_count_key = 'system_client_count';
/**
* memory size
* @var int
*/
protected $size;
/**
* @param int $size memory size
* @param string $file
*/
public function __construct($size = 33554432, $file = __FILE__)
{
$this->size = $size;
if (function_exists("shm_attach") === false) {
$message = "\nYour PHP configuration needs adjustment. " .
"See: http://us2.php.net/manual/en/shmop.setup.php. " .
"To enable the System V shared memory support compile " .
" PHP with the option --enable-sysvshm.";
throw new \RuntimeException($message);
}
$this->attach($file); //create resources (shared memory)
}
/**
* connect shared memory
*
* @param string $file
*/
public function attach($file = __FILE__)
{
if (!file_exists($file)) {
$touch = touch($file);
if (!$touch) {
throw new \RuntimeException("file is not exists and it can not be created. file: {$file}");
}
}
$key = ftok($file, 'a');
$this->shm = shm_attach($key, $this->size); //allocate shared memory
}
/**
* remove shared memory.
* you should know that it maybe does not work.
*
* @return bool
*/
public function remove()
{
//dallocate shared memory
if (!shm_remove($this->shm)) {
return false;
}
$this->dettach();
// shm_remove maybe not working. it likes a php bug.
unset($this->shm);
return true;
}
/**
* @return bool
*/
public function dettach()
{
return shm_detach($this->shm); //allocate shared memory
}
/**
* set var
*
* @param $key
* @param $value
* @return bool
*/
public function set($key, $value)
{
return shm_put_var($this->shm, $this->shm_key($key), $value); //store var
}
/**
* generate shm key
*
* @param $val
* @return mixed
*/
public function shm_key($val)
{ // enable all world langs and chars !
// text to number system.
return preg_replace("/[^0-9]/", "", (preg_replace("/[^0-9]/", "", md5($val)) / 35676248) / 619876);
}
/**
* get var
*
* @param $key
* @param null $default
* @return bool|mixed
*/
public function get($key, $default = null)
{
if ($this->has($key)) {
return shm_get_var($this->shm, $this->shm_key($key));
} else {
return $default;
}
}
/**
* has var ?
*
* @param $key
* @return bool
*/
public function has($key)
{
if (shm_has_var($this->shm, $this->shm_key($key))) { // check is isset
return true;
} else {
return false;
}
}
/**
* delete var
*
* @param $key
* @return bool
*/
public function delete($key)
{
if ($this->has($key)) {
return shm_remove_var($this->shm, $this->shm_key($key));
} else {
return false;
}
}
/**
* init when wakeup
*/
public function __wakeup()
{
$this->attach();
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/11/2
* Time: 17:45
*/
namespace Jenner\SimpleFork;
/**
* fixed pool
*
* @package Jenner\SimpleFork
*/
class FixedPool extends AbstractPool
{
/**
* @var int max process count
*/
protected $max;
/**
* @param int $max
*/
public function __construct($max = 4)
{
$this->max = $max;
}
public function execute(Process $process)
{
Utils::checkOverwriteRunMethod(get_class($process));
if ($this->aliveCount() < $this->max && !$process->isStarted()) {
$process->start();
}
array_push($this->processes, $process);
}
/**
* wait for all process done
*
* @param bool $block block the master process
* to keep the sub process count all the time
* @param int $interval check time interval
*/
public function wait($block = false, $interval = 100)
{
do {
if ($this->isFinished()) {
return;
}
parent::wait(false);
if ($this->aliveCount() < $this->max) {
foreach ($this->processes as $process) {
if ($process->isStarted()) continue;
$process->start();
if ($this->aliveCount() >= $this->max) break;
}
}
$block ? usleep($interval) : null;
} while ($block);
}
}

View File

@@ -0,0 +1,126 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/21
* Time: 14:30
*/
namespace Jenner\SimpleFork\Lock;
/**
* file lock
*
* @package Jenner\SimpleFork\Lock
*/
class FileLock implements LockInterface
{
/**
* @var string lock file
*/
protected $file;
/**
* @var resource
*/
protected $fp;
/**
* @var bool
*/
protected $locked = false;
/**
* @param $file
*/
private function __construct($file)
{
if (!file_exists($file) || !is_readable($file)) {
throw new \RuntimeException("{$file} is not exists or not readable");
}
$this->fp = fopen($file, "r+");
if (!is_resource($this->fp)) {
throw new \RuntimeException("open {$file} failed");
}
}
/**
* create a file lock instance
* if the file is not exists, it will be created
*
* @param string $file lock file
* @return FileLock
*/
public static function create($file)
{
return new FileLock($file);
}
/**
* get a lock
*
* @param bool $blocking
* @return mixed
*/
public function acquire($blocking = true)
{
if ($this->locked) {
throw new \RuntimeException('already lock by yourself');
}
if ($blocking) {
$locked = flock($this->fp, LOCK_EX);
} else {
$locked = flock($this->fp, LOCK_EX | LOCK_NB);
}
if ($locked !== true) {
return false;
}
$this->locked = true;
return true;
}
/**
* is locked
*
* @return mixed
*/
public function isLocked()
{
return $this->locked === true ? true : false;
}
/**
*
*/
public function __destory()
{
if ($this->locked) {
$this->release();
}
}
/**
* release lock
*
* @return mixed
*/
public function release()
{
if (!$this->locked) {
throw new \RuntimeException('release a non lock');
}
$unlock = flock($this->fp, LOCK_UN);
fclose($this->fp);
if ($unlock !== true) {
return false;
}
$this->locked = false;
return true;
}
}

View File

@@ -0,0 +1,40 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/21
* Time: 14:24
*/
namespace Jenner\SimpleFork\Lock;
/**
* lock for processes to mutual exclusion
*
* @package Jenner\SimpleFork\Lock
*/
interface LockInterface
{
/**
* get a lock
*
* @param bool $blocking
* @return bool
*/
public function acquire($blocking = true);
/**
* release lock
*
* @return bool
*/
public function release();
/**
* is locked
*
* @return bool
*/
public function isLocked();
}

View File

@@ -0,0 +1,163 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/12
* Time: 20:52
*/
namespace Jenner\SimpleFork\Lock;
/**
* sem lock
*
* @package Jenner\SimpleFork\Lock
*/
class Semaphore implements LockInterface
{
/**
* @var
*/
private $lock_id;
/**
* @var bool
*/
private $locked = false;
/**
* init a lock
*
* @param $key
* @param $count
* @throws \RuntimeException
*/
private function __construct($key, $count = 1)
{
if (($this->lock_id = sem_get($this->_stringToSemKey($key), $count)) === false) {
throw new \RuntimeException("Cannot create semaphore for key: {$key}");
}
}
/**
* Semaphore requires a numeric value as the key
*
* @param $identifier
* @return int
*/
protected function _stringToSemKey($identifier)
{
$md5 = md5($identifier);
$key = 0;
for ($i = 0; $i < 32; $i++) {
$key += ord($md5{$i}) * $i;
}
return $key;
}
/**
* create a lock instance
*
* @param $key
* @return Semaphore
*/
public static function create($key)
{
return new Semaphore($key);
}
/**
* release lock
*
* @throws \RuntimeException
*/
public function __destruct()
{
if ($this->isLocked()) {
$this->release();
}
}
/**
* is locked
*
* @return bool
*/
public function isLocked()
{
return $this->locked === true ? true : false;
}
/**
* release lock
*
* @return bool
* @throws \RuntimeException
*/
public function release()
{
if (!$this->locked) {
throw new \RuntimeException("release a non lock");
}
if (!sem_release($this->lock_id)) {
return false;
}
$this->locked = false;
return true;
}
/**
* get a lock
*
* @param bool $blocking
* @return bool
*/
public function acquire($blocking = true)
{
if ($this->locked) {
throw new \RuntimeException('already lock by yourself');
}
if ($blocking === false) {
if (version_compare(PHP_VERSION, '5.6.0') < 0) {
throw new \RuntimeException('php version is at least 5.6.0 for param blocking');
}
if (!sem_acquire($this->lock_id, true)) {
return false;
}
$this->locked = true;
return true;
}
if (!sem_acquire($this->lock_id)) {
return false;
}
$this->locked = true;
return true;
}
/**
* remove the semaphore resource
*
* @return bool
*/
public function remove()
{
if ($this->locked) {
throw new \RuntimeException('can not remove a locked semaphore resource');
}
if (!is_resource($this->lock_id)) {
throw new \RuntimeException('can not remove a empty semaphore resource');
}
if (!sem_release($this->lock_id)) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,106 @@
<?php
/**
* @author Jenner <hypxm@qq.com>
* @blog http://www.huyanping.cn
* @license https://opensource.org/licenses/MIT MIT
* @datetime: 2015/11/19 20:49
*/
namespace Jenner\SimpleFork;
/**
* parallel pool
*
* @package Jenner\SimpleFork
*/
class ParallelPool extends AbstractPool
{
/**
* @var callable|Runnable sub process callback
*/
protected $runnable;
/**
* @var int max process count
*/
protected $max;
/**
* @param callable|Runnable $callback
* @param int $max
*/
public function __construct($callback, $max = 4)
{
if (!is_callable($callback) && !($callback instanceof Runnable)) {
throw new \InvalidArgumentException('callback must be a callback function or a object of Runnalbe');
}
$this->runnable = $callback;
$this->max = $max;
}
/**
* start the same number processes and kill the old sub process
* just like nginx -s reload
* this method will block until all the old process exit;
*
* @param bool $block
*/
public function reload($block = true)
{
$old_processes = $this->processes;
for ($i = 0; $i < $this->max; $i++) {
$process = new Process($this->runnable);
$process->start();
$this->processes[$process->getPid()] = $process;
}
foreach ($old_processes as $process) {
$process->shutdown();
$process->wait($block);
unset($this->processes[$process->getPid()]);
}
}
/**
* keep sub process count
*
* @param bool $block block the master process
* to keep the sub process count all the time
* @param int $interval check time interval
*/
public function keep($block = false, $interval = 100)
{
do {
$this->start();
// recycle sub process and delete the processes
// which are not running from process list
foreach ($this->processes as $process) {
if (!$process->isRunning()) {
unset($this->processes[$process->getPid()]);
}
}
$block ? usleep($interval) : null;
} while ($block);
}
/**
* start the pool
*/
public function start()
{
$alive_count = $this->aliveCount();
// create sub process and run
if ($alive_count < $this->max) {
$need = $this->max - $alive_count;
for ($i = 0; $i < $need; $i++) {
$process = new Process($this->runnable);
$process->start();
$this->processes[$process->getPid()] = $process;
}
}
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/12
* Time: 17:54
*/
namespace Jenner\SimpleFork;
/**
* pool
*
* @package Jenner\SimpleFork
*/
class Pool extends AbstractPool
{
/**
* add a process
*
* @param Process $process
* @param null|string $name process name
* @return int
*/
public function execute(Process $process, $name = null)
{
if (!is_null($name)) {
$process->name($name);
}
if (!$process->isStarted()) {
$process->start();
}
return array_push($this->processes, $process);
}
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* @author Jenner <hypxm@qq.com>
* @blog http://www.huyanping.cn
* @license https://opensource.org/licenses/MIT MIT
* @datetime: 2015/11/19 21:14
*/
namespace Jenner\SimpleFork;
class PoolFactory
{
/**
* create a pool instance
*
* @return Pool
*/
public static function newPool()
{
return new Pool();
}
/**
* create a fixed pool instance
*
* @param int $max
* @return FixedPool
*/
public static function newFixedPool($max = 4)
{
return new FixedPool($max);
}
/**
* create a parallel pool instance
*
* @param $callback
* @param int $max
* @return ParallelPool
*/
public static function newParallelPool($callback, $max = 4)
{
return new ParallelPool($callback, $max);
}
/**
* create a single pool
*
* @return SinglePool
*/
public static function newSinglePool()
{
return new SinglePool();
}
}

View File

@@ -0,0 +1,373 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/12
* Time: 15:25
*/
namespace Jenner\SimpleFork;
class Process
{
/**
* @var Runnable|callable
*/
protected $runnable;
/**
* @var int
*/
protected $pid = 0;
/**
* @var string custom process name
*/
protected $name = null;
/**
* @var bool if the process is started
*/
protected $started = false;
/**
* @var bool
*/
protected $running = false;
/**
* @var int the signal which made the process terminate
*/
protected $term_signal = null;
/**
* @var int the signal which made the process stop
*/
protected $stop_signal = null;
/**
* @var int error code
*/
protected $errno = null;
/**
* @var string error message
*/
protected $errmsg = null;
/**
* @var bool
*/
protected $if_signal = false;
/**
* @var array
*/
protected $callbacks = array();
/**
* @var array signal handlers
*/
protected $signal_handlers = array();
/**
* @param string $execution it can be a Runnable object, callback function or null
* @param null $name process name,you can manager the process by it's name.
*/
public function __construct($execution = null, $name = null)
{
if (!is_null($execution) && $execution instanceof Runnable) {
$this->runnable = $execution;
} elseif (!is_null($execution) && is_callable($execution)) {
$this->runnable = $execution;
} elseif (!is_null($execution)) {
throw new \InvalidArgumentException('param execution is not a object of Runnable or callable');
} else {
Utils::checkOverwriteRunMethod(get_class($this));
}
if (!is_null($name)) {
$this->name = $name;
}
$this->initStatus();
}
/**
* init process status
*/
protected function initStatus()
{
$this->pid = null;
$this->running = null;
$this->term_signal = null;
$this->stop_signal = null;
$this->errno = null;
$this->errmsg = null;
}
/**
* get pid
*
* @return int
*/
public function getPid()
{
return $this->pid;
}
/**
* get or set name
*
* @param string|null $name
* @return mixed
*/
public function name($name = null)
{
if (!is_null($name)) {
$this->name = $name;
} else {
return $this->name;
}
}
/**
* if the process is stopped
*
* @return bool
*/
public function isStopped()
{
if (is_null($this->errno)) {
return false;
}
return true;
}
/**
* if the process is started
*
* @return bool
*/
public function isStarted()
{
return $this->started;
}
/**
* get pcntl errno
*
* @return int
*/
public function errno()
{
return $this->errno;
}
/**
* get pcntl errmsg
*
* @return string
*/
public function errmsg()
{
return $this->errmsg;
}
public function ifSignal()
{
return $this->if_signal;
}
/**
* start the sub process
* and run the callback
*
* @return string pid
*/
public function start()
{
if (!empty($this->pid) && $this->isRunning()) {
throw new \LogicException("the process is already running");
}
$callback = $this->getCallable();
$pid = pcntl_fork();
if ($pid < 0) {
throw new \RuntimeException("fork error");
} elseif ($pid > 0) {
$this->pid = $pid;
$this->running = true;
$this->started = true;
} else {
$this->pid = getmypid();
$this->signal();
foreach ($this->signal_handlers as $signal => $handler) {
pcntl_signal($signal, $handler);
}
call_user_func($callback);
exit(0);
}
}
/**
* if the process is running
*
* @return bool
*/
public function isRunning()
{
$this->updateStatus();
return $this->running;
}
/**
* update the process status
*
* @param bool $block
*/
protected function updateStatus($block = false)
{
if ($this->running !== true) {
return;
}
if ($block) {
$res = pcntl_waitpid($this->pid, $status);
} else {
$res = pcntl_waitpid($this->pid, $status, WNOHANG | WUNTRACED);
}
if ($res === -1) {
throw new \RuntimeException('pcntl_waitpid failed. the process maybe available');
} elseif ($res === 0) {
$this->running = true;
} else {
if (pcntl_wifsignaled($status)) {
$this->term_signal = pcntl_wtermsig($status);
}
if (pcntl_wifstopped($status)) {
$this->stop_signal = pcntl_wstopsig($status);
}
if (pcntl_wifexited($status)) {
$this->errno = pcntl_wexitstatus($status);
$this->errmsg = pcntl_strerror($this->errno);
} else {
$this->errno = pcntl_get_last_error();
$this->errmsg = pcntl_strerror($this->errno);
}
if (pcntl_wifsignaled($status)) {
$this->if_signal = true;
} else {
$this->if_signal = false;
}
$this->running = false;
}
}
/**
* get sub process callback
*
* @return array|callable|null
*/
protected function getCallable()
{
$callback = null;
if (is_object($this->runnable) && $this->runnable instanceof Runnable) {
$callback = array($this->runnable, 'run');
} elseif (is_callable($this->runnable)) {
$callback = $this->runnable;
} else {
$callback = array($this, 'run');
}
return $callback;
}
/**
* register signal SIGTERM handler,
* when the parent process call shutdown and use the default signal,
* this handler will be triggered
*/
protected function signal()
{
pcntl_signal(SIGTERM, function () {
exit(0);
});
}
/**
* kill self
*
* @param bool|true $block
* @param int $signal
*/
public function shutdown($block = true, $signal = SIGTERM)
{
if (empty($this->pid)) {
throw new \LogicException('the process pid is null, so maybe the process is not started');
}
if (!$this->isRunning()) {
throw new \LogicException("the process is not running");
}
if (!posix_kill($this->pid, $signal)) {
throw new \RuntimeException("kill son process failed");
}
$this->updateStatus($block);
}
/**
* waiting for the sub process exit
*
* @param bool|true $block if block the process
* @param int $sleep default 0.1s check sub process status
* every $sleep milliseconds.
*/
public function wait($block = true, $sleep = 100000)
{
while (true) {
if ($this->isRunning() === false) {
return;
}
if (!$block) {
break;
}
usleep($sleep);
}
}
/**
* register sub process signal handler,
* when the sub process start, the handlers will be registered
*
* @param $signal
* @param callable $handler
*/
public function registerSignalHandler($signal, callable $handler)
{
$this->signal_handlers[$signal] = $handler;
}
/**
* after php-5.3.0, we can call pcntl_singal_dispatch to call signal handlers for pending signals
* which can save cpu resources than using declare(tick=n)
*
* @return bool
*/
public function dispatchSignal()
{
return pcntl_signal_dispatch();
}
/**
* you should overwrite this function
* if you do not use the Runnable or callback.
*/
public function run()
{
}
}

View File

@@ -0,0 +1,143 @@
<?php
/**
* @author Jenner <hypxm@qq.com>
* @blog http://www.huyanping.cn
* @license https://opensource.org/licenses/MIT MIT
* @datetime: 2015/11/24 16:29
*/
namespace Jenner\SimpleFork\Queue;
class Pipe
{
/**
* @var resource
*/
protected $read;
/**
* @var resource
*/
protected $write;
/**
* @var string
*/
protected $filename;
/**
* @var bool
*/
protected $block;
/**
* @param string $filename fifo filename
* @param int $mode
* @param bool $block if blocking
*/
public function __construct($filename = '/tmp/simple-fork.pipe', $mode = 0666, $block = false)
{
if (!file_exists($filename) && !posix_mkfifo($filename, $mode)) {
throw new \RuntimeException('create pipe failed');
}
if (filetype($filename) != 'fifo') {
throw new \RuntimeException('file exists and it is not a fifo file');
}
$this->filename = $filename;
$this->block = $block;
}
public function setBlock($block = true)
{
if (is_resource($this->read)) {
$set = stream_set_blocking($this->read, $block);
if (!$set) {
throw new \RuntimeException('stream_set_blocking failed');
}
}
if (is_resource($this->write)) {
$set = stream_set_blocking($this->write, $block);
if (!$set) {
throw new \RuntimeException('stream_set_blocking failed');
}
}
$this->block = $block;
}
/**
* if the stream is blocking, you would better set the value of size,
* it will not return until the data size is equal to the value of param size
*
* @param int $size
* @return string
*/
public function read($size = 1024)
{
if (!is_resource($this->read)) {
$this->read = fopen($this->filename, 'r+');
if (!is_resource($this->read)) {
throw new \RuntimeException('open file failed');
}
if (!$this->block) {
$set = stream_set_blocking($this->read, false);
if (!$set) {
throw new \RuntimeException('stream_set_blocking failed');
}
}
}
return fread($this->read, $size);
}
/**
* @param $message
* @return int
*/
public function write($message)
{
if (!is_resource($this->write)) {
$this->write = fopen($this->filename, 'w+');
if (!is_resource($this->write)) {
throw new \RuntimeException('open file failed');
}
if (!$this->block) {
$set = stream_set_blocking($this->write, false);
if (!$set) {
throw new \RuntimeException('stream_set_blocking failed');
}
}
}
return fwrite($this->write, $message);
}
/**
*
*/
public function __destruct()
{
$this->close();
}
/**
*
*/
public function close()
{
if (is_resource($this->read)) {
fclose($this->read);
}
if (is_resource($this->write)) {
fclose($this->write);
}
}
public function remove()
{
return unlink($this->filename);
}
}

View File

@@ -0,0 +1,104 @@
<?php
/**
* @author Jenner <hypxm@qq.com>
* @blog http://www.huyanping.cn
* @license https://opensource.org/licenses/MIT MIT
* @datetime: 2015/11/24 18:38
*/
namespace Jenner\SimpleFork\Queue;
class PipeQueue implements QueueInterface
{
/**
* @var Pipe
*/
protected $pipe;
/**
* @var bool
*/
protected $block;
/**
* @param string $filename fifo filename
* @param int $mode
* @param bool $block if blocking
*/
public function __construct($filename = '/tmp/simple-fork.pipe', $mode = 0666)
{
$this->pipe = new Pipe($filename, $mode);
$this->block = false;
$this->pipe->setBlock($this->block);
}
/**
* put value into the queue of channel
*
* @param $value
* @return bool
*/
public function put($value)
{
$len = strlen($value);
if ($len > 2147483647) {
throw new \RuntimeException('value is too long');
}
$raw = pack('N', $len) . $value;
$write_len = $this->pipe->write($raw);
return $write_len == strlen($raw);
}
/**
* get value from the queue of channel
*
* @param bool $block if block when the queue is empty
* @return bool|string
*/
public function get($block = false)
{
if ($this->block != $block) {
$this->pipe->setBlock($block);
$this->block = $block;
}
$len = $this->pipe->read(4);
if ($len === false) {
throw new \RuntimeException('read pipe failed');
}
if (strlen($len) === 0) {
return null;
}
$len = unpack('N', $len);
if (empty($len) || !array_key_exists(1, $len) || empty($len[1])) {
throw new \RuntimeException('data protocol error');
}
$len = intval($len[1]);
$value = '';
while (true) {
$temp = $this->pipe->read($len);
if (strlen($temp) == $len) {
return $temp;
}
$value .= $temp;
$len -= strlen($temp);
if ($len == 0) {
return $value;
}
}
}
/**
* remove the queue resource
*
* @return bool
*/
public function remove()
{
$this->pipe->close();
$this->pipe->remove();
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/12
* Time: 15:11
*/
namespace Jenner\SimpleFork\Queue;
/**
* queue for processes to transfer data
*
* @package Jenner\SimpleFork\Queue
*/
interface QueueInterface
{
/**
* put value into the queue of channel
*
* @param $value
* @return bool
*/
public function put($value);
/**
* get value from the queue of channel
*
* @param bool $block if block when the queue is empty
* @return bool|string
*/
public function get($block = false);
}

View File

@@ -0,0 +1,144 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/20
* Time: 15:03
*/
namespace Jenner\SimpleFork\Queue;
/**
* redis queue
*
* @package Jenner\SimpleFork\Queue
*/
class RedisQueue implements QueueInterface
{
/**
* @var \Redis
*/
protected $redis;
/**
* @var string redis key of queue
*/
protected $channel;
/**
* @param string $host redis server host
* @param int $port redis server port
* @param int $database redis server database num
* @param string $channel redis queue key
* @param string $prefix prefix of redis queue key
*/
public function __construct(
$host = '127.0.0.1',
$port = 6379,
$database = 0,
$channel = 'cache',
$prefix = 'simple-fork-'
)
{
$this->redis = new \Redis();
$connection_result = $this->redis->connect($host, $port);
if (!$connection_result) {
throw new \RuntimeException('can not connect to the redis server');
}
if ($database != 0) {
$select_result = $this->redis->select($database);
if (!$select_result) {
throw new \RuntimeException('can not select the database');
}
}
if (empty($channel)) {
throw new \InvalidArgumentException('channel can not be empty');
}
$this->channel = $channel;
if (empty($prefix)) return;
$set_option_result = $this->redis->setOption(\Redis::OPT_PREFIX, $prefix);
if (!$set_option_result) {
throw new \RuntimeException('can not set the \Redis::OPT_PREFIX Option');
}
}
/**
* put value into the queue
*
* @param $value
* @return bool
*/
public function put($value)
{
if ($this->redis->lPush($this->channel, $value) !== false) {
return true;
}
return false;
}
/**
* get value from the queue
*
* @param bool $block if block when the queue is empty
* @return bool|string
*/
public function get($block = false)
{
if (!$block) {
return $this->redis->rPop($this->channel);
} else {
while (true) {
$record = $this->redis->rPop($this->channel);
if ($record === false) {
usleep(1000);
continue;
}
return $record;
}
}
}
/**
* get the size of the queue
*
* @return int
*/
public function size()
{
return $this->redis->lSize($this->channel);
}
/**
* remove the queue resource
*
* @return mixed
*/
public function remove()
{
return $this->redis->delete($this->channel);
}
/**
* close the connection
*/
public function __destruct()
{
$this->close();
}
/**
* close the connection
*/
public function close()
{
$this->redis->close();
}
}

View File

@@ -0,0 +1,293 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/12
* Time: 15:15
*/
namespace Jenner\SimpleFork\Queue;
/**
* system v message queue
*
* @package Jenner\SimpleFork\Queue
*/
class SystemVMessageQueue implements QueueInterface
{
/**
* @var int channel
*/
protected $msg_type;
/**
* @var
*/
protected $queue;
/**
* @var bool
*/
protected $serialize_needed;
/**
* @var bool
*/
protected $block_send;
/**
* @var int
*/
protected $option_receive;
/**
* @var int
*/
protected $maxsize;
/**
* @var
*/
protected $key_t;
/**
* @var string
*/
protected $ipc_filename;
/**
* @param string $ipc_filename ipc file to make ipc key.
* if it does not exists, it will try to create the file.
* @param int $channel message type
* @param bool $serialize_needed serialize or not
* @param bool $block_send if block when the queue is full
* @param int $option_receive if the value is MSG_IPC_NOWAIT it will not
* going to wait a message coming. if the value is null,
* it will block and wait a message
* @param int $maxsize the max size of queue
*/
public function __construct(
$ipc_filename = __FILE__,
$channel = 1,
$serialize_needed = true,
$block_send = true,
$option_receive = MSG_IPC_NOWAIT,
$maxsize = 100000
)
{
$this->ipc_filename = $ipc_filename;
$this->msg_type = $channel;
$this->serialize_needed = $serialize_needed;
$this->block_send = $block_send;
$this->option_receive = $option_receive;
$this->maxsize = $maxsize;
$this->initQueue($ipc_filename, $channel);
}
/**
* init queue
*
* @param $ipc_filename
* @param $msg_type
* @throws \Exception
*/
protected function initQueue($ipc_filename, $msg_type)
{
$this->key_t = $this->getIpcKey($ipc_filename, $msg_type);
$this->queue = \msg_get_queue($this->key_t);
if (!$this->queue) throw new \RuntimeException('msg_get_queue failed');
}
/**
* @param $ipc_filename
* @param $msg_type
* @throws \Exception
* @return int
*/
public function getIpcKey($ipc_filename, $msg_type)
{
if (!file_exists($ipc_filename)) {
$create_file = touch($ipc_filename);
if ($create_file === false) {
throw new \RuntimeException('ipc_file is not exists and create failed');
}
}
$key_t = \ftok($ipc_filename, $msg_type);
if ($key_t == 0) throw new \RuntimeException('ftok error');
return $key_t;
}
/**
* get message
*
* @param bool $block if block when the queue is empty
* @return bool|string
*/
public function get($block = false)
{
$queue_status = $this->status();
if ($queue_status['msg_qnum'] > 0) {
$option_receive = $block ? 0 : $this->option_receive;
if (\msg_receive(
$this->queue,
$this->msg_type,
$msgtype_erhalten,
$this->maxsize,
$data,
$this->serialize_needed,
$option_receive,
$err
) === true
) {
return $data;
} else {
throw new \RuntimeException($err);
}
} else {
return false;
}
}
public function status()
{
$queue_status = \msg_stat_queue($this->queue);
return $queue_status;
}
/*
* return array's keys
* msg_perm.uid The uid of the owner of the queue.
* msg_perm.gid The gid of the owner of the queue.
* msg_perm.mode The file access mode of the queue.
* msg_stime The time that the last message was sent to the queue.
* msg_rtime The time that the last message was received from the queue.
* msg_ctime The time that the queue was last changed.
* msg_qnum The number of messages waiting to be read from the queue.
* msg_qbytes The maximum number of bytes allowed in one message queue.
* On Linux, this value may be read and modified via /proc/sys/kernel/msgmnb.
* msg_lspid The pid of the process that sent the last message to the queue.
* msg_lrpid The pid of the process that received the last message from the queue.
*
* @return array
*/
/**
* put message
*
* @param $message
* @return bool
* @throws \Exception
*/
public function put($message)
{
if (!\msg_send($this->queue, $this->msg_type, $message, $this->serialize_needed, $this->block_send, $err) === true) {
throw new \RuntimeException($err);
}
return true;
}
/**
* get the size of queue
*
* @return mixed
*/
public function size()
{
$status = $this->status();
return $status['msg_qnum'];
}
/**
* allows you to change the values of the msg_perm.uid,
* msg_perm.gid, msg_perm.mode and msg_qbytes fields of the underlying message queue data structure
*
* @param string $key status key
* @param int $value status value
* @return bool
*/
public function setStatus($key, $value)
{
$this->checkSetPrivilege($key);
if ($key == 'msg_qbytes')
return $this->setMaxQueueSize($value);
$queue_status[$key] = $value;
return \msg_set_queue($this->queue, $queue_status);
}
/**
* check the privilege of update the queue's status
*
* @param $key
* @throws \Exception
*/
private function checkSetPrivilege($key)
{
$privilege_field = array('msg_perm.uid', 'msg_perm.gid', 'msg_perm.mode');
if (!\in_array($key, $privilege_field)) {
$message = 'you can only change msg_perm.uid, msg_perm.gid, ' .
' msg_perm.mode and msg_qbytes. And msg_qbytes needs root privileges';
throw new \RuntimeException($message);
}
}
/**
* update the max size of queue
* need root
*
* @param $size
* @throws \Exception
* @return bool
*/
public function setMaxQueueSize($size)
{
$user = \get_current_user();
if ($user !== 'root')
throw new \Exception('changing msg_qbytes needs root privileges');
return $this->setStatus('msg_qbytes', $size);
}
/**
* remove queue
*
* @return bool
*/
public function remove()
{
return \msg_remove_queue($this->queue);
}
/**
* check if the queue is exists or not
*
* @param $key
* @return bool
*/
public function queueExists($key)
{
return \msg_queue_exists($key);
}
/**
* init when wakeup
*/
public function __wakeup()
{
$this->initQueue($this->ipc_filename, $this->msg_type);
}
/**
*
*/
public function __destruct()
{
unset($this);
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/12
* Time: 15:28
*/
namespace Jenner\SimpleFork;
interface Runnable
{
/**
* process entry
*
* @return mixed
*/
public function run();
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* @author Jenner <hypxm@qq.com>
* @blog http://www.huyanping.cn
* @license https://opensource.org/licenses/MIT MIT
* @datetime: 2015/11/19 21:13
*/
namespace Jenner\SimpleFork;
/**
* Only one process will be started at the same time
*
* @package Jenner\SimpleFork
*/
class SinglePool extends FixedPool
{
/**
* SinglePool constructor.
*/
public function __construct()
{
parent::__construct(1);
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* @author Jenner <hypxm@qq.com>
* @license https://opensource.org/licenses/MIT MIT
* @datetime: 2015/11/11 17:50
*/
namespace Jenner\SimpleFork;
class Utils
{
/**
* check if the sub class of Process has overwrite the run method
*
* @param $child_class
*/
public static function checkOverwriteRunMethod($child_class)
{
$parent_class = '\\Jenner\\SimpleFork\\Process';
if ($child_class == $parent_class) {
$message = "you should extend the `{$parent_class}`" .
' and overwrite the run method';
throw new \RuntimeException($message);
}
$child = new \ReflectionClass($child_class);
if ($child->getParentClass() === false) {
$message = "you should extend the `{$parent_class}`" .
' and overwrite the run method';
throw new \RuntimeException($message);
}
$parent_methods = $child->getParentClass()->getMethods(\ReflectionMethod::IS_PUBLIC);
foreach ($parent_methods as $parent_method) {
if ($parent_method->getName() !== 'run') continue;
$declaring_class = $child->getMethod($parent_method->getName())
->getDeclaringClass()
->getName();
if ($declaring_class === $parent_class) {
throw new \RuntimeException('you must overwrite the run method');
}
}
}
}

View File

@@ -0,0 +1,19 @@
Copyright (c) 2011 Michael Dowling <mtdowling@gmail.com> and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Jenner
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,54 @@
<?php
namespace addons\crontab\model;
use think\Model;
class Crontab extends Model
{
// 开启自动写入时间戳字段
protected $autoWriteTimestamp = 'integer';
// 定义时间戳字段名
protected $createTime = 'createtime';
protected $updateTime = 'updatetime';
// 定义字段类型
protected $type = [
];
// 追加属性
protected $append = [
'type_text'
];
public static function getTypeList()
{
return [
'url' => __('Request Url'),
'sql' => __('Execute Sql Script'),
'shell' => __('Execute Shell'),
];
}
public function getTypeTextAttr($value, $data)
{
$typelist = self::getTypeList();
$value = $value ? $value : $data['type'];
return $value && isset($typelist[$value]) ? $typelist[$value] : $value;
}
protected function setBegintimeAttr($value)
{
return $value && !is_numeric($value) ? strtotime($value) : $value;
}
protected function setEndtimeAttr($value)
{
return $value && !is_numeric($value) ? strtotime($value) : $value;
}
protected function setExecutetimeAttr($value)
{
return $value && !is_numeric($value) ? strtotime($value) : $value;
}
}

1
addons/epay/.addonrc Normal file
View File

@@ -0,0 +1 @@
{"files":["application\/admin\/controller\/Epay.php","public\/assets\/addons\/epay\/less\/common.less","public\/assets\/addons\/epay\/js\/jquery.qrcode.min.js","public\/assets\/addons\/epay\/js\/common.js","public\/assets\/addons\/epay\/css\/common.css","public\/assets\/addons\/epay\/images\/paid.png","public\/assets\/addons\/epay\/images\/scan.png","public\/assets\/addons\/epay\/images\/logo-alipay.png","public\/assets\/addons\/epay\/images\/logo-wechat.png","public\/assets\/addons\/epay\/images\/screenshot-alipay.png","public\/assets\/addons\/epay\/images\/alipay.png","public\/assets\/addons\/epay\/images\/screenshot-wechat.png","public\/assets\/addons\/epay\/images\/wechat.png","public\/assets\/addons\/epay\/images\/expired.png"],"license":"regular","licenseto":"101612","licensekey":"g5nZMF4RcJTD3I2d idWWgXE4jbNMVib2T4zioQ==","domains":["45.221"],"licensecodes":[],"validations":["01aa56d41a1a59da181975a3a6c82078"]}

100
addons/epay/Epay.php Normal file
View File

@@ -0,0 +1,100 @@
<?php
namespace addons\epay;
use addons\epay\library\Service;
use think\Addons;
use think\Config;
use think\Loader;
/**
* 微信支付宝整合插件
*/
class Epay extends Addons
{
/**
* 插件安装方法
* @return bool
*/
public function install()
{
return true;
}
/**
* 插件卸载方法
* @return bool
*/
public function uninstall()
{
return true;
}
/**
* 插件启用方法
* @return bool
*/
public function enable()
{
return true;
}
/**
* 插件禁用方法
* @return bool
*/
public function disable()
{
return true;
}
// 支持自定义加载
public function epayConfigInit()
{
$this->actionBegin();
}
// 插件方法加载开始
public function addonActionBegin()
{
$this->actionBegin();
}
// 模块控制器方法加载开始
public function actionBegin()
{
//添加命名空间
if (!class_exists('\Yansongda\Pay\Pay')) {
//SDK版本
$version = Service::getSdkVersion();
$libraryDir = ADDON_PATH . 'epay' . DS . 'library' . DS;
Loader::addNamespace('Yansongda\Pay', $libraryDir . $version . DS . 'Yansongda' . DS . 'Pay' . DS);
$checkArr = [
'\Hyperf\Context\Context' => 'context',
'\Hyperf\Contract\Castable' => 'contract',
'\Hyperf\Engine\Constant' => 'engine',
'\Hyperf\Macroable\Macroable' => 'macroable',
'\Hyperf\Pimple\Container' => 'pimple',
'\Hyperf\Utils\Arr' => 'utils',
];
foreach ($checkArr as $index => $item) {
if (!class_exists($index)) {
Loader::addNamespace(substr($index, 1, strrpos($index, '\\') - 1), $libraryDir . 'hyperf' . DS . $item . DS . 'src' . DS);
}
}
if (!class_exists('\Yansongda\Supports\Logger')) {
Loader::addNamespace('Yansongda\Supports', $libraryDir . $version . DS . 'Yansongda' . DS . 'Supports' . DS);
}
// V3需载入辅助函数
if ($version == Service::SDK_VERSION_V3) {
require_once $libraryDir . $version . DS . 'Yansongda' . DS . 'Pay' . DS . 'Functions.php';
}
}
}
}

View File

450
addons/epay/config.html Normal file
View File

@@ -0,0 +1,450 @@
<form id="config-form" class="edit-form form-horizontal" role="form" data-toggle="validator" method="POST" action="">
<div class="panel panel-default panel-intro">
<div class="panel-heading">
<ul class="nav nav-tabs nav-group">
<li class="active"><a href="#wechat" data-toggle="tab">微信支付</a></li>
<li><a href="#alipay" data-toggle="tab">支付宝</a></li>
</ul>
</div>
<div class="panel-body">
<div id="myTabContent" class="tab-content">
{foreach $addon.config as $item}
{if $item.name=='version'}
<input type="hidden" value="{$item.value}" name="row[version]"/>
{elseif $item.name=='wechat'/}
<div class="tab-pane fade active in" id="wechat">
<table class="table table-striped table-config">
<tbody>
<tr>
<td width="20%">APP的app_id</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[wechat][appid]" value="{$item.value.appid|default=''}" class="form-control" data-rule="" data-tip="APP应用中支付时使用"/>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>公众号的app_id</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[wechat][app_id]" value="{$item.value.app_id|default=''}" class="form-control" data-rule="" data-tip="公众号中支付时使用"/>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>公众号的app_secret</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[wechat][app_secret]" value="{$item.value.app_secret|default=''}" class="form-control" data-rule="" data-tip="公众号中支付时使用"/>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>小程序的app_id</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[wechat][miniapp_id]" value="{$item.value.miniapp_id|default=''}" class="form-control" data-rule="" data-tip="仅在小程序支付时使用"/>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>微信支付商户号</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[wechat][mch_id]" value="{$item.value.mch_id|default=''}" class="form-control" data-rule="" data-tip=""/>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>微信支付商户API密钥V2</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[wechat][key]" value="{$item.value.key|default=''}" class="form-control" data-rule="" data-tip=""/>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>微信支付商户API密钥V3</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[wechat][key_v3]" value="{$item.value.key_v3|default=''}" class="form-control" data-rule="" data-tip=""/>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>支付模式</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
{:Form::radios('row[wechat][mode]',['normal'=>'正式环境','dev'=>'沙箱环境','service'=>'服务商模式'],$item.value.mode??'normal')}
<div style="margin-top:5px;" data-type="dev" class="text-muted {if ($item.value.mode??'')!=='dev'}hidden{/if}">
<i class="fa fa-info-circle"></i> 沙箱环境:<a href="https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=23_1&index=2" target="_blank">微信支付验收指引</a>
</div>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr data-type="service" class="{:$item.value.mode!='service'?'hidden':''}">
<td>子商户商户号ID</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[wechat][sub_mch_id]" value="{$item.value.sub_mch_id|default=''}" class="form-control" data-rule="" data-tip="如果未用到子商户,请勿填写"/>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr data-type="service" class="{:$item.value.mode!='service'?'hidden':''}">
<td>子商户APP的app_id</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[wechat][sub_appid]" value="{$item.value.sub_appid|default=''}" class="form-control" data-rule="" data-tip="如果未用到子商户,请勿填写"/>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr data-type="service" class="{:$item.value.mode!='service'?'hidden':''}">
<td>子商户公众号的app_id</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[wechat][sub_app_id]" value="{$item.value.sub_app_id|default=''}" class="form-control" data-rule="" data-tip="如果未用到子商户,请勿填写"/>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr data-type="service" class="{:$item.value.mode!='service'?'hidden':''}">
<td>子商户小程序的app_id</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[wechat][sub_miniapp_id]" value="{$item.value.sub_miniapp_id|default=''}" class="form-control" data-rule="" data-tip="如果未用到子商户,请勿填写"/>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>回调通知地址</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[wechat][notify_url]" value="{$item.value.notify_url|default=''}" class="form-control" data-rule="" data-tip="请勿随意修改,实际以逻辑代码中请求的为准"/>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>微信支付API证书cert</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<div class="input-group">
<input id="c-cert_client" class="form-control" size="50" name="row[wechat][cert_client]" type="text" value="{$item.value.cert_client|htmlentities}" data-tip="可选, 仅在退款、红包等情况时需要用到">
<div class="input-group-addon no-border no-padding">
<span><button type="button" id="faupload-cert_client" class="btn btn-danger faupload" data-url="epay/upload" data-multipart='{"certname":"cert_client"}' data-mimetype="pem" data-input-id="c-cert_client" data-multiple="false"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
</div>
<span class="msg-box n-right" for="c-cert_client"></span>
</div>
<div style="margin-top:5px;"><a href="https://pay.weixin.qq.com" target="_blank"><i class="fa fa-question-circle"></i> 如何获取微信支付API证书?</a></div>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>微信支付API证书key</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<div class="input-group">
<input id="c-cert_key" class="form-control" size="50" name="row[wechat][cert_key]" type="text" value="{$item.value.cert_key|htmlentities}" data-tip="可选, 仅在退款、红包等情况时需要用到">
<div class="input-group-addon no-border no-padding">
<span><button type="button" id="faupload-cert_key" class="btn btn-danger faupload" data-url="epay/upload" data-multipart='{"certname":"cert_key"}' data-mimetype="pem" data-input-id="c-cert_key" data-multiple="false"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
</div>
<span class="msg-box n-right" for="c-cert_key"></span>
</div>
<div style="margin-top:5px;"><a href="https://pay.weixin.qq.com" target="_blank"><i class="fa fa-question-circle"></i> 如何获取微信支付API证书?</a></div>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>记录日志</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
{:Form::radios('row[wechat][log]',['1'=>'开启','0'=>'关闭'],$item.value.log)}
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
{elseif $item.name=='alipay'}
<div class="tab-pane fade" id="alipay">
<table class="table table-striped table-config">
<tbody>
<tr>
<td>支付模式</td>
<td>
<div class="row">
<div class="col-sm-12 col-xs-12">
{:Form::radios('row[alipay][mode]',['normal'=>'正式环境','dev'=>'沙箱环境', 'service'=>'服务商模式'],$item.value.mode??'normal')}
<div style="margin-top:5px;" data-mode="dev" class="text-muted {if ($item.value.mode??'')!=='dev'}hidden{/if}">
<i class="fa fa-info-circle"></i> 如果使用沙箱环境务必使用沙箱的app_id和沙箱配置以及使用沙箱账号进行测试。<br>
沙箱环境:<a href="https://openhome.alipay.com/develop/sandbox/app" target="_blank">https://openhome.alipay.com/develop/sandbox/app</a>
</div>
</div>
</div>
</td>
</tr>
<tr class="text-muted {if ($item.value.mode??'')!=='service'}hidden{/if}" data-mode="service">
<td width="20%">服务商ID(pid)</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[alipay][pid]" value="{$item.value.pid|default=''}" class="form-control" data-rule="" data-tip=""/>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td width="20%">应用ID(app_id)</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[alipay][app_id]" value="{$item.value.app_id|default=''}" class="form-control" data-rule="" data-tip=""/>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>回调通知地址</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[alipay][notify_url]" value="{$item.value.notify_url|default=''}" class="form-control" data-rule="" data-tip="请勿随意修改,实际以逻辑代码中请求的为准"/>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>支付跳转地址</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[alipay][return_url]" value="{$item.value.return_url|default=''}" class="form-control" data-rule="" data-tip="请勿随意修改,实际以逻辑代码中请求的为准"/>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>应用私钥(private_key)</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[alipay][private_key]" value="{$item.value.private_key|default=''}" class="form-control" data-rule=""/>
<div style="margin-top:5px;"><a href="https://opensupport.alipay.com/support/helpcenter/207/201602469554" target="_blank"><i class="fa fa-question-circle"></i> 如何获取应用私钥?</a></div>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>签名方式</td>
<td>
<div>
<div class="radio">
<label for="row[alipay][signtype]-publickey"><input id="row[alipay][signtype]-publickey" name="row[alipay][signtype]" {if isset($item.value.signtype)&&$item.value.signtype=='publickey'}checked{/if} type="radio" value="publickey"> 普通公钥</label>
<label for="row[alipay][signtype]-cert"><input id="row[alipay][signtype]-cert" {if isset($item.value.signtype)&&$item.value.signtype=='cert'}checked{/if} name="row[alipay][signtype]" type="radio" value="cert"> 公钥证书</label>
</div>
</div>
<div style="margin:5px 0;" class="text-muted">
<i class="fa fa-info-circle"></i> 如果要使用转账、提现功能,则必须使用公钥证书
</div>
<div data-signtype="publickey" class="{if ($item.value.signtype??'')==='cert'}hidden{/if}">
<a href="https://opensupport.alipay.com/support/FAQ/65b9c843a8e10e054512d07dprod" target="_blank"><i class="fa fa-info-circle"></i> 如何生成支付宝公钥、应用私钥?</a>
</div>
<div data-signtype="cert" class="{if ($item.value.signtype??'')==='publickey'}hidden{/if}">
<a href="https://opensupport.alipay.com/support/FAQ/6718ab4563fae8044fe13dc7prod" target="_blank"><i class="fa fa-info-circle"></i> 支付宝公钥证书、应用公钥证书、支付宝根证书?</a>
</div>
</td>
</tr>
<tr>
<td>
<span data-signtype="publickey" class="{if ($item.value.signtype??'')==='cert'}hidden{/if}">支付宝公钥</span>
<span data-signtype="cert" class="{if ($item.value.signtype??'')==='publickey' || ($item.value.signtype??'')==''}hidden{/if}">支付宝公钥证书路径</span>
(alipay_public_key)
</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<div class="input-group">
<input id="c-ali_public_key" class="form-control" size="50" name="row[alipay][ali_public_key]" type="text" value="{$item.value.ali_public_key|default=''|htmlentities}" placeholder="普通公钥请直接粘贴,公钥证书请点击右侧的上传">
<div class="input-group-addon no-border no-padding {if ($item.value.signtype??'')==='publickey' || ($item.value.signtype??'')==''}hidden{/if}" data-signtype="cert">
<span><button type="button" id="faupload-ali_public_key" class="btn btn-danger faupload" data-url="epay/upload" data-multipart='{"certname":"ali_public_key"}' data-mimetype="crt" data-input-id="c-ali_public_key" data-multiple="false"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
</div>
<span class="msg-box n-right" for="c-ali_public_key"></span>
</div>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr class="{if ($item.value.signtype??'')==='publickey' || ($item.value.signtype??'')==''}hidden{/if}" data-signtype="cert">
<td>
<span data-signtype="publickey" class="{if ($item.value.signtype??'')==='cert'}hidden{/if}">应用公钥</span>
<span data-signtype="cert" class="{if ($item.value.signtype??'')==='publickey' || ($item.value.signtype??'')==''}hidden{/if}">应用公钥证书路径</span>
(app_cert_public_key)
</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<div class="input-group">
<input id="c-app_cert_public_key" class="form-control" size="50" name="row[alipay][app_cert_public_key]" type="text" value="{$item.value.app_cert_public_key|default=''|htmlentities}">
<div class="input-group-addon no-border no-padding {if ($item.value.signtype??'')==='publickey' || ($item.value.signtype??'')==''}hidden{/if}" data-signtype="cert">
<span><button type="button" id="faupload-app_cert_public_key" class="btn btn-danger faupload" data-url="epay/upload" data-multipart='{"certname":"app_cert_public_key"}' data-mimetype="crt" data-input-id="c-app_cert_public_key" data-multiple="false"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
</div>
<span class="msg-box n-right" for="c-app_cert_public_key"></span>
</div>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr class="{if ($item.value.signtype??'')==='publickey' || ($item.value.signtype??'')==''}hidden{/if}" data-signtype="cert">
<td>支付宝根证书路径(alipay_root_cert)</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<div class="input-group">
<input id="c-alipay_root_cert" class="form-control" size="50" name="row[alipay][alipay_root_cert]" type="text" value="{$item.value.alipay_root_cert|default=''|htmlentities}">
<div class="input-group-addon no-border no-padding">
<span><button type="button" id="faupload-alipay_root_cert" class="btn btn-danger faupload" data-url="epay/upload" data-multipart='{"certname":"alipay_root_cert"}' data-mimetype="crt" data-input-id="c-alipay_root_cert" data-multiple="false"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
</div>
<span class="msg-box n-right" for="c-alipay_root_cert"></span>
</div>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>记录日志</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
{:Form::radios('row[alipay][log]',['1'=>'开启','0'=>'关闭'],$item.value.log)}
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>PC端使用扫码支付</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
{:Form::radios('row[alipay][scanpay]',['1'=>'开启','0'=>'关闭'],$item.value.scanpay??0)}
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
{/if}
{/foreach}
<div class="form-group layer-footer">
<label class="control-label col-xs-12 col-sm-2"></label>
<div class="col-xs-12 col-sm-8">
<button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
<button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
</div>
</div>
</div>
</div>
</div>
</form>
<script>
require.callback = function () {
define('backend/addon', ['backend', 'form'], function (Backend, Form) {
var Controller = {
config: function () {
$(document).on("click", ".nav-group li a[data-toggle='tab']", function () {
if ($(this).attr("href") === "#all") {
$(".tab-pane").addClass("active in");
}
return;
});
$(document).on("click", "input[name='row[wechat][mode]']", function () {
$("#wechat [data-type]").addClass("hidden");
$("#wechat [data-type='" + $(this).val() + "']").removeClass("hidden");
});
$(document).on("click", "input[name='row[alipay][mode]']", function () {
$("#alipay [data-mode]").addClass("hidden");
$("#alipay [data-mode='" + $(this).val() + "']").removeClass("hidden");
});
$(document).on("click", "input[name='row[alipay][signtype]']", function () {
let value = $(this).val();
$("#alipay [data-signtype]").addClass("hidden");
$("#alipay [data-signtype='" + value + "']").removeClass("hidden");
});
Form.api.bindevent($("form[role=form]"), undefined, undefined, function () {
let value = $("input[name='row[alipay][signtype]']:checked").val();
// 如果选择了普通公钥,则需要清空未使用的应用公钥证书路径值和支付宝根证书路径值
if (value === 'publickey') {
$("#c-app_cert_public_key,#c-alipay_root_cert").val('');
}
});
}
};
return Controller;
});
};
</script>

70
addons/epay/config.php Normal file
View File

@@ -0,0 +1,70 @@
<?php
return [
[
'name' => 'version',
'title' => 'API版本(请勿修改该值)',
'type' => 'radio',
'content' => [],
'value' => 'v2',
'rule' => '',
'msg' => '',
'tip' => 'V2版本只支持微信支付V2密钥V3版本只支持微信支付V3密钥请勿修改该值',
'ok' => '',
'extend' => '',
],
[
'name' => 'wechat',
'title' => '微信',
'type' => 'array',
'content' => [],
'value' => [
'appid' => '',
'app_id' => '',
'app_secret' => '',
'miniapp_id' => '',
'mch_id' => '',
'key' => '',
'key_v3' => '',
'mode' => 'normal',
'sub_mch_id' => '',
'sub_appid' => '',
'sub_app_id' => '',
'sub_miniapp_id' => '',
'notify_url' => '',
'cert_client' => '/addons/epay/certs/apiclient_cert.pem',
'cert_key' => '/addons/epay/certs/apiclient_key.pem',
'log' => '1',
],
'rule' => 'required',
'msg' => '',
'tip' => '微信参数配置',
'ok' => '',
'extend' => '',
],
[
'name' => 'alipay',
'title' => '支付宝',
'type' => 'array',
'content' => [],
'value' => [
'app_id' => '',
'mode' => 'normal',
'notify_url' => '/addons/epay/api/notifyx/type/alipay',
'return_url' => '/addons/epay/api/returnx/type/alipay',
'private_key' => '',
'signtype' => 'cert',
'pid' => '',
'ali_public_key' => '',
'app_cert_public_key' => '',
'alipay_root_cert' => '',
'log' => '1',
'scanpay' => '0',
],
'rule' => 'required',
'msg' => '',
'tip' => '支付宝参数配置',
'ok' => '',
'extend' => '',
]
];

View File

@@ -0,0 +1,234 @@
<?php
namespace addons\epay\controller;
use addons\epay\library\Service;
use addons\epay\library\Wechat;
use addons\third\model\Third;
use app\common\library\Auth;
use Exception;
use think\addons\Controller;
use think\Response;
use think\Session;
use Yansongda\Pay\Exceptions\GatewayException;
use Yansongda\Pay\Pay;
/**
* API接口控制器
*
* @package addons\epay\controller
*/
class Api extends Controller
{
protected $layout = 'default';
protected $config = [];
/**
* 默认方法
*/
public function index()
{
return;
}
/**
* 外部提交
*/
public function submit()
{
$this->request->filter('trim');
$out_trade_no = $this->request->request("out_trade_no");
$title = $this->request->request("title");
$amount = $this->request->request('amount');
$type = $this->request->request('type', $this->request->request('paytype'));
$method = $this->request->request('method', 'web');
$openid = $this->request->request('openid', '');
$auth_code = $this->request->request('auth_code', '');
$notifyurl = $this->request->request('notifyurl', '');
$returnurl = $this->request->request('returnurl', '');
if (!$amount || $amount < 0) {
$this->error("支付金额必须大于0");
}
if (!$type || !in_array($type, ['alipay', 'wechat'])) {
$this->error("支付类型错误");
}
$params = [
'type' => $type,
'out_trade_no' => $out_trade_no,
'title' => $title,
'amount' => $amount,
'method' => $method,
'openid' => $openid,
'auth_code' => $auth_code,
'notifyurl' => $notifyurl,
'returnurl' => $returnurl,
];
return Service::submitOrder($params);
}
/**
* 微信支付(公众号支付&PC扫码支付)
*/
public function wechat()
{
$config = Service::getConfig('wechat');
$isWechat = stripos($this->request->server('HTTP_USER_AGENT'), 'MicroMessenger') !== false;
$isMobile = $this->request->isMobile();
$this->view->assign("isWechat", $isWechat);
$this->view->assign("isMobile", $isMobile);
//发起PC支付(Scan支付)(PC扫码模式)
if ($this->request->isAjax()) {
$pay = Pay::wechat($config);
$orderid = $this->request->post("orderid");
try {
$result = Service::isVersionV3() ? $pay->find(['out_trade_no' => $orderid]) : $pay->find($orderid, 'scan');
$this->success("", "", ['status' => $result['trade_state'] ?? 'NOTPAY']);
} catch (GatewayException $e) {
$this->error("查询失败(1001)");
}
}
$orderData = Session::get("wechatorderdata");
if (!$orderData) {
$this->error("请求参数错误");
}
if ($isWechat && $isMobile) {
//发起公众号(jsapi支付),openid必须
//如果没有openid则自动去获取openid
if (!isset($orderData['openid']) || !$orderData['openid']) {
$orderData['openid'] = Service::getOpenid();
}
$orderData['method'] = 'mp';
$type = 'jsapi';
$payData = Service::submitOrder($orderData);
if (!isset($payData['paySign'])) {
$this->error("创建订单失败,请返回重试", "");
}
} else {
$orderData['method'] = 'scan';
$type = 'pc';
$payData = Service::submitOrder($orderData);
if (!isset($payData['code_url'])) {
$this->error("创建订单失败,请返回重试", "");
}
}
$this->view->assign("orderData", $orderData);
$this->view->assign("payData", $payData);
$this->view->assign("type", $type);
$this->view->assign("title", "微信支付");
return $this->view->fetch();
}
/**
* 支付宝支付(PC扫码支付)
*/
public function alipay()
{
$config = Service::getConfig('alipay');
$isWechat = stripos($this->request->server('HTTP_USER_AGENT'), 'MicroMessenger') !== false;
$isMobile = $this->request->isMobile();
$this->view->assign("isWechat", $isWechat);
$this->view->assign("isMobile", $isMobile);
if ($this->request->isAjax()) {
$orderid = $this->request->post("orderid");
$pay = Pay::alipay($config);
try {
$result = $pay->find(['out_trade_no' => $orderid]);
if ($result['code'] == '10000' && $result['trade_status'] == 'TRADE_SUCCESS') {
$this->success("", "", ['status' => $result['trade_status']]);
} else {
$this->error("查询失败");
}
} catch (GatewayException $e) {
$this->error("查询失败(1001)");
}
}
//发起PC支付(Scan支付)(PC扫码模式)
$orderData = Session::get("alipayorderdata");
if (!$orderData) {
$this->error("请求参数错误");
}
$orderData['method'] = 'scan';
$payData = Service::submitOrder($orderData);
if (!isset($payData['qr_code'])) {
$this->error("创建订单失败,请返回重试");
}
$type = 'pc';
$this->view->assign("orderData", $orderData);
$this->view->assign("payData", $payData);
$this->view->assign("type", $type);
$this->view->assign("title", "支付宝支付");
return $this->view->fetch();
}
/**
* 支付成功回调
*/
public function notifyx()
{
$paytype = $this->request->param('paytype');
$pay = Service::checkNotify($paytype);
if (!$pay) {
return json(['code' => 'FAIL', 'message' => '失败'], 500, ['Content-Type' => 'application/json']);
}
// 获取回调数据V3和V2的回调接收不同
$data = Service::isVersionV3() ? $pay->callback() : $pay->verify();
try {
//微信支付V3返回和V2不同
if (Service::isVersionV3() && $paytype === 'wechat') {
$data = $data['resource']['ciphertext'];
$data['total_fee'] = $data['amount']['total'];
}
\think\Log::record($data);
//获取支付金额、订单号
$payamount = $paytype == 'alipay' ? $data['total_amount'] : $data['total_fee'] / 100;
$out_trade_no = $data['out_trade_no'];
\think\Log::record("回调成功,订单号:{$out_trade_no},金额:{$payamount}");
//你可以在此编写订单逻辑
} catch (Exception $e) {
\think\Log::record("回调逻辑处理错误:" . $e->getMessage(), "error");
}
//下面这句必须要执行,且在此之前不能有任何输出
if (Service::isVersionV3()) {
return $pay->success()->getBody()->getContents();
} else {
return $pay->success()->send();
}
}
/**
* 支付成功返回
*/
public function returnx()
{
$paytype = $this->request->param('paytype');
if (Service::checkReturn($paytype)) {
echo '签名错误';
return;
}
//你可以在这里定义你的提示信息,但切记不可在此编写逻辑
$this->success("恭喜你!支付成功!", addon_url("epay/index/index"));
}
}

View File

@@ -0,0 +1,131 @@
<?php
namespace addons\epay\controller;
use addons\epay\library\Service;
use fast\Random;
use think\addons\Controller;
use Exception;
/**
* 微信支付宝整合插件首页
*
* 此控制器仅用于开发展示说明和测试,请自行添加一个新的控制器进行处理返回和回调事件,同时删除此控制器文件
*
* Class Index
* @package addons\epay\controller
*/
class Index extends Controller
{
protected $layout = 'default';
protected $config = [];
public function _initialize()
{
parent::_initialize();
if (!config("app_debug")) {
$this->error("仅在开发环境下查看");
}
}
public function index()
{
$this->view->assign("title", "微信支付宝整合");
return $this->view->fetch();
}
/**
* 体验,仅供开发测试
*/
public function experience()
{
$amount = $this->request->post('amount');
$type = $this->request->post('type');
$method = $this->request->post('method');
$openid = $this->request->post('openid', "");
if (!$amount || $amount < 0) {
$this->error("支付金额必须大于0");
}
if (!$type || !in_array($type, ['alipay', 'wechat'])) {
$this->error("支付类型不能为空");
}
if (in_array($method, ['miniapp', 'mp']) && !$openid) {
$this->error("openid不能为空");
}
//订单号
$out_trade_no = date("YmdHis") . mt_rand(100000, 999999);
//订单标题
$title = '测试订单';
//回调链接
$notifyurl = $this->request->root(true) . '/addons/epay/index/notifyx/paytype/' . $type;
$returnurl = $this->request->root(true) . '/addons/epay/index/returnx/paytype/' . $type . '/out_trade_no/' . $out_trade_no;
$response = Service::submitOrder($amount, $out_trade_no, $type, $title, $notifyurl, $returnurl, $method, $openid);
return $response;
}
/**
* 支付成功,仅供开发测试
*/
public function notifyx()
{
$paytype = $this->request->param('paytype');
$pay = Service::checkNotify($paytype);
if (!$pay) {
return json(['code' => 'FAIL', 'message' => '失败'], 500, ['Content-Type' => 'application/json']);
}
// 获取回调数据V3和V2的回调接收不同
$data = Service::isVersionV3() ? $pay->callback() : $pay->verify();
try {
//微信支付V3返回和V2不同
if (Service::isVersionV3() && $paytype === 'wechat') {
$data = $data['resource']['ciphertext'];
$data['total_fee'] = $data['amount']['total'];
}
\think\Log::record($data);
//获取支付金额、订单号
$payamount = $paytype == 'alipay' ? $data['total_amount'] : $data['total_fee'] / 100;
$out_trade_no = $data['out_trade_no'];
\think\Log::record("回调成功,订单号:{$out_trade_no},金额:{$payamount}");
//你可以在此编写订单逻辑
} catch (Exception $e) {
\think\Log::record("回调逻辑处理错误:" . $e->getMessage(), "error");
}
//下面这句必须要执行,且在此之前不能有任何输出
if (Service::isVersionV3()) {
return $pay->success()->getBody()->getContents();
} else {
return $pay->success()->send();
}
}
/**
* 支付返回,仅供开发测试
*/
public function returnx()
{
$paytype = $this->request->param('paytype');
$out_trade_no = $this->request->param('out_trade_no');
$pay = Service::checkReturn($paytype);
if (!$pay) {
$this->error('签名错误', '');
}
//你可以在这里定义你的提示信息,但切记不可在此编写逻辑
$this->success("请返回网站查看支付结果", addon_url("epay/index/index"));
}
}

10
addons/epay/info.ini Normal file
View File

@@ -0,0 +1,10 @@
name = epay
title = 微信支付宝整合
intro = 可用于快速整合企业微信、支付宝支付功能
author = FastAdmin
website = https://www.fastadmin.net
version = 1.3.9
state = 1
url = /addons/epay
license = regular
licenseto = 101612

View File

@@ -0,0 +1,18 @@
<?php
namespace addons\epay\library;
class Collection extends \Yansongda\Supports\Collection
{
/**
* 创建 Collection 实例
* @access public
* @param array $items 数据
* @return static
*/
public static function make($items = [])
{
return new static($items);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace addons\epay\library;
use think\Exception;
class OrderException extends Exception
{
public function __construct($message = "", $code = 0, $data = [])
{
$this->message = $message;
$this->code = $code;
$this->data = $data;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace addons\epay\library;
class RedirectResponse extends \Symfony\Component\HttpFoundation\RedirectResponse implements \JsonSerializable, \Serializable
{
public function __toString()
{
return $this->getContent();
}
public function setTargetUrl($url)
{
if ('' === ($url ?? '')) {
throw new \InvalidArgumentException('无法跳转到空页面');
}
$this->targetUrl = $url;
$this->setContent(
sprintf('<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="refresh" content="0;url=\'%1$s\'" />
<title>正在跳转支付 %1$s</title>
</head>
<body>
<div id="redirect" style="display:none;">正在跳转支付 <a href="%1$s">%1$s</a></div>
<script type="text/javascript">
setTimeout(function(){
document.getElementById("redirect").style.display = "block";
}, 1000);
</script>
</body>
</html>', htmlspecialchars($url, \ENT_QUOTES, 'UTF-8')));
$this->headers->set('Location', $url);
return $this;
}
public function jsonSerialize()
{
return $this->getContent();
}
public function serialize()
{
return serialize($this->content);
}
public function unserialize($serialized)
{
return $this->content = unserialize($serialized);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace addons\epay\library;
class Response extends \Symfony\Component\HttpFoundation\Response implements \JsonSerializable, \Serializable
{
public function __toString()
{
return $this->getContent();
}
public function jsonSerialize()
{
return $this->getContent();
}
public function serialize()
{
return serialize($this->content);
}
public function unserialize($serialized)
{
return $this->content = unserialize($serialized);
}
}

View File

@@ -0,0 +1,489 @@
<?php
namespace addons\epay\library;
use addons\third\model\Third;
use app\common\library\Auth;
use Exception;
use think\Hook;
use think\Session;
use Yansongda\Pay\Pay;
use Yansongda\Supports\Str;
/**
* 订单服务类
*
* @package addons\epay\library
*/
class Service
{
public const SDK_VERSION_V2 = 'v2';
public const SDK_VERSION_V3 = 'v3';
/**
* 提交订单
* @param array|float $amount 订单金额
* @param string $orderid 订单号
* @param string $type 支付类型,可选alipay或wechat
* @param string $title 订单标题
* @param string $notifyurl 通知回调URL
* @param string $returnurl 跳转返回URL
* @param string $method 支付方法
* @param string $openid Openid
* @param array $custom 自定义微信支付宝相关配置
* @return Response|RedirectResponse|Collection
* @throws Exception
*/
public static function submitOrder($amount, $orderid = null, $type = null, $title = null, $notifyurl = null, $returnurl = null, $method = null, $openid = '', $custom = [])
{
$version = self::getSdkVersion();
$request = request();
$addonConfig = get_addon_config('epay');
if (!is_array($amount)) {
$params = [
'amount' => $amount,
'orderid' => $orderid,
'type' => $type,
'title' => $title,
'notifyurl' => $notifyurl,
'returnurl' => $returnurl,
'method' => $method,
'openid' => $openid,
'custom' => $custom,
];
} else {
$params = $amount;
}
$type = isset($params['type']) && in_array($params['type'], ['alipay', 'wechat']) ? $params['type'] : 'wechat';
$method = $params['method'] ?? 'web';
$orderid = $params['orderid'] ?? date("YmdHis") . mt_rand(100000, 999999);
$amount = $params['amount'] ?? 1;
$title = $params['title'] ?? "支付";
$auth_code = $params['auth_code'] ?? '';
$openid = $params['openid'] ?? '';
//自定义微信支付宝相关配置
$custom = $params['custom'] ?? [];
//未定义则使用默认回调和跳转
$notifyurl = !empty($params['notifyurl']) ? $params['notifyurl'] : $request->root(true) . '/addons/epay/index/notifyx/paytype/' . $type;
$returnurl = !empty($params['returnurl']) ? $params['returnurl'] : $request->root(true) . '/addons/epay/index/returnx/paytype/' . $type . '/out_trade_no/' . $orderid;
$html = '';
$config = Service::getConfig($type, array_merge($custom, ['notify_url' => $notifyurl, 'return_url' => $returnurl]));
//判断是否移动端或微信内浏览器
$isMobile = $request->isMobile();
$isWechat = strpos($request->server('HTTP_USER_AGENT'), 'MicroMessenger') !== false;
$result = null;
if ($type == 'alipay') {
//如果是PC支付,判断当前环境,进行跳转
if ($method == 'web') {
//如果是微信环境或后台配置PC使用扫码支付
if ($isWechat || $addonConfig['alipay']['scanpay']) {
Session::set("alipayorderdata", $params);
$url = addon_url('epay/api/alipay', [], true, true);
return new RedirectResponse($url);
} elseif ($isMobile) {
$method = 'wap';
}
}
//创建支付对象
$pay = Pay::alipay($config);
$params = [
'out_trade_no' => $orderid,//你的订单号
'total_amount' => $amount,//单位元
'subject' => $title,
];
switch ($method) {
case 'web':
//电脑支付
$result = $pay->web($params);
break;
case 'wap':
//手机网页支付
$result = $pay->wap($params);
break;
case 'app':
//APP支付
$result = $pay->app($params);
break;
case 'scan':
//扫码支付
$result = $pay->scan($params);
break;
case 'pos':
//刷卡支付必须要有auth_code
$params['auth_code'] = $auth_code;
$result = $pay->pos($params);
break;
case 'mini':
case 'miniapp':
//小程序支付,直接返回字符串
//小程序支付必须要有buyer_id或buyer_open_id
if (is_numeric($openid) && strlen($openid) === 16) {
$params['buyer_id'] = $openid;
} else {
$params['buyer_open_id'] = $openid;
}
$result = $pay->mini($params);
break;
default:
}
} else {
//如果是PC支付,判断当前环境,进行跳转
if ($method == 'web') {
//如果是移动端,但不是微信环境
if ($isMobile && !$isWechat) {
$method = 'wap';
} else {
Session::set("wechatorderdata", $params);
$url = addon_url('epay/api/wechat', [], true, true);
return new RedirectResponse($url);
}
}
//单位分
$total_fee = function_exists('bcmul') ? bcmul($amount, 100) : $amount * 100;
$total_fee = (int)$total_fee;
$ip = $request->ip();
//微信服务商模式时需传递sub_openid参数
$openidName = $addonConfig['wechat']['mode'] == 'service' ? 'sub_openid' : 'openid';
//创建支付对象
$pay = Pay::wechat($config);
if (self::isVersionV3()) {
//V3支付
$params = [
'out_trade_no' => $orderid,
'description' => $title,
'amount' => [
'total' => $total_fee,
]
];
switch ($method) {
case 'mp':
//公众号支付
//公众号支付必须有openid
$params['payer'] = [$openidName => $openid];
$result = $pay->mp($params);
break;
case 'wap':
//手机网页支付,跳转
$params['scene_info'] = [
'payer_client_ip' => $ip,
'h5_info' => [
'type' => 'Wap',
]
];
$result = $pay->wap($params);
break;
case 'app':
//APP支付,直接返回字符串
$result = $pay->app($params);
break;
case 'scan':
//扫码支付,直接返回字符串
$result = $pay->scan($params);
break;
case 'pos':
//刷卡支付,直接返回字符串
//刷卡支付必须要有auth_code
$params['auth_code'] = $auth_code;
$result = $pay->pos($params);
break;
case 'mini':
case 'miniapp':
//小程序支付,直接返回字符串
//小程序支付必须要有openid
$params['payer'] = [$openidName => $openid];
$result = $pay->mini($params);
break;
default:
}
} else {
//V2支付
$params = [
'out_trade_no' => $orderid,
'body' => $title,
'total_fee' => $total_fee,
];
switch ($method) {
case 'mp':
//公众号支付
//公众号支付必须有openid
$params[$openidName] = $openid;
$result = $pay->mp($params);
break;
case 'wap':
//手机网页支付,跳转
$params['spbill_create_ip'] = $ip;
$result = $pay->wap($params);
break;
case 'app':
//APP支付,直接返回字符串
$result = $pay->app($params);
break;
case 'scan':
//扫码支付,直接返回字符串
$result = $pay->scan($params);
break;
case 'pos':
//刷卡支付,直接返回字符串
//刷卡支付必须要有auth_code
$params['auth_code'] = $auth_code;
$result = $pay->pos($params);
break;
case 'mini':
case 'miniapp':
//小程序支付,直接返回字符串
//小程序支付必须要有openid
$params[$openidName] = $openid;
$result = $pay->miniapp($params);
break;
default:
}
}
}
//使用重写的Response类、RedirectResponse、Collection类
if ($result instanceof \Symfony\Component\HttpFoundation\RedirectResponse) {
$result = new RedirectResponse($result->getTargetUrl());
} elseif ($result instanceof \Symfony\Component\HttpFoundation\Response) {
$result = new Response($result->getContent());
} elseif ($result instanceof \Yansongda\Supports\Collection) {
$result = Collection::make($result->all());
} elseif ($result instanceof \GuzzleHttp\Psr7\Response) {
$result = new Response($result->getBody());
}
return $result;
}
/**
* 验证回调是否成功
* @param string $type 支付类型
* @param array $custom 自定义配置信息
* @return bool|\Yansongda\Pay\Gateways\Alipay|\Yansongda\Pay\Gateways\Wechat|\Yansongda\Pay\Provider\Wechat|\Yansongda\Pay\Provider\Alipay
*/
public static function checkNotify($type, $custom = [])
{
$type = strtolower($type);
if (!in_array($type, ['wechat', 'alipay'])) {
return false;
}
$version = self::getSdkVersion();
try {
$config = self::getConfig($type, $custom);
$pay = $type == 'wechat' ? Pay::wechat($config) : Pay::alipay($config);
$data = Service::isVersionV3() ? $pay->callback() : $pay->verify();
if ($type == 'alipay') {
if (in_array($data['trade_status'], ['TRADE_SUCCESS', 'TRADE_FINISHED'])) {
return $pay;
}
} else {
return $pay;
}
} catch (Exception $e) {
\think\Log::record("回调请求参数解析错误", "error");
return false;
}
return false;
}
/**
* 验证返回是否成功,请勿用于判断是否支付成功的逻辑验证
* 已弃用
*
* @param string $type 支付类型
* @param array $custom 自定义配置信息
* @return bool
* @deprecated 已弃用,请勿用于逻辑验证
*/
public static function checkReturn($type, $custom = [])
{
//由于PC及移动端无法获取请求的参数信息取消return验证均返回true
return true;
}
/**
* 处理证书路径
* @param array $config 配置
* @param string $field 字段
* @return void
*/
private static function processAddonsPath(&$config, $field)
{
if (isset($config[$field]) && substr($config[$field], 0, 8) == '/addons/') {
$config[$field] = ROOT_PATH . str_replace('/', DS, substr($config[$field], 1));
}
}
/**
* 获取配置
* @param string $type 支付类型
* @param array $custom 自定义配置,用于覆盖插件默认配置
* @return array
*/
public static function getConfig($type = 'wechat', $custom = [])
{
$addonConfig = get_addon_config('epay');
$config = $addonConfig[$type] ?? $addonConfig['wechat'];
// SDK版本
$version = self::getSdkVersion();
// 处理微信证书路径
if ($type === 'wechat') {
$certFields = ['cert_client', 'cert_key'];
foreach ($certFields as $field) {
self::processAddonsPath($config, $field);
}
}
// 处理支付宝证书路径
if ($type === 'alipay') {
$config['signtype'] = $config['signtype'] ?? 'publickey';
if ($config['signtype'] == 'cert') {
$certFields = ['app_cert_public_key', 'alipay_root_cert', 'ali_public_key'];
foreach ($certFields as $field) {
self::processAddonsPath($config, $field);
}
} else {
// 如果是普通公钥需要将app_cert_public_key和alipay_root_cert设置为空不然会导致错误
$config['app_cert_public_key'] = '';
$config['alipay_root_cert'] = '';
}
}
// V3支付
if (self::isVersionV3()) {
if ($type == 'wechat') {
$config['mp_app_id'] = $config['app_id'] ?? '';
$config['app_id'] = $config['appid'] ?? '';
$config['mini_app_id'] = $config['miniapp_id'] ?? '';
$config['combine_mch_id'] = $config['combine_mch_id'] ?? '';
$config['mch_secret_key'] = $config['key_v3'] ?? '';
$config['mch_secret_cert'] = $config['cert_key'];
$config['mch_public_cert_path'] = $config['cert_client'];
$config['sub_mp_app_id'] = $config['sub_appid'] ?? '';
$config['sub_app_id'] = $config['sub_app_id'] ?? '';
$config['sub_mini_app_id'] = $config['sub_miniapp_id'] ?? '';
$config['sub_mch_id'] = $config['sub_mch_id'] ?? '';
} elseif ($type == 'alipay') {
$config['app_secret_cert'] = $config['private_key'] ?? '';
$config['app_public_cert_path'] = $config['app_cert_public_key'] ?? '';
$config['alipay_public_cert_path'] = $config['ali_public_key'] ?? '';
$config['alipay_root_cert_path'] = $config['alipay_root_cert'] ?? '';
$config['service_provider_id'] = $config['pid'] ?? '';
}
$modeArr = ['normal' => 0, 'dev' => 1, 'service' => 2];
$config['mode'] = $modeArr[$config['mode']] ?? 0;
}
// 日志
if ($config['log']) {
$config['log'] = [
'enable' => true,
'file' => LOG_PATH . 'epaylogs' . DS . $type . '-' . date("Y-m-d") . '.log',
'level' => 'debug'
];
} else {
$config['log'] = [
'enable' => false,
];
}
// GuzzleHttp配置可选
$config['http'] = [
'timeout' => 10,
'connect_timeout' => 10,
// 更多配置项请参考 [Guzzle](https://guzzle-cn.readthedocs.io/zh_CN/latest/request-options.html)
];
$config['notify_url'] = empty($config['notify_url']) ? addon_url('epay/api/notifyx', [], false) . '/type/' . $type : $config['notify_url'];
$config['notify_url'] = !preg_match("/^(http:\/\/|https:\/\/)/i", $config['notify_url']) ? request()->root(true) . $config['notify_url'] : $config['notify_url'];
$config['return_url'] = empty($config['return_url']) ? addon_url('epay/api/returnx', [], false) . '/type/' . $type : $config['return_url'];
$config['return_url'] = !preg_match("/^(http:\/\/|https:\/\/)/i", $config['return_url']) ? request()->root(true) . $config['return_url'] : $config['return_url'];
//合并自定义配置
$config = array_merge($config, $custom);
//v3版本时返回的结构不同
if (self::isVersionV3()) {
$config = [$type => ['default' => $config], 'logger' => $config['log'], 'http' => $config['http'], '_force' => true];
}
return $config;
}
/**
* 获取微信Openid
*
* @param array $custom 自定义配置信息
* @return mixed|string
*/
public static function getOpenid($custom = [])
{
$openid = '';
$auth = Auth::instance();
if ($auth->isLogin()) {
$third = get_addon_info('third');
if ($third && $third['state']) {
$thirdInfo = Third::where('user_id', $auth->id)->where('platform', 'wechat')->where('apptype', 'mp')->find();
$openid = $thirdInfo ? $thirdInfo['openid'] : '';
}
}
if (!$openid) {
$openid = Session::get("openid");
//如果未传openid则去读取openid
if (!$openid) {
$addonConfig = get_addon_config('epay');
$wechat = new Wechat($custom['app_id'] ?? $addonConfig['wechat']['app_id'], $custom['app_secret'] ?? $addonConfig['wechat']['app_secret']);
$openid = $wechat->getOpenid();
}
}
return $openid;
}
/**
* 获取SDK版本
* @return mixed|string
*/
public static function getSdkVersion()
{
$addonConfig = get_addon_config('epay');
return $addonConfig['version'] ?? self::SDK_VERSION_V2;
}
/**
* 判断是否V2支付
* @return bool
*/
public static function isVersionV2()
{
return self::getSdkVersion() === self::SDK_VERSION_V2;
}
/**
* 判断是否V3支付
* @return bool
*/
public static function isVersionV3()
{
return self::getSdkVersion() === self::SDK_VERSION_V3;
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace addons\epay\library;
use fast\Http;
use think\Cache;
use think\Session;
/**
* 微信授权
*
*/
class Wechat
{
private $app_id = '';
private $app_secret = '';
private $scope = 'snsapi_userinfo';
public function __construct($app_id, $app_secret)
{
$this->app_id = $app_id;
$this->app_secret = $app_secret;
}
/**
* 获取微信授权链接
*
* @return string
*/
public function getAuthorizeUrl()
{
$redirect_uri = addon_url('epay/api/wechat', [], true, true);
$redirect_uri = urlencode($redirect_uri);
$state = \fast\Random::alnum();
Session::set('state', $state);
return "https://open.weixin.qq.com/connect/oauth2/authorize?appid={$this->app_id}&redirect_uri={$redirect_uri}&response_type=code&scope={$this->scope}&state={$state}#wechat_redirect";
}
/**
* 获取微信openid
*
* @return mixed|string
*/
public function getOpenid()
{
$openid = Session::get('openid');
if (!$openid) {
if (!isset($_GET['code'])) {
$url = $this->getAuthorizeUrl();
Header("Location: $url");
exit();
} else {
$state = Session::get('state');
if ($state == $_GET['state']) {
$code = $_GET['code'];
$token = $this->getAccessToken($code);
if (!isset($token['openid']) && isset($token['errmsg'])) {
exception($token['errmsg']);
}
$openid = $token['openid'] ?? '';
if ($openid) {
Session::set("openid", $openid);
}
}
}
}
return $openid;
}
/**
* 获取授权token网页授权
*
* @param string $code
* @return mixed|string
*/
public function getAccessToken($code = '')
{
$params = [
'appid' => $this->app_id,
'secret' => $this->app_secret,
'code' => $code,
'grant_type' => 'authorization_code'
];
$ret = Http::sendRequest('https://api.weixin.qq.com/sns/oauth2/access_token', $params, 'GET');
if ($ret['ret']) {
$ar = json_decode($ret['msg'], true);
return $ar;
}
return [];
}
public function getJsticket($code = '')
{
$jsticket = Session::get('jsticket');
if (!$jsticket) {
$token = $this->getAccessToken($code);
$params = [
'access_token' => 'token',
'type' => 'jsapi',
];
$ret = Http::sendRequest('https://api.weixin.qq.com/cgi-bin/ticket/getticket', $params, 'GET');
if ($ret['ret']) {
$ar = json_decode($ret['msg'], true);
return $ar;
}
}
return $jsticket;
}
}

View File

@@ -0,0 +1,2 @@
/tests export-ignore
/.github export-ignore

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Hyperf
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,40 @@
{
"name": "hyperf/context",
"description": "A coroutine context library.",
"license": "MIT",
"keywords": [
"php",
"swoole",
"hyperf",
"context"
],
"homepage": "https://hyperf.io",
"support": {
"docs": "https://hyperf.wiki",
"issues": "https://github.com/hyperf/hyperf/issues",
"pull-request": "https://github.com/hyperf/hyperf/pulls",
"source": "https://github.com/hyperf/hyperf"
},
"require": {
"php": ">=7.2",
"hyperf/engine": "^1.1"
},
"autoload": {
"psr-4": {
"Hyperf\\Context\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"HyperfTest\\Context\\": "tests/"
}
},
"config": {
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-master": "2.2-dev"
}
}
}

View File

@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Context;
use Hyperf\Engine\Coroutine;
class Context
{
protected static $nonCoContext = [];
public static function set(string $id, $value)
{
if (Coroutine::id() > 0) {
Coroutine::getContextFor()[$id] = $value;
} else {
static::$nonCoContext[$id] = $value;
}
return $value;
}
public static function get(string $id, $default = null, $coroutineId = null)
{
if (Coroutine::id() > 0) {
return Coroutine::getContextFor($coroutineId)[$id] ?? $default;
}
return static::$nonCoContext[$id] ?? $default;
}
public static function has(string $id, $coroutineId = null)
{
if (Coroutine::id() > 0) {
return isset(Coroutine::getContextFor($coroutineId)[$id]);
}
return isset(static::$nonCoContext[$id]);
}
/**
* Release the context when you are not in coroutine environment.
*/
public static function destroy(string $id)
{
unset(static::$nonCoContext[$id]);
}
/**
* Copy the context from a coroutine to current coroutine.
* This method will delete the origin values in current coroutine.
*/
public static function copy(int $fromCoroutineId, array $keys = []): void
{
$from = Coroutine::getContextFor($fromCoroutineId);
if ($from === null) {
return;
}
$current = Coroutine::getContextFor();
if ($keys) {
$map = array_intersect_key($from->getArrayCopy(), array_flip($keys));
} else {
$map = $from->getArrayCopy();
}
$current->exchangeArray($map);
}
/**
* Retrieve the value and override it by closure.
*/
public static function override(string $id, \Closure $closure)
{
$value = null;
if (self::has($id)) {
$value = self::get($id);
}
$value = $closure($value);
self::set($id, $value);
return $value;
}
/**
* Retrieve the value and store it if not exists.
* @param mixed $value
*/
public static function getOrSet(string $id, $value)
{
if (! self::has($id)) {
return self::set($id, value($value));
}
return self::get($id);
}
public static function getContainer()
{
if (Coroutine::id() > 0) {
return Coroutine::getContextFor();
}
return static::$nonCoContext;
}
}

View File

@@ -0,0 +1,2 @@
/tests export-ignore
/.github export-ignore

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Hyperf
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,33 @@
{
"name": "hyperf/contract",
"description": "The contracts of Hyperf.",
"license": "MIT",
"keywords": [
"php",
"swoole",
"hyperf"
],
"homepage": "https://hyperf.io",
"support": {
"docs": "https://hyperf.wiki",
"issues": "https://github.com/hyperf/hyperf/issues",
"pull-request": "https://github.com/hyperf/hyperf/pulls",
"source": "https://github.com/hyperf/hyperf"
},
"require": {
"php": ">=7.2"
},
"autoload": {
"psr-4": {
"Hyperf\\Contract\\": "src/"
}
},
"config": {
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-master": "2.2-dev"
}
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface ApplicationInterface
{
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface Castable
{
/**
* Get the name of the caster class to use when casting from / to this cast target.
*
* @return CastsAttributes|CastsInboundAttributes|string
*/
public static function castUsing();
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface CastsAttributes
{
/**
* Transform the attribute from the underlying model values.
*
* @param object $model
* @param mixed $value
* @return mixed
*/
public function get($model, string $key, $value, array $attributes);
/**
* Transform the attribute to its underlying model values.
*
* @param object $model
* @param mixed $value
* @return array|string
*/
public function set($model, string $key, $value, array $attributes);
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface CastsInboundAttributes
{
/**
* Transform the attribute to its underlying model values.
*
* @param object $model
* @param mixed $value
* @return array
*/
public function set($model, string $key, $value, array $attributes);
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface CompressInterface
{
public function compress(): UnCompressInterface;
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface ConfigInterface
{
/**
* Finds an entry of the container by its identifier and returns it.
*
* @param string $key identifier of the entry to look for
* @param mixed $default default value of the entry when does not found
* @return mixed entry
*/
public function get(string $key, $default = null);
/**
* Returns true if the container can return an entry for the given identifier.
* Returns false otherwise.
*
* @param string $keys identifier of the entry to look for
* @return bool
*/
public function has(string $keys);
/**
* Set a value to the container by its identifier.
*
* @param string $key identifier of the entry to set
* @param mixed $value the value that save to container
*/
public function set(string $key, $value);
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface ConnectionInterface
{
/**
* Get the real connection from pool.
*/
public function getConnection();
/**
* Reconnect the connection.
*/
public function reconnect(): bool;
/**
* Check the connection is valid.
*/
public function check(): bool;
/**
* Close the connection.
*/
public function close(): bool;
/**
* Release the connection to pool.
*/
public function release(): void;
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
use Psr\Container\ContainerInterface as PsrContainerInterface;
interface ContainerInterface extends PsrContainerInterface
{
/**
* Build an entry of the container by its name.
* This method behave like get() except resolves the entry again every time.
* For example if the entry is a class then a new instance will be created each time.
* This method makes the container behave like a factory.
*
* @param string $name entry name or a class name
* @param array $parameters Optional parameters to use to build the entry. Use this to force specific parameters
* to specific values. Parameters not defined in this array will be resolved using
* the container.
* @throws InvalidArgumentException the name parameter must be of type string
* @throws NotFoundException no entry found for the given name
*/
public function make(string $name, array $parameters = []);
/**
* Bind an arbitrary resolved entry to an identifier.
* Useful for testing 'get'.
*
* @param mixed $entry
*/
public function set(string $name, $entry);
/**
* Unbind an arbitrary resolved entry.
*/
public function unbind(string $name);
/**
* Bind an arbitrary definition to an identifier.
* Useful for testing 'make'.
*
* @param array|callable|string $definition
*/
public function define(string $name, $definition);
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface DispatcherInterface
{
public function dispatch(...$params);
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface FrequencyInterface
{
/**
* Number of hit per time.
*/
public function hit(int $number = 1): bool;
/**
* Hits per second.
*/
public function frequency(): float;
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface IdGeneratorInterface
{
public function generate();
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface LengthAwarePaginatorInterface extends PaginatorInterface
{
/**
* Create a range of pagination URLs.
*/
public function getUrlRange(int $start, int $end): array;
/**
* Determine the total number of items in the data store.
*/
public function total(): int;
/**
* Get the page number of the last available page.
*/
public function lastPage(): int;
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface MiddlewareInitializerInterface
{
public function initCoreMiddleware(string $serverName): void;
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface NormalizerInterface
{
/**
* Normalizes an object into a set of arrays/scalars.
*
* @param mixed $object
* @return null|array|\ArrayObject|bool|float|int|string
*/
public function normalize($object);
/**
* Denormalizes data back into an object of the given class.
*
* @param mixed $data Data to restore
* @param string $class The expected class to instantiate
* @return mixed|object
*/
public function denormalize($data, string $class);
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
use Swoole\Http\Response;
use Swoole\Server;
interface OnCloseInterface
{
/**
* @param Response|Server $server
*/
public function onClose($server, int $fd, int $reactorId): void;
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
use Swoole\Http\Request;
use Swoole\Http\Response;
interface OnHandShakeInterface
{
public function onHandShake(Request $request, Response $response): void;
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
use Swoole\Http\Response;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
interface OnMessageInterface
{
/**
* @param Response|Server $server
*/
public function onMessage($server, Frame $frame): void;
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
use Swoole\Http\Request;
use Swoole\Http\Response;
use Swoole\WebSocket\Server;
interface OnOpenInterface
{
/**
* @param Response|Server $server
*/
public function onOpen($server, Request $request): void;
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
use Swoole\WebSocket\Server;
interface OnPacketInterface
{
/**
* @param Server $server
* @param mixed $data
* @param array $clientInfo
*/
public function onPacket($server, $data, $clientInfo): void;
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
use Swoole\Coroutine\Server\Connection;
use Swoole\Server as SwooleServer;
interface OnReceiveInterface
{
/**
* @param Connection|SwooleServer $server
*/
public function onReceive($server, int $fd, int $reactorId, string $data): void;
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface OnRequestInterface
{
/**
* @param mixed $request swoole request or psr server request
* @param mixed $response swoole response or swow session
*/
public function onRequest($request, $response): void;
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface PackerInterface
{
public function pack($data): string;
public function unpack(string $data);
}

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface PaginatorInterface
{
/**
* Get the URL for a given page.
*/
public function url(int $page): string;
/**
* Add a set of query string values to the paginator.
*
* @param array|string $key
* @return $this
*/
public function appends($key, ?string $value = null);
/**
* Get / set the URL fragment to be appended to URLs.
*
* @return $this|string
*/
public function fragment(?string $fragment = null);
/**
* The URL for the next page, or null.
*/
public function nextPageUrl(): ?string;
/**
* Get the URL for the previous page, or null.
*/
public function previousPageUrl(): ?string;
/**
* Get all of the items being paginated.
*/
public function items(): array;
/**
* Get the "index" of the first item being paginated.
*/
public function firstItem(): ?int;
/**
* Get the "index" of the last item being paginated.
*/
public function lastItem(): ?int;
/**
* Determine how many items are being shown per page.
*/
public function perPage(): int;
/**
* Determine the current page being paginated.
*/
public function currentPage(): int;
/**
* Determine if there are enough items to split into multiple pages.
*/
public function hasPages(): bool;
/**
* Determine if there is more items in the data store.
*/
public function hasMorePages(): bool;
/**
* Determine if the list of items is empty or not.
*/
public function isEmpty(): bool;
/**
* Determine if the list of items is not empty.
*/
public function isNotEmpty(): bool;
/**
* Render the paginator using a given view.
*/
public function render(?string $view = null, array $data = []): string;
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface PoolInterface
{
/**
* Get a connection from the connection pool.
*/
public function get(): ConnectionInterface;
/**
* Release a connection back to the connection pool.
*/
public function release(ConnectionInterface $connection): void;
/**
* Close and clear the connection pool.
*/
public function flush(): void;
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface PoolOptionInterface
{
public function getMaxConnections(): int;
public function getMinConnections(): int;
public function getConnectTimeout(): float;
public function getWaitTimeout(): float;
public function getHeartbeat(): float;
public function getMaxIdleTime(): float;
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
use Swoole\Coroutine\Http\Server as CoHttpServer;
use Swoole\Coroutine\Server as CoServer;
use Swoole\Server;
interface ProcessInterface
{
/**
* Create the process object according to process number and bind to server.
* @param CoHttpServer|CoServer|Server $server
*/
public function bind($server): void;
/**
* Determine if the process should start ?
* @param CoServer|Server $server
*/
public function isEnable($server): bool;
/**
* The logical of process will place in here.
*/
public function handle(): void;
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
use Psr\Http\Message\ResponseInterface;
interface ResponseEmitterInterface
{
/**
* @param mixed $connection swoole response or swow session
*/
public function emit(ResponseInterface $response, $connection, bool $withContent = true);
}

View File

@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface SessionInterface
{
/**
* Starts the session storage.
*
* @throws \RuntimeException if session fails to start
* @return bool True if session started
*/
public function start(): bool;
/**
* Returns the session ID.
*
* @return string The session ID
*/
public function getId(): string;
/**
* Sets the session ID.
*/
public function setId(string $id);
/**
* Returns the session name.
*/
public function getName(): string;
/**
* Sets the session name.
*/
public function setName(string $name);
/**
* Invalidates the current session.
*
* Clears all session attributes and flashes and regenerates the
* session and deletes the old session from persistence.
*
* @param int $lifetime Sets the cookie lifetime for the session cookie. A null value
* will leave the system settings unchanged, 0 sets the cookie
* to expire with browser session. Time is in seconds, and is
* not a Unix timestamp.
*
* @return bool True if session invalidated, false if error
*/
public function invalidate(?int $lifetime = null): bool;
/**
* Migrates the current session to a new session id while maintaining all
* session attributes.
*
* @param bool $destroy Whether to delete the old session or leave it to garbage collection
* @param int $lifetime Sets the cookie lifetime for the session cookie. A null value
* will leave the system settings unchanged, 0 sets the cookie
* to expire with browser session. Time is in seconds, and is
* not a Unix timestamp.
*
* @return bool True if session migrated, false if error
*/
public function migrate(bool $destroy = false, ?int $lifetime = null): bool;
/**
* Force the session to be saved and closed.
*
* This method is generally not required for real sessions as
* the session will be automatically saved at the end of
* code execution.
*/
public function save(): void;
/**
* Checks if an attribute is defined.
*
* @param string $name The attribute name
*
* @return bool true if the attribute is defined, false otherwise
*/
public function has(string $name): bool;
/**
* Returns an attribute.
*
* @param string $name The attribute name
* @param mixed $default The default value if not found
*/
public function get(string $name, $default = null);
/**
* Sets an attribute.
* @param mixed $value
*/
public function set(string $name, $value): void;
/**
* Put a key / value pair or array of key / value pairs in the session.
*
* @param array|string $key
* @param null|mixed $value
*/
public function put($key, $value = null): void;
/**
* Returns attributes.
*/
public function all(): array;
/**
* Sets attributes.
*/
public function replace(array $attributes): void;
/**
* Removes an attribute, returning its value.
*
* @return mixed The removed value or null when it does not exist
*/
public function remove(string $name);
/**
* Remove one or many items from the session.
*
* @param array|string $keys
*/
public function forget($keys): void;
/**
* Clears all attributes.
*/
public function clear(): void;
/**
* Checks if the session was started.
*/
public function isStarted(): bool;
/**
* Get the previous URL from the session.
*/
public function previousUrl(): ?string;
/**
* Set the "previous" URL in the session.
*/
public function setPreviousUrl(string $url): void;
}

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