feat: 添加短信应用基本功能- 新增应用管理、短信中心、用户登录注册等功能页面

- 实现应用创建、删除、短信搜索、分页等功能
- 添加用户登录、注册、修改密码等账户管理功能
- 新增通用消息显示模板
This commit is contained in:
mao 2025-04-30 18:00:08 +08:00
parent 6163a47ec6
commit a4e5648147
64 changed files with 7005 additions and 4 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
/runtime
/.idea
/.vscode
/vendor
*.log
.env
/tests/tmp
/tests/.phpunit.result.cache

20
LICENSE
View File

@ -1,9 +1,21 @@
MIT License
Copyright (c) 2025 mao
Copyright (c) 2021 walkor<walkor@workerman.net> and contributors (see https://github.com/walkor/webman/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:
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 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.
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,24 @@
<?php
namespace app\controller;
use app\model\User;
use support\Request;
use support\View;
class IndexController
{
public function __construct()
{
View::assign(["nickname" => session('nickname', "未登录")]);
View::assign([
"header" => view('common/header')->rawBody(),
"footer" => view('common/footer')->rawBody()
]);
}
public function index(Request $request)
{
return view('index/index');
}
}

View File

@ -0,0 +1,234 @@
<?php
namespace app\controller;
use app\model\SmsApp;
use app\model\SmsContent;
use app\model\User;
use app\model\UsersToken;
use support\Request;
use support\View;
class SmsController
{
public function index(Request $request)
{
if (session('nickname', "未登录") === "未登录") return msg('请先登录', "请登录后继续。", "/user/login", '立即登录');
View::assign(["nickname" => session('nickname', "未登录")]);
View::assign([
"header" => view('common/header')->rawBody(),
"footer" => view('common/footer')->rawBody()
]);
$sms_app = SmsApp::where('user_id', session('user_id'))->get();
// 获取短信内容
$smsContent = SmsContent::select();
// 根据应用名称获取短信内容
if (!empty($request->get('app_name'))) {
$smsContent->where('app_name', $request->get('app_name'));
}
// 根据UUID获取短信内容
if (!empty($request->get('app_uuid'))) {
$smsContent->where('app_uuid', $request->get('app_uuid'));
}
// 根据手机号获取短信内容
if (!empty($request->get('from'))) {
$smsContent->whereLike('from', "%" . $request->get('from') . "%");
}
// 根据IP获取短信内容
if (!empty($request->get('ip'))) {
$smsContent->whereLike('ip', "%" . $request->get('ip') . "%");
}
// 根据内容获取短信内容
if (!empty($request->get('content'))) {
$smsContent->whereLike('content', "%" . $request->get('content') . "%");
}
// 根据接收时间获取短信内容
if (!empty($request->get('receive_time'))) {
$rt = $request->get('receive_time');
if (count($rt) === 2) {
if (!empty($rt[0]) && !empty($rt[1])) {
$smsContent->whereBetween('created_at', $request->get('receive_time'));
}
}
}
// 倒序
$smsContent->orderBy('id', 'desc');
$smsContentPaginate = $smsContent->paginate($request->get('limit', 10));
return view('sms/index', ['get' => $request->get(), 'smsContent' => $smsContentPaginate, 'sms_app' => $sms_app]);
}
public function s(Request $request, string $uuid)
{
// token是否存在
$token = $request->get('token', '');
if (!$token) $token = $request->post('token', '');
if (!$token) {
return json(['code' => 1, 'msg' => 'token不存在']);
}
$tokenExist = UsersToken::where('token', $token)->first();
if (!$tokenExist) {
return json(['code' => 2, 'msg' => 'token不存在']);
}
$user_id = $tokenExist->user_id;
$sms_app = SmsApp::where('app_uuid', $uuid)->first();
if (!$sms_app) {
return json(['code' => 3, 'msg' => '应用不存在']);
}
if ($sms_app->user_id !== $user_id) {
return json(['code' => 4, 'msg' => '应用不存在']);
}
$smsContent = new SmsContent();
$smsContent->user_id = $user_id;
$smsContent->app_name = $sms_app->app_name;
$smsContent->app_uuid = $uuid;
$smsContent->headers = json_encode($request->header());
$smsContent->original = json_encode($request->all());
$smsContent->from = $request->input('from', '');
$smsContent->content = $request->input('content', '');
$smsContent->ip = $request->getRemoteIp() . ":" . $request->getRemotePort();
$smsContent->save();
}
public function app(Request $request)
{
if (session('nickname', "未登录") === "未登录") return msg('请先登录', "请登录后继续。", "/user/login", '立即登录');
View::assign(["nickname" => session('nickname', "未登录")]);
View::assign([
"header" => view('common/header')->rawBody(),
"footer" => view('common/footer')->rawBody()
]);
$user = User::where('id', session('user_id'))->first();
if (!$user) {
session(['nickname' => null, 'user_id' => null]);
return msg('用户不存在', "请重新登录后继续。", "/user/login", "登录");
}
$sms_app = SmsApp::where('user_id', $user->id)->get();
View::assign([
'sms_app' => $sms_app
]);
return view('sms/app');
}
public function createApp(Request $request)
{
if (session('nickname', "未登录") === "未登录") return msg('请先登录', "请登录后继续。", "/user/login", '立即登录');
View::assign(["nickname" => session('nickname', "未登录")]);
View::assign([
"header" => view('common/header')->rawBody(),
"footer" => view('common/footer')->rawBody()
]);
$user = User::where('id', session('user_id'))->first();
if (!$user) {
session(['nickname' => null, 'user_id' => null]);
return msg('用户不存在', "请重新登录后继续。", "/user/login", "登录");
}
return view('sms/create_app');
}
public function appStore(Request $request)
{
if (session('nickname', "未登录") === "未登录") return msg('请先登录', "请登录后继续。", "/user/login", '立即登录');
View::assign(["nickname" => session('nickname', "未登录")]);
View::assign([
"header" => view('common/header')->rawBody(),
"footer" => view('common/footer')->rawBody()
]);
$user = User::where('id', session('user_id'))->first();
if (!$user) {
session(['nickname' => null, 'user_id' => null]);
return msg('用户不存在', "请重新登录后继续。", "/user/login", "登录");
}
if (empty($request->post('app_name', ''))) {
return msg('请输入应用名称', "请输入应用名称后继续。", "/sms/create-app", "返回");
}
$appName = $request->post('app_name', '');
if (strlen($appName) > 255) {
return msg('应用名称过长', "请输入255个字符内名称后继续。", "/sms/create-app", "返回");
}
$appNameExist = SmsApp::where('app_name', $appName)->first();
if ($appNameExist) {
return msg('应用名称已存在', "请重新创建。", "/sms/create-app", "返回");
}
$uuid = uniqid('', true);
$uuid = md5($uuid);
$uuid = substr($uuid, 0, 8) . ' - ' . substr($uuid, 8, 4) . ' - ' . substr($uuid, 12, 4) . ' - ' . substr($uuid, 16, 4) . ' - ' . substr($uuid, 20, 12);
$uuidExist = SmsApp::where('app_uuid', $uuid)->first();
if ($uuidExist) {
return msg('UUID已存在', "请重新创建。", "/sms/create-app", "返回");
}
$sms_app = new SmsApp();
$sms_app->user_id = $user->id;
$sms_app->app_name = $request->post('app_name', '');
$sms_app->app_uuid = $uuid;
$sms_app->save();
return msg('创建成功', "请使用个人密钥访问此应用。", "/sms/app", "返回");
}
public function deleteApp(Request $request)
{
if (session('nickname', "未登录") === "未登录") return msg('请先登录', "请登录后继续。", "/user/login", '立即登录');
View::assign(["nickname" => session('nickname', "未登录")]);
View::assign([
"header" => view('common/header')->rawBody(),
"footer" => view('common/footer')->rawBody()
]);
$user = User::where('id', session('user_id'))->first();
if (!$user) {
session(['nickname' => null, 'user_id' => null]);
return msg('用户不存在', "请重新登录后继续。", "/user/login", "登录");
}
$sms_app = SmsApp::where('id', $request->get('id', ''))->where('user_id', $user->id)->first();
if (!$sms_app) {
return msg('应用不存在', "删除失败", "/sms/app", "返回");
}
$sms_app->delete();
return msg('删除成功', "删除成功", "/sms/app", "返回");
}
public function delete(Request $request)
{
if (session('nickname', "未登录") === "未登录") return msg('请先登录', "请登录后继续。", "/user/login", '立即登录');
View::assign(["nickname" => session('nickname', "未登录")]);
View::assign([
"header" => view('common/header')->rawBody(),
"footer" => view('common/footer')->rawBody()
]);
$user = User::where('id', session('user_id'))->first();
if (!$user) {
session(['nickname' => null, 'user_id' => null]);
return msg('用户不存在', "请重新登录后继续。", "/user/login", "登录");
}
$sms_content = SmsContent::where('id', $request->get('id', ''))->where('user_id', $user->id)->first();
if (!$sms_content) {
return msg('短信不存在', "删除失败", "/sms/index", "返回");
}
$sms_content->delete();
return msg('删除成功', "删除成功", "/sms", "返回");
}
}

View File

@ -0,0 +1,277 @@
<?php
namespace app\controller;
use app\model\SmsContent;
use app\model\User;
use app\model\UsersLog;
use app\model\UsersToken;
use support\Request;
use support\View;
class UserController
{
public function regedit(Request $request)
{
View::assign(["nickname" => session('nickname', "未登录")]);
View::assign([
"header" => view('common/header')->rawBody(),
"footer" => view('common/footer')->rawBody()
]);
return view('user/regedit');
}
public function login(Request $request)
{
View::assign(["nickname" => session('nickname', "未登录")]);
View::assign([
"header" => view('common/header')->rawBody(),
"footer" => view('common/footer')->rawBody()
]);
return view('user/login');
}
public function logout(Request $request)
{
session(['nickname' => null, 'user_id' => null]);
View::assign(["nickname" => session('nickname', "未登录")]);
View::assign([
"header" => view('common/header')->rawBody(),
"footer" => view('common/footer')->rawBody()
]);
return msg('退出成功', "您可以继续操作。");
}
public function enter(Request $request)
{
View::assign(["nickname" => session('nickname', "未登录")]);
View::assign([
"header" => view('common/header')->rawBody(),
"footer" => view('common/footer')->rawBody()
]);
$username = $request->post('username', '');
$password = $request->post('password', '');
if (empty($username) || empty($password)) {
return msg('请输入用户名或密码', "请输入用户名或密码后继续。", "/user/login", "返回");
}
$user = User::where('username', $username)->first();
if (!$user) {
return msg('用户不存在', "请输入正确的用户名后继续。", "/user/login", "返回");
}
if (!password_verify($password, $user->password)) {
return msg('密码错误', "请输入正确的密码后继续。", "/user/login", "返回");
}
session([
'nickname' => $user->nickname,
'user_id' => $user->id
]);
$logs = new UsersLog();
$logs->user_id = $user->id;
$logs->ip = $request->getRemoteIp() . ":" . $request->getRemotePort();
$logs->save();
return msg('登录成功', "您可以继续操作。");
}
public function password(Request $request): \support\Response
{
if (session('nickname', "未登录") === "未登录") return msg('请先登录', "请登录后继续。", "/user/login", '立即登录');
View::assign(["nickname" => session('nickname', "未登录")]);
View::assign([
"header" => view('common/header')->rawBody(),
"footer" => view('common/footer')->rawBody()
]);
return view('user/password');
}
public function update(Request $request)
{
// 修改密码
if (session('nickname', "未登录") === "未登录") return msg('请先登录', "请登录后继续。", "/user/login", '立即登录');
$password = $request->post('password', '');
$repassword = $request->post('confirmPassword', '');
if (empty($password)) {
return msg('请输入密码', "请输入密码后继续。", "/user/password", "返回");
}
if (strlen($password) < 6 || strlen($password) > 16) {
return msg('密码长度为6-16位', "请输入6-16位密码后继续。", "/user/password", "返回");
}
if ($password !== $repassword) {
return msg('两次密码不一致', "请输入两次密码后继续。", "/user/password", "返回");
}
$user = User::where('id', session('user_id'))->first();
$user->password = password_hash($password, PASSWORD_DEFAULT);
$user->save();
session(['nickname' => null, 'user_id' => null]);
return msg('修改成功', "请登录后继续。", "/user/login", "登录");
}
public function store(Request $request)
{
View::assign(["nickname" => session('nickname', "未登录")]);
View::assign([
"header" => view('common/header')->rawBody(),
"footer" => view('common/footer')->rawBody()
]);
$agreement = $request->post('agreement', 'off');
if ($agreement !== 'on') {
return msg('请先同意用户协议', "请勾选用户协议后继续。", "/user/regedit", "返回");
}
$username = $request->post('username', '');
$nickname = $request->post('nickname', '');
$nickname = empty($nickname) ? '用户' . uniqid() : $nickname;
$password = $request->post('password', '');
$repassword = $request->post('confirmPassword', '');
if (empty($username) || empty($password)) {
return msg('请输入用户名或密码', "请输入用户名或密码后继续。", "/user/regedit", "返回");
}
if (strlen($username) < 4 || strlen($username) > 16) {
return msg('用户名长度为4-16位', "请输入4-16位用户名后继续。", "/user/regedit", "返回");
}
if (strlen($password) < 6 || strlen($password) > 16) {
return msg('密码长度为6-16位', "请输入6-16位密码后继续。", "/user/regedit", "返回");
}
if ($password !== $repassword) {
return msg('两次密码不一致', "请输入两次密码后继续。", "/user/regedit", "返回");
}
$user = User::where('username', $username)->first();
if ($user) {
return msg('用户已存在', "请输入其他用户名后继续。", "/user/regedit", "返回");
}
$user = new User();
$user->username = $username;
$user->nickname = $nickname;
$user->password = password_hash($password, PASSWORD_DEFAULT);
$user->save();
return msg('注册成功', "请登录后继续。", "/user/login", "登录");
}
public function token(Request $request)
{
if (session('nickname', "未登录") === "未登录") return msg('请先登录', "请登录后继续。", "/user/login", '立即登录');
View::assign(["nickname" => session('nickname', "未登录")]);
View::assign([
"header" => view('common/header')->rawBody(),
"footer" => view('common/footer')->rawBody()
]);
$user = User::where('id', session('user_id'))->first();
if (!$user) {
session(['nickname' => null, 'user_id' => null]);
return msg('用户不存在', "请重新登录后继续。", "/user/login", "登录");
}
$users_token = UsersToken::where('user_id', $user->id)->first();
if (!$users_token) {
$users_token = new UsersToken();
$users_token->user_id = $user->id;
$uid_md5 = md5($user->id . uniqid());
$users_token->token = strtoupper($user->id) . $uid_md5;
$users_token->save();
}
return view('user/token', [
'token' => $users_token->token,
'updateTime' => $users_token->updated_at
]);
}
public function tokenUpdate(Request $request)
{
if (session('nickname', "未登录") === "未登录") return msg('请先登录', "请登录后继续。", "/user/login", '立即登录');
View::assign(["nickname" => session('nickname', "未登录")]);
View::assign([
"header" => view('common/header')->rawBody(),
"footer" => view('common/footer')->rawBody()
]);
$user = User::where('id', session('user_id'))->first();
if (!$user) {
session(['nickname' => null, 'user_id' => null]);
return msg('用户不存在', "请重新登录后继续。", "/user/login", "登录");
}
$users_token = UsersToken::where('user_id', $user->id)->first();
if (!$users_token) {
$users_token = new UsersToken();
$users_token->user_id = $user->id;
$uid_md5 = md5($user->id . uniqid());
$users_token->token = strtoupper($user->id) . $uid_md5;
$users_token->save();
return msg('更新成功', "请使用新的密钥访问所有应用。", "/user/token", "我的密钥");
}
$users_token->token = strtoupper($user->id) . md5($user->id . uniqid());
$users_token->save();
return msg('更新成功', "请使用新的密钥访问所有应用。", "/user/token", "我的密钥");
}
public function cleanStart(Request $request)
{
if (session('nickname', "未登录") === "未登录") return msg('请先登录', "请登录后继续。", "/user/login", '立即登录');
View::assign(["nickname" => session('nickname', "未登录")]);
View::assign([
"header" => view('common/header')->rawBody(),
"footer" => view('common/footer')->rawBody()
]);
$user = User::where('id', session('user_id'))->first();
if (!$user) {
session(['nickname' => null, 'user_id' => null]);
return msg('用户不存在', "请重新登录后继续。", "/user/login", "登录");
}
$smsContent = SmsContent::where('user_id', $user->id)->get();
foreach ($smsContent as $item) {
$item->delete();
}
return msg('清除成功', "所有短信已清除。", "/user/clean", "返回");
}
public function clean(Request $request)
{
if (session('nickname', "未登录") === "未登录") return msg('请先登录', "请登录后继续。", "/user/login", '立即登录');
View::assign(["nickname" => session('nickname', "未登录")]);
View::assign([
"header" => view('common/header')->rawBody(),
"footer" => view('common/footer')->rawBody()
]);
$user = User::where('id', session('user_id'))->first();
if (!$user) {
session(['nickname' => null, 'user_id' => null]);
return msg('用户不存在', "请重新登录后继续。", "/user/login", "登录");
}
return view('user/clean');
}
public function logs(Request $request)
{
if (session('nickname', "未登录") === "未登录") return msg('请先登录', "请登录后继续。", "/user/login", '立即登录');
View::assign(["nickname" => session('nickname', "未登录")]);
View::assign([
"header" => view('common/header')->rawBody(),
"footer" => view('common/footer')->rawBody()
]);
$user = User::where('id', session('user_id'))->first();
if (!$user) {
session(['nickname' => null, 'user_id' => null]);
return msg('用户不存在', "请重新登录后继续。", "/user/login", "登录");
}
$logs = UsersLog::where('user_id', $user->id)->orderBy('id', 'desc')->paginate(100);
return view('user/logs', [
'logs' => $logs
]);
}
}

18
app/functions.php Normal file
View File

@ -0,0 +1,18 @@
<?php
use support\View;
/**
* Here is your custom functions.
*/
function msg($title = '', $content = '', $link = '/', $link_text = '返回首页')
{
View::assign(["nickname" => session('nickname', "未登录")]);
View::assign([
"header" => view('common/header')->rawBody(),
"footer" => view('common/footer')->rawBody()
]);
return view('common/msg', ['msg_title' => $title, 'msg_content' => $content, 'link' => $link, 'link_text' => $link_text]);
}

View File

@ -0,0 +1,42 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
/**
* Class StaticFile
* @package app\middleware
*/
class StaticFile implements MiddlewareInterface
{
public function process(Request $request, callable $next): Response
{
// Access to files beginning with. Is prohibited
if (strpos($request->path(), '/.') !== false) {
return response('<h1>403 forbidden</h1>', 403);
}
/** @var Response $response */
$response = $next($request);
// Add cross domain HTTP header
/*$response->withHeaders([
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Credentials' => 'true',
]);*/
return $response;
}
}

9
app/model/SmsApp.php Normal file
View File

@ -0,0 +1,9 @@
<?php
namespace app\model;
use support\Model;
class SmsApp extends Model
{
}

9
app/model/SmsContent.php Normal file
View File

@ -0,0 +1,9 @@
<?php
namespace app\model;
use support\Model;
class SmsContent extends Model
{
}

29
app/model/Test.php Normal file
View File

@ -0,0 +1,29 @@
<?php
namespace app\model;
use support\Model;
class Test extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'test';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
}

9
app/model/User.php Normal file
View File

@ -0,0 +1,9 @@
<?php
namespace app\model;
use support\Model;
class User extends Model
{
}

9
app/model/UsersLog.php Normal file
View File

@ -0,0 +1,9 @@
<?php
namespace app\model;
use support\Model;
class UsersLog extends Model
{
}

9
app/model/UsersToken.php Normal file
View File

@ -0,0 +1,9 @@
<?php
namespace app\model;
use support\Model;
class UsersToken extends Model
{
}

10
app/process/Http.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace app\process;
use Webman\App;
class Http extends App
{
}

282
app/process/Monitor.php Normal file
View File

@ -0,0 +1,282 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace app\process;
use FilesystemIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;
use Workerman\Timer;
use Workerman\Worker;
/**
* Class FileMonitor
* @package process
*/
class Monitor
{
/**
* @var array
*/
protected array $paths = [];
/**
* @var array
*/
protected array $extensions = [];
/**
* @var array
*/
protected array $loadedFiles = [];
/**
* @var int
*/
protected int $ppid = 0;
/**
* Pause monitor
* @return void
*/
public static function pause(): void
{
file_put_contents(static::lockFile(), time());
}
/**
* Resume monitor
* @return void
*/
public static function resume(): void
{
clearstatcache();
if (is_file(static::lockFile())) {
unlink(static::lockFile());
}
}
/**
* Whether monitor is paused
* @return bool
*/
public static function isPaused(): bool
{
clearstatcache();
return file_exists(static::lockFile());
}
/**
* Lock file
* @return string
*/
protected static function lockFile(): string
{
return runtime_path('monitor.lock');
}
/**
* FileMonitor constructor.
* @param $monitorDir
* @param $monitorExtensions
* @param array $options
*/
public function __construct($monitorDir, $monitorExtensions, array $options = [])
{
$this->ppid = function_exists('posix_getppid') ? posix_getppid() : 0;
static::resume();
$this->paths = (array)$monitorDir;
$this->extensions = $monitorExtensions;
foreach (get_included_files() as $index => $file) {
$this->loadedFiles[$file] = $index;
if (strpos($file, 'webman-framework/src/support/App.php')) {
break;
}
}
if (!Worker::getAllWorkers()) {
return;
}
$disableFunctions = explode(',', ini_get('disable_functions'));
if (in_array('exec', $disableFunctions, true)) {
echo "\nMonitor file change turned off because exec() has been disabled by disable_functions setting in " . PHP_CONFIG_FILE_PATH . "/php.ini\n";
} else {
if ($options['enable_file_monitor'] ?? true) {
Timer::add(1, function () {
$this->checkAllFilesChange();
});
}
}
$memoryLimit = $this->getMemoryLimit($options['memory_limit'] ?? null);
if ($memoryLimit && ($options['enable_memory_monitor'] ?? true)) {
Timer::add(60, [$this, 'checkMemory'], [$memoryLimit]);
}
}
/**
* @param $monitorDir
* @return bool
*/
public function checkFilesChange($monitorDir): bool
{
static $lastMtime, $tooManyFilesCheck;
if (!$lastMtime) {
$lastMtime = time();
}
clearstatcache();
if (!is_dir($monitorDir)) {
if (!is_file($monitorDir)) {
return false;
}
$iterator = [new SplFileInfo($monitorDir)];
} else {
// recursive traversal directory
$dirIterator = new RecursiveDirectoryIterator($monitorDir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS);
$iterator = new RecursiveIteratorIterator($dirIterator);
}
$count = 0;
foreach ($iterator as $file) {
$count ++;
/** var SplFileInfo $file */
if (is_dir($file->getRealPath())) {
continue;
}
// check mtime
if (in_array($file->getExtension(), $this->extensions, true) && $lastMtime < $file->getMTime()) {
$lastMtime = $file->getMTime();
if (DIRECTORY_SEPARATOR === '/' && isset($this->loadedFiles[$file->getRealPath()])) {
echo "$file updated but cannot be reloaded because only auto-loaded files support reload.\n";
continue;
}
$var = 0;
exec('"'.PHP_BINARY . '" -l ' . $file, $out, $var);
if ($var) {
continue;
}
echo $file . " updated and reload\n";
// send SIGUSR1 signal to master process for reload
if (DIRECTORY_SEPARATOR === '/' && $this->ppid > 1) {
posix_kill($this->ppid, SIGUSR1);
} else {
return true;
}
break;
}
}
if (!$tooManyFilesCheck && $count > 1000) {
echo "Monitor: There are too many files ($count files) in $monitorDir which makes file monitoring very slow\n";
$tooManyFilesCheck = 1;
}
return false;
}
/**
* @return bool
*/
public function checkAllFilesChange(): bool
{
if (static::isPaused()) {
return false;
}
foreach ($this->paths as $path) {
if ($this->checkFilesChange($path)) {
return true;
}
}
return false;
}
/**
* @param $memoryLimit
* @return void
*/
public function checkMemory($memoryLimit): void
{
if (static::isPaused() || $memoryLimit <= 0) {
return;
}
$ppid = $this->ppid;
if ($ppid <= 1) {
return;
}
$processPath = "/proc/$ppid";
if (!is_dir($processPath)) {
// Process not exist
$this->ppid = 0;
return;
}
$childrenFile = "/proc/$ppid/task/$ppid/children";
if (!is_file($childrenFile) || !($children = file_get_contents($childrenFile))) {
return;
}
foreach (explode(' ', $children) as $pid) {
$pid = (int)$pid;
$statusFile = "/proc/$pid/status";
if (!is_file($statusFile) || !($status = file_get_contents($statusFile))) {
continue;
}
$mem = 0;
if (preg_match('/VmRSS\s*?:\s*?(\d+?)\s*?kB/', $status, $match)) {
$mem = $match[1];
}
$mem = (int)($mem / 1024);
if ($mem >= $memoryLimit) {
posix_kill($pid, SIGINT);
}
}
}
/**
* Get memory limit
* @param $memoryLimit
* @return int
*/
protected function getMemoryLimit($memoryLimit): int
{
if ($memoryLimit === 0) {
return 0;
}
$usePhpIni = false;
if (!$memoryLimit) {
$memoryLimit = ini_get('memory_limit');
$usePhpIni = true;
}
if ($memoryLimit == -1) {
return 0;
}
$unit = strtolower($memoryLimit[strlen($memoryLimit) - 1]);
$memoryLimit = (int)$memoryLimit;
if ($unit === 'g') {
$memoryLimit = 1024 * $memoryLimit;
} else if ($unit === 'k') {
$memoryLimit = ($memoryLimit / 1024);
} else if ($unit === 'm') {
$memoryLimit = (int)($memoryLimit);
} else if ($unit === 't') {
$memoryLimit = (1024 * 1024 * $memoryLimit);
} else {
$memoryLimit = ($memoryLimit / (1024 * 1024));
}
if ($memoryLimit < 50) {
$memoryLimit = 50;
}
if ($usePhpIni) {
$memoryLimit = (0.8 * $memoryLimit);
}
return (int)$memoryLimit;
}
}

View File

@ -0,0 +1,2 @@
</body>
</html>

View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>SmsRecipient</title>
<link rel="stylesheet" href="/assets/layui/css/layui.css">
<link rel="stylesheet" href="/assets/style.css">
</head>
<body>
<script src="/assets/layui/layui.js"></script>
<div>
<ul class="layui-nav layui-bg-cyan">
<li class="layui-nav-item"><a href="#">SmsRecipient</a></li>
<li class="layui-nav-item"><a href="/">首页</a></li>
<?php if($nickname == "未登录"): ?>
<li class="layui-nav-item"><a href="/user/login">登录系统</a></li>
<?php else: ?>
<li class="layui-nav-item"><a href="/sms">短信中心</a></li>
<li class="layui-nav-item"><a href="/sms/app">应用管理</a></li>
<li class="layui-nav-item">
<a href="javascript:;"><?= $nickname ?></a>
<dl class="layui-nav-child">
<dd><a href="/user/logs">登录日志</a></dd>
<dd><a href="/user/clean">清空短信</a></dd>
<dd><a href="/user/password">修改密码</a></dd>
<dd><a href="/user/token">我的密钥</a></dd>
<dd><a href="/user/logout">退出系统</a></dd>
</dl>
</li>
<?php endif; ?>
</ul>
</div>

15
app/view/common/msg.html Normal file
View File

@ -0,0 +1,15 @@
<?= $header ?>
<div class="layui-bg-gray" style="padding: 16px;">
<div class="layui-row layui-col-space15">
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header"><?= $msg_title ?></div>
<div class="layui-card-body">
<?= $msg_content ?>
<br>
<a href="<?= $link ?>" class="layui-btn"><?= $link_text ?></a>
</div>
</div>
</div>
</div>
<?= $footer ?>

View File

@ -0,0 +1,3 @@
<?= $header ?>
后续补充……
<?= $footer ?>

35
app/view/sms/app.html Normal file
View File

@ -0,0 +1,35 @@
<?= $header ?>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">应用管理</div>
<div class="layui-card-body">
<a href="/sms/create-app/" class="layui-btn">创建应用</a>
<hr>
<table class="layui-table">
<thead>
<tr>
<th>应用名称</th>
<th>UUID</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php foreach ($sms_app as $app): ?>
<tr>
<td><?= $app['app_name'] ?></td>
<td><?= $app['app_uuid'] ?></td>
<td><?= $app['created_at'] ?></td>
<td>
<a href="/sms/delete-app?id=<?= $app['id'] ?>" class="layui-btn layui-btn-xs">删除</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?= $footer ?>

View File

@ -0,0 +1,30 @@
<?= $header ?>
<br>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">创建应用</div>
<div class="layui-card-body">
<form class="layui-form" method="post" action="/sms/app-store">
<div>
<div class="layui-form-item">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-password"></i>
</div>
<input type="text" name="app_name" value="" lay-verify="required" placeholder="应用名称"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<button class="layui-btn" lay-submit>创建</button>
</div>
</div>
</form>
</div>
</div>
</div>
<?= $footer ?>

223
app/view/sms/index.html Normal file
View File

@ -0,0 +1,223 @@
<?= $header ?>
<div>
<div class="layui-card">
<div class="layui-card-header">短信中心</div>
<div class="layui-card-body">
<form class="layui-form ii" style="display: flex;flex-direction: row;flex-wrap: wrap;">
<div class="layui-input-group">
<div class="layui-input-prefix">
应用名称
</div>
<select name="app_name">
<option value="">请选择</option>
<?php foreach($sms_app as $app): ?>
<?php if(array_key_exists('app_name', $get) && $app['app_name'] == $get['app_name']): ?>
<option value="<?= $app['app_name'] ?>" selected><?= $app['app_name'] ?></option>
<?php else: ?>
<option value="<?= $app['app_name'] ?>"><?= $app['app_name'] ?></option>
<?php endif; ?>
<?php endforeach; ?>
</select>
</div>
<div class="layui-input-group">
<div class="layui-input-prefix">
UUID
</div>
<select name="app_uuid">
<option value="">请选择</option>
<?php foreach($sms_app as $app): ?>
<?php if(array_key_exists('app_uuid', $get) && $app['app_uuid'] == $get['app_uuid']): ?>
<option value="<?= $app['app_uuid'] ?>" selected><?= $app['app_uuid'] ?></option>
<?php else: ?>
<option value="<?= $app['app_uuid'] ?>"><?= $app['app_uuid'] ?></option>
<?php endif; ?>
<?php endforeach; ?>
</select>
</div>
<div class="layui-input-group">
<div class="layui-input-prefix">
from
</div>
<input type="text" name="from" placeholder="模糊搜索"
value="<?= array_key_exists('from', $get) ? $get['from'] : ''?>"
class="layui-input">
</div>
<div class="layui-input-group">
<div class="layui-input-prefix">
content
</div>
<input type="text" name="content" placeholder="模糊搜索"
value="<?= array_key_exists('content', $get) ? $get['content'] : ''?>"
class="layui-input">
</div>
<div class="layui-input-group">
<div class="layui-input-prefix">
IP
</div>
<input type="text" name="ip" placeholder="模糊搜索"
value="<?= array_key_exists('ip', $get) ? $get['ip'] : ''?>"
class="layui-input">
</div>
<div class="layui-input-group">
<div class="layui-input-prefix">
起始时间
</div>
<input type="text" name="receive_time[0]" class="layui-input" id="rt0"
value="<?= array_key_exists('receive_time', $get) ? $get['receive_time'][0] : ''?>"
placeholder="yyyy-MM-dd HH:mm:ss">
</div>
<div class="layui-input-group">
<div class="layui-input-prefix">
结束时间
</div>
<input type="text" name="receive_time[1]" class="layui-input" id="rt1"
value="<?= array_key_exists('receive_time', $get) ? $get['receive_time'][1] : ''?>"
placeholder="yyyy-MM-dd HH:mm:ss">
</div>
<div class="layui-input-group">
<button class="layui-btn" lay-submit>搜索</button>
</div>
<div class="layui-input-group">
<a class="layui-btn" href="/sms">重置</a>
</div>
</form>
<hr>
<table class="layui-table" lay-even>
<colgroup>
<col>
<col>
<col>
<col>
<col>
<col>
<col>
<col>
<col>
</colgroup>
<thead>
<tr>
<th>#</th>
<th>应用名称</th>
<th>UUID</th>
<th>Headers</th>
<th>From</th>
<th>Content</th>
<th>IP</th>
<th>接收时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php foreach ($smsContent as $item): ?>
<tr>
<td><?= $item['id'] ?></td>
<td><?= $item['app_name'] ?></td>
<td><?= $item['app_uuid'] ?></td>
<td>
<div>
<?php if(!empty($item['headers'])): ;?>
<?php $headers = json_decode($item['headers'],true);if (json_last_error() !== JSON_ERROR_NONE) {$headers = [];} ?>
<?php if(array_key_exists('accept-language', $headers)) unset($headers['accept-language']); ?>
<?php if(array_key_exists('user-agent', $headers)) unset($headers['user-agent']); ?>
<?php if(array_key_exists('content-type', $headers)) unset($headers['content-type']); ?>
<?php if(array_key_exists('content-length', $headers)) unset($headers['content-length']); ?>
<?php if(array_key_exists('host', $headers)) unset($headers['host']); ?>
<?php if(array_key_exists('connection', $headers)) unset($headers['connection']); ?>
<?php if(array_key_exists('accept-encoding', $headers)) unset($headers['accept-encoding']); ?>
<?php if(array_key_exists('cache-control', $headers)) unset($headers['cache-control']); ?>
<?php foreach ($headers as $key => $value): ?>
<div>
<?= $key ?>: <?= $value ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</td>
<td><?= $item['from'] ?></td>
<td><?= $item['content'] ?></td>
<td><?= $item['ip'] ?></td>
<td><?= $item['created_at'] ?></td>
<td>
<a href="/sms/delete?id=<?= $item['id'] ?>" class="layui-btn layui-btn-xs">删除记录</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<hr>
<div id="pn"></div>
</div>
</div>
</div>
<script>
function redirectWithParams(newPage, newLimit) {
// 获取当前URL对象
const currentUrl = new URL(window.location.href);
// 覆盖或新增page和limit参数
currentUrl.searchParams.set('page', newPage);
currentUrl.searchParams.set('limit', newLimit);
// 跳转到新URL
window.location.href = currentUrl.toString();
}
layui.use(function () {
var laydate = layui.laydate;
// 日期时间选择器
laydate.render({
elem: '#rt0',
type: 'datetime'
});
// 日期时间选择器
laydate.render({
elem: '#rt1',
type: 'datetime'
});
var laypage = layui.laypage;
var layer = layui.layer;
// 只显示上一页、下一页、当前页
laypage.render({
elem: 'pn',
count: <?= $smsContent->total() ?>,
curr: <?= $smsContent->currentPage() ?>,
limit: <?= $smsContent->perPage() ?>,
limits: [10, 20, 30, 50, 100],
first: false,
last: false,
layout: ['count', 'prev', 'page', 'next', 'limit', 'refresh', 'skip'],
jump: function (obj, first) {
// 首次不执行
if (!first) {
redirectWithParams(obj.curr,obj.limit)
}
}
});
});
</script>
<?= $footer ?>

13
app/view/user/clean.html Normal file
View File

@ -0,0 +1,13 @@
<?= $header ?>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">清空短信</div>
<div class="layui-card-body">
<br>
<a href="/user/clean-start" class="layui-btn">确认清空短信</a>
</div>
</div>
</div>
<?= $footer ?>

42
app/view/user/login.html Normal file
View File

@ -0,0 +1,42 @@
<?= $header ?>
<br>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">登录系统</div>
<div class="layui-card-body">
<form class="layui-form" method="post" action="/user/enter">
<div class="demo-login-container">
<div class="layui-form-item">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-username"></i>
</div>
<input type="text" name="username" value="" lay-verify="required" placeholder="用户名" lay-reqtext="请填写用户名" autocomplete="off" class="layui-input" lay-affix="clear">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-password"></i>
</div>
<input type="password" name="password" value="" lay-verify="required" placeholder="密 码" lay-reqtext="请填写密码" autocomplete="off" class="layui-input" lay-affix="eye">
</div>
</div>
<div class="layui-form-item">
<a href="#forget" style="float: right; margin-top: 7px;">忘记密码?</a>
</div>
<div class="layui-form-item">
<button class="layui-btn layui-btn-fluid" lay-submit lay-filter="demo-login">登录</button>
</div>
<div class="layui-form-item demo-login-other">
没有账号?<a href="/user/regedit">注册一个</a>
</div>
</div>
</form>
</div>
</div>
</div>
<?= $footer ?>

26
app/view/user/logs.html Normal file
View File

@ -0,0 +1,26 @@
<?= $header ?>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">登录日志</div>
<div class="layui-card-body">
<br>
<div class="layui-timeline">
<?php foreach($logs as $log): ?>
<div class="layui-timeline-item">
<i class="layui-icon layui-timeline-axis layui-icon-right"></i>
<div class="layui-timeline-content layui-text">
<div class="layui-timeline-title"><?= $log['created_at'] ?></div>
<p><?= $log['ip'] ?></p>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<?= $footer ?>

View File

@ -0,0 +1,34 @@
<?= $header ?>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">修改密码</div>
<div class="layui-card-body">
<form class="layui-form" method="post" action="/user/update">
<div class="layui-form-item">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-password"></i>
</div>
<input type="password" name="password" value="" lay-verify="required" placeholder="密码"
autocomplete="off" class="layui-input" id="reg-password" lay-affix="eye"></input>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-password"></i>
</div>
<input type="password" name="confirmPassword" value="" lay-verify="required|confirmPassword"
placeholder="确认密码" autocomplete="off" class="layui-input" lay-affix="eye"></input>
</div>
</div>
<div class="layui-form-item">
<button class="layui-btn layui-btn-fluid" lay-submit>修改密码</button>
</div>
</form>
</div>
</div>
</div>
<?= $footer ?>

View File

@ -0,0 +1,65 @@
<?= $header ?>
<br>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">注册账号</div>
<div class="layui-card-body">
<form class="layui-form" method="post" action="/user/store">
<div>
<div class="layui-form-item">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-password"></i>
</div>
<input type="text" name="username" value="" lay-verify="required" placeholder="用户名"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-password"></i>
</div>
<input type="password" name="password" value="" lay-verify="required" placeholder="密码"
autocomplete="off" class="layui-input" id="reg-password" lay-affix="eye">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-password"></i>
</div>
<input type="password" name="confirmPassword" value="" lay-verify="required|confirmPassword"
placeholder="确认密码" autocomplete="off" class="layui-input" lay-affix="eye">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-username"></i>
</div>
<input type="text" name="nickname" value="" placeholder="昵称"
autocomplete="off" class="layui-input" lay-affix="clear">
</div>
</div>
<div class="layui-form-item">
<input type="checkbox" name="agreement" lay-verify="required" lay-skin="primary" title="同意">
<a href="/user_agreement.html" target="_blank" style="position: relative; top: 6px; left: -15px;">
<ins>用户协议</ins>
</a>
</div>
<div class="layui-form-item">
<button class="layui-btn layui-btn-fluid" lay-submit>注册</button>
</div>
<div class="layui-form-item">
<a href="/user/login">登录已有帐号</a>
</div>
</div>
</form>
</div>
</div>
</div>
<?= $footer ?>

16
app/view/user/token.html Normal file
View File

@ -0,0 +1,16 @@
<?= $header ?>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">我的密钥</div>
<div class="layui-card-body">
<p>您当前的密钥为:<?= $token ?></p>
<p>更新于 <?= $updateTime ?></p>
<p>请妥善保管,不要泄露给别人。</p>
<br>
<a href="/user/token-update" class="layui-btn">重置密钥</a>
</div>
</div>
</div>
<?= $footer ?>

60
composer.json Normal file
View File

@ -0,0 +1,60 @@
{
"name": "workerman/webman",
"type": "project",
"keywords": [
"high performance",
"http service"
],
"homepage": "https://www.workerman.net",
"license": "MIT",
"description": "High performance HTTP Service Framework.",
"authors": [
{
"name": "walkor",
"email": "walkor@workerman.net",
"homepage": "https://www.workerman.net",
"role": "Developer"
}
],
"support": {
"email": "walkor@workerman.net",
"issues": "https://github.com/walkor/webman/issues",
"forum": "https://wenda.workerman.net/",
"wiki": "https://workerman.net/doc/webman",
"source": "https://github.com/walkor/webman"
},
"require": {
"php": ">=8.1",
"workerman/webman-framework": "^2.1",
"monolog/monolog": "^2.0",
"webman/database": "^2.1",
"illuminate/pagination": "^12.11",
"illuminate/events": "^12.10",
"symfony/var-dumper": "^7.2",
"vlucas/phpdotenv": "^5.6"
},
"suggest": {
"ext-event": "For better performance. "
},
"autoload": {
"psr-4": {
"": "./",
"app\\": "./app",
"App\\": "./app",
"app\\View\\Components\\": "./app/view/components"
}
},
"scripts": {
"post-package-install": [
"support\\Plugin::install"
],
"post-package-update": [
"support\\Plugin::install"
],
"pre-package-uninstall": [
"support\\Plugin::uninstall"
]
},
"minimum-stability": "dev",
"prefer-stable": true
}

4245
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

26
config/app.php Normal file
View File

@ -0,0 +1,26 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use support\Request;
return [
'debug' => true,
'error_reporting' => E_ALL,
'default_timezone' => 'Asia/Shanghai',
'request_class' => Request::class,
'public_path' => base_path() . DIRECTORY_SEPARATOR . 'public',
'runtime_path' => base_path(false) . DIRECTORY_SEPARATOR . 'runtime',
'controller_suffix' => 'Controller',
'controller_reuse' => false,
];

21
config/autoload.php Normal file
View File

@ -0,0 +1,21 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [
'files' => [
base_path() . '/app/functions.php',
base_path() . '/support/Request.php',
base_path() . '/support/Response.php',
]
];

17
config/bootstrap.php Normal file
View File

@ -0,0 +1,17 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [
support\bootstrap\Session::class,
];

15
config/container.php Normal file
View File

@ -0,0 +1,15 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return new Webman\Container;

29
config/database.php Normal file
View File

@ -0,0 +1,29 @@
<?php
return [
'default' => 'mysql',
'connections' => [
'mysql' => [
'driver' => 'mysql',
'host' => getenv('DB_HOST'),
'port' => getenv('DB_PORT'),
'database' => getenv('DB_DATABASE'),
'username' => getenv('DB_USERNAME'),
'password' => getenv('DB_PASSWORD'),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_general_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
'options' => [
PDO::ATTR_EMULATE_PREPARES => false, // Must be false for Swoole and Swow drivers.
],
'pool' => [
'max_connections' => 5,
'min_connections' => 1,
'wait_timeout' => 3,
'idle_timeout' => 60,
'heartbeat_interval' => 50,
],
],
],
];

15
config/dependence.php Normal file
View File

@ -0,0 +1,15 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [];

17
config/exception.php Normal file
View File

@ -0,0 +1,17 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [
'' => support\exception\Handler::class,
];

32
config/log.php Normal file
View File

@ -0,0 +1,32 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [
'default' => [
'handlers' => [
[
'class' => Monolog\Handler\RotatingFileHandler::class,
'constructor' => [
runtime_path() . '/logs/webman.log',
7, //$maxFiles
Monolog\Logger::DEBUG,
],
'formatter' => [
'class' => Monolog\Formatter\LineFormatter::class,
'constructor' => [null, 'Y-m-d H:i:s', true],
],
]
],
],
];

15
config/middleware.php Normal file
View File

@ -0,0 +1,15 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [];

62
config/process.php Normal file
View File

@ -0,0 +1,62 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use support\Log;
use support\Request;
use app\process\Http;
global $argv;
return [
'webman' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8787',
'count' => cpu_count() * 4,
'user' => '',
'group' => '',
'reusePort' => false,
'eventLoop' => '',
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
// File update detection and automatic reload
'monitor' => [
'handler' => app\process\Monitor::class,
'reloadable' => false,
'constructor' => [
// Monitor these directories
'monitorDir' => array_merge([
app_path(),
config_path(),
base_path() . '/process',
base_path() . '/support',
base_path() . '/resource',
base_path() . '/.env',
], glob(base_path() . '/plugin/*/app'), glob(base_path() . '/plugin/*/config'), glob(base_path() . '/plugin/*/api')),
// Files with these suffixes will be monitored
'monitorExtensions' => [
'php', 'html', 'htm', 'env'
],
'options' => [
'enable_file_monitor' => !in_array('-d', $argv) && DIRECTORY_SEPARATOR === '/',
'enable_memory_monitor' => DIRECTORY_SEPARATOR === '/',
]
]
]
];

17
config/route.php Normal file
View File

@ -0,0 +1,17 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use Webman\Route;
Route::any('/s/{uuid}', [\app\controller\SmsController::class, 's']);

23
config/server.php Normal file
View File

@ -0,0 +1,23 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [
'event_loop' => '',
'stop_timeout' => 2,
'pid_file' => runtime_path() . '/webman.pid',
'status_file' => runtime_path() . '/webman.status',
'stdout_file' => runtime_path() . '/logs/stdout.log',
'log_file' => runtime_path() . '/logs/workerman.log',
'max_package_size' => 10 * 1024 * 1024
];

65
config/session.php Normal file
View File

@ -0,0 +1,65 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use Webman\Session\FileSessionHandler;
use Webman\Session\RedisSessionHandler;
use Webman\Session\RedisClusterSessionHandler;
return [
'type' => 'file', // or redis or redis_cluster
'handler' => FileSessionHandler::class,
'config' => [
'file' => [
'save_path' => runtime_path() . '/sessions',
],
'redis' => [
'host' => '127.0.0.1',
'port' => 6379,
'auth' => '',
'timeout' => 2,
'database' => '',
'prefix' => 'redis_session_',
],
'redis_cluster' => [
'host' => ['127.0.0.1:7000', '127.0.0.1:7001', '127.0.0.1:7001'],
'timeout' => 2,
'auth' => '',
'prefix' => 'redis_session_',
]
],
'session_name' => 'PHPSID',
'auto_update_timestamp' => false,
'lifetime' => 7*24*60*60,
'cookie_lifetime' => 365*24*60*60,
'cookie_path' => '/',
'domain' => '',
'http_only' => true,
'secure' => false,
'same_site' => '',
'gc_probability' => [1, 1000],
];

23
config/static.php Normal file
View File

@ -0,0 +1,23 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
/**
* Static file settings
*/
return [
'enable' => true,
'middleware' => [ // Static file Middleware
//app\middleware\StaticFile::class,
],
];

25
config/translation.php Normal file
View File

@ -0,0 +1,25 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
/**
* Multilingual configuration
*/
return [
// Default language
'locale' => 'zh_CN',
// Fallback language
'fallback_locale' => ['zh_CN', 'en'],
// Folder where language files are stored
'path' => base_path() . '/resource/translations',
];

22
config/view.php Normal file
View File

@ -0,0 +1,22 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use support\view\Raw;
use support\view\Twig;
use support\view\Blade;
use support\view\ThinkPHP;
return [
'handler' => Raw::class
];

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3
public/assets/style.css Normal file
View File

@ -0,0 +1,3 @@
.ii > .layui-input-group {
margin-right: 10px;
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

5
start.php Normal file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env php
<?php
chdir(__DIR__);
require_once __DIR__ . '/vendor/autoload.php';
support\App::run();

24
support/Request.php Normal file
View File

@ -0,0 +1,24 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
/**
* Class Request
* @package support
*/
class Request extends \Webman\Http\Request
{
}

24
support/Response.php Normal file
View File

@ -0,0 +1,24 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
/**
* Class Response
* @package support
*/
class Response extends \Webman\Http\Response
{
}

139
support/bootstrap.php Normal file
View File

@ -0,0 +1,139 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use Dotenv\Dotenv;
use support\Log;
use Webman\Bootstrap;
use Webman\Config;
use Webman\Middleware;
use Webman\Route;
use Webman\Util;
use Workerman\Events\Select;
use Workerman\Worker;
$worker = $worker ?? null;
if (empty(Worker::$eventLoopClass)) {
Worker::$eventLoopClass = Select::class;
}
set_error_handler(function ($level, $message, $file = '', $line = 0) {
if (error_reporting() & $level) {
throw new ErrorException($message, 0, $level, $file, $line);
}
});
if ($worker) {
register_shutdown_function(function ($startTime) {
if (time() - $startTime <= 0.1) {
sleep(1);
}
}, time());
}
if (class_exists('Dotenv\Dotenv') && file_exists(base_path(false) . '/.env')) {
if (method_exists('Dotenv\Dotenv', 'createUnsafeMutable')) {
Dotenv::createUnsafeMutable(base_path(false))->load();
} else {
Dotenv::createMutable(base_path(false))->load();
}
}
Config::clear();
support\App::loadAllConfig(['route']);
if ($timezone = config('app.default_timezone')) {
date_default_timezone_set($timezone);
}
foreach (config('autoload.files', []) as $file) {
include_once $file;
}
foreach (config('plugin', []) as $firm => $projects) {
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['autoload']['files'] ?? [] as $file) {
include_once $file;
}
}
foreach ($projects['autoload']['files'] ?? [] as $file) {
include_once $file;
}
}
Middleware::load(config('middleware', []));
foreach (config('plugin', []) as $firm => $projects) {
foreach ($projects as $name => $project) {
if (!is_array($project) || $name === 'static') {
continue;
}
Middleware::load($project['middleware'] ?? []);
}
Middleware::load($projects['middleware'] ?? [], $firm);
if ($staticMiddlewares = config("plugin.$firm.static.middleware")) {
Middleware::load(['__static__' => $staticMiddlewares], $firm);
}
}
Middleware::load(['__static__' => config('static.middleware', [])]);
foreach (config('bootstrap', []) as $className) {
if (!class_exists($className)) {
$log = "Warning: Class $className setting in config/bootstrap.php not found\r\n";
echo $log;
Log::error($log);
continue;
}
/** @var Bootstrap $className */
$className::start($worker);
}
foreach (config('plugin', []) as $firm => $projects) {
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['bootstrap'] ?? [] as $className) {
if (!class_exists($className)) {
$log = "Warning: Class $className setting in config/plugin/$firm/$name/bootstrap.php not found\r\n";
echo $log;
Log::error($log);
continue;
}
/** @var Bootstrap $className */
$className::start($worker);
}
}
foreach ($projects['bootstrap'] ?? [] as $className) {
/** @var string $className */
if (!class_exists($className)) {
$log = "Warning: Class $className setting in plugin/$firm/config/bootstrap.php not found\r\n";
echo $log;
Log::error($log);
continue;
}
/** @var Bootstrap $className */
$className::start($worker);
}
}
$directory = base_path() . '/plugin';
$paths = [config_path()];
foreach (Util::scanDir($directory) as $path) {
if (is_dir($path = "$path/config")) {
$paths[] = $path;
}
}
Route::load($paths);

3
windows.bat Normal file
View File

@ -0,0 +1,3 @@
CHCP 65001
php windows.php
pause

136
windows.php Normal file
View File

@ -0,0 +1,136 @@
<?php
/**
* Start file for windows
*/
chdir(__DIR__);
require_once __DIR__ . '/vendor/autoload.php';
use Dotenv\Dotenv;
use support\App;
use Workerman\Worker;
ini_set('display_errors', 'on');
error_reporting(E_ALL);
if (class_exists('Dotenv\Dotenv') && file_exists(base_path() . '/.env')) {
if (method_exists('Dotenv\Dotenv', 'createUnsafeImmutable')) {
Dotenv::createUnsafeImmutable(base_path())->load();
} else {
Dotenv::createMutable(base_path())->load();
}
}
App::loadAllConfig(['route']);
$errorReporting = config('app.error_reporting');
if (isset($errorReporting)) {
error_reporting($errorReporting);
}
$runtimeProcessPath = runtime_path() . DIRECTORY_SEPARATOR . '/windows';
$paths = [
$runtimeProcessPath,
runtime_path('logs'),
runtime_path('views')
];
foreach ($paths as $path) {
if (!is_dir($path)) {
mkdir($path, 0777, true);
}
}
$processFiles = [];
if (config('server.listen')) {
$processFiles[] = __DIR__ . DIRECTORY_SEPARATOR . 'start.php';
}
foreach (config('process', []) as $processName => $config) {
$processFiles[] = write_process_file($runtimeProcessPath, $processName, '');
}
foreach (config('plugin', []) as $firm => $projects) {
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['process'] ?? [] as $processName => $config) {
$processFiles[] = write_process_file($runtimeProcessPath, $processName, "$firm.$name");
}
}
foreach ($projects['process'] ?? [] as $processName => $config) {
$processFiles[] = write_process_file($runtimeProcessPath, $processName, $firm);
}
}
function write_process_file($runtimeProcessPath, $processName, $firm): string
{
$processParam = $firm ? "plugin.$firm.$processName" : $processName;
$configParam = $firm ? "config('plugin.$firm.process')['$processName']" : "config('process')['$processName']";
$fileContent = <<<EOF
<?php
require_once __DIR__ . '/../../vendor/autoload.php';
use Workerman\Worker;
use Workerman\Connection\TcpConnection;
use Webman\Config;
use support\App;
ini_set('display_errors', 'on');
error_reporting(E_ALL);
if (is_callable('opcache_reset')) {
opcache_reset();
}
if (!\$appConfigFile = config_path('app.php')) {
throw new RuntimeException('Config file not found: app.php');
}
\$appConfig = require \$appConfigFile;
if (\$timezone = \$appConfig['default_timezone'] ?? '') {
date_default_timezone_set(\$timezone);
}
App::loadAllConfig(['route']);
worker_start('$processParam', $configParam);
if (DIRECTORY_SEPARATOR != "/") {
Worker::\$logFile = config('server')['log_file'] ?? Worker::\$logFile;
TcpConnection::\$defaultMaxPackageSize = config('server')['max_package_size'] ?? 10*1024*1024;
}
Worker::runAll();
EOF;
$processFile = $runtimeProcessPath . DIRECTORY_SEPARATOR . "start_$processParam.php";
file_put_contents($processFile, $fileContent);
return $processFile;
}
if ($monitorConfig = config('process.monitor.constructor')) {
$monitorHandler = config('process.monitor.handler');
$monitor = new $monitorHandler(...array_values($monitorConfig));
}
function popen_processes($processFiles)
{
$cmd = '"' . PHP_BINARY . '" ' . implode(' ', $processFiles);
$descriptorspec = [STDIN, STDOUT, STDOUT];
$resource = proc_open($cmd, $descriptorspec, $pipes, null, null, ['bypass_shell' => true]);
if (!$resource) {
exit("Can not execute $cmd\r\n");
}
return $resource;
}
$resource = popen_processes($processFiles);
echo "\r\n";
while (1) {
sleep(1);
if (!empty($monitor) && $monitor->checkAllFilesChange()) {
$status = proc_get_status($resource);
$pid = $status['pid'];
shell_exec("taskkill /F /T /PID $pid");
proc_close($resource);
$resource = popen_processes($processFiles);
}
}