master
zguangjian 2025-07-03 11:08:07 +08:00
commit 2a60f7ddbb
16 changed files with 833 additions and 0 deletions

View File

@ -0,0 +1,11 @@
<?php
namespace app\controller;
use support\Response;
class BaseController
{
}

View File

@ -0,0 +1,113 @@
<?php
namespace app\controller;
use plugin\admin\app\model\Link;
use plugin\admin\app\model\Option;
use plugin\admin\app\model\Product;
use plugin\admin\app\model\ProductCate;
use support\Redis;
use support\Request;
use support\Response;
use Workerman\Connection\TcpConnection;
use Workerman\Protocols\Http\ServerSentEvents;
use Workerman\Timer;
class IndexController extends BaseController
{
//首页seo
public function index(): Response
{
$config = Option::where(['name' => 'system_config'])->value('value');
$config = json_decode($config, true);
return view("index", ['logo' => $config['logo']]);
}
public function header(Request $request): Response
{
$connection = $request->connection;
$id = Timer::add(1, function () use ($connection, &$id) {
if ($connection->getStatus() != TcpConnection::STATUS_ESTABLISHED) {
Timer::del($id);
}
$connection->send(new ServerSentEvents(["data" => json_encode(['date'=>date('Y-m-d H:i:s')])]));
});
return \response("", 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'Connection' => 'keep-alive',
]);
}
public function sse()
{
return view("sse");
}
//获取配置参数
public function getOptions(): Response
{
$options = Option::where(['name' => 'system_config'])->value('value');
$config = json_decode($options, true);
$logo = $config['logo'];
unset($logo["title"]);
$config['banner'] = ImgSrcByArr($config['banner']);
foreach ($config['banner'] as &$banner) {
$banner = explode(",", $banner);
}
$link = Link::orderBy("id", "asc")->select(["title", "url"])->get();
return Success(['options' => ImgSrcByArr($logo), 'banner' => $config['banner'], 'link' => $link]);
}
//产品分类 && 重磅新品
public function getProductCate(): Response
{
$cate = ProductCate::where(['pid' => 0])->orderBy("id")->select(["id", "title", "e_title"])->get()->toArray();
$firstCate = reset($cate);
$productList = Product::where(['cid' => $firstCate['id']])->select(["cover", "name", "label", "new", "url", "property", "corner_mark"])->get();
$childrenCate = ProductCate::whereIn("pid", array_column($cate, 'id'))->select(["id", "title", "pid", "e_title"])->orderBy("id")->get();
$childrenList = [];
foreach ($childrenCate as $item) {
$childrenList[$item['pid']][] = $item;
}
foreach ($productList as $product) {
$cover = explode(",", $product->cover);
foreach ($cover as &$item) {
$item = ImgSrc($item);
}
$product->cover = $cover;
}
foreach ($cate as &$c) {
$c['children'] = $childrenList[$c['id']] ?? [];
}
return Success(['cate' => $cate, 'productList' => $productList]);
}
//产品列表0
public function getProductList(Request $request): Response
{
$cid = $request->get('cid', 0);
$cate = ProductCate::where(['id' => $cid])->first();
if (!$cate) return Error("参数异常!");
$cateList = [];
if ($cate->pid == 0) {
$cateList = ProductCate::where(['pid' => $cate->id])->pluck("id")->toArray();
}
$cateList[] = $cate->id;
$projectList = Product::whereIn("cid", $cateList)->select(["cover", "name", "label", "new", "url", "property", "corner_mark"])->orderBy("id", "desc")->paginate($request->get("per_page", 6));
foreach ($projectList->items() as &$item) {
$item->cover = ImgSrc($item->cover);
$item->cover = explode(",", $item->cover);
}
return Success([
'data' => $projectList->items(),
'total' => $projectList->total(),
'current_page' => $projectList->currentPage(),
'last_page' => $projectList->lastPage(),
]);
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace app\controller;
use Illuminate\Database\Eloquent\Builder;
use plugin\admin\app\model\Article;
use plugin\admin\app\model\ArticleCate;
use support\Redis;
use support\Request;
use support\Response;
class PublicController extends BaseController
{
public function articleList(Request $request): Response
{
$cate = ArticleCate::orderBy("id")->select(["id as value", "title as name"])->get()->toArray();
$cateColumn = array_column($cate, 'name', 'value');
$list = Article::whereDate("date", "<=", date('Y-m-d'))
->where(function (Builder $query) use ($request) {
if ($request->get("cid")) {
$query->where("cid", $request->get("cid"));
}
return $query;
})->select(["id", "title", "date", "intro", "cover", "cid", "link_status", "link"])
->orderByDesc("date")
->paginate($request->get("per_page", 15));
/** @var Article $item */
foreach ($list->items() as $item) {
$item['cname'] = $cateColumn[$item->cid];
$item['cover'] = ImgSrc($item->cover);
unset($item->cid);
}
return Success(['article' => [
"total" => $list->total(),
"data" => $list->items(),
"current_page" => $list->currentPage(),
"last_page" => $list->lastPage(),
"per_page" => $list->perPage(),
], "cate" => $cate]);
}
public function articleDetail(Request $request): Response
{
$id = $request->get('id');
$article = Article::where("id", $id)->select(["id", "title", "date", "intro", "cover", "content"])->first();
if (!$article || strtotime($article->date) > time()) return Error("参数错误!");
$article->content = str_replace("/upload/img/", getHostUrl() . "/upload/img/", $article->content);
Article::where("id", $id)->increment("read");
return Success($article);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace app\exception;
use Webman\Http\Request;
use Webman\Http\Response;
use function json_encode;
class AjaxException extends \RuntimeException
{
public function render(Request $request): Response
{
return new Response(
200,
['Content-Type' => 'application/json'],
json_encode(
[
'code' => $this->getCode() ?: 500,
'msg' => $this->getMessage(),
'data' => [],
'time' => time()
],
JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
)
);
}
}

86
app/functions.php Normal file
View File

@ -0,0 +1,86 @@
<?php
use support\Response;
/**
* Here is your custom functions.
*/
/**
* 成功相响应
* @param string $msg
* @param array $data
* @param int $code
* @return Response
*/
function Success(array|object $data = [], string $msg = '', int $code = 200): Response
{
return new Response(200,
['Content-Type' => 'application/json', 'access-control-allow-origin' => '*'],
json_encode([
'msg' => $msg, 'code' => $code, 'data' => $data, 'time' => time()
]));
}
/**
* 失败响应
* @param string $msg
* @param array $data
* @param int $code
* @return Response
*/
function Error(string $msg = '', array $data = [], int $code = 500): Response
{
return new Response(200,
['Content-Type' => 'application/json', 'access-control-allow-origin' => '*'],
json_encode([
'msg' => $msg, 'code' => $code, 'data' => $data, 'time' => time()
]));
}
function getHostUrl(): bool|array|string
{
return getenv(getenv('ONLINE') ? 'APP_HOST' : 'DEV_HOST');
}
//
function ImgSrc(string $str): string
{
if (str_contains($str, '/app/admin/upload')) {
if (str_contains(",", $str)) {
$arr = explode(",", $str);
foreach ($arr as &$v) {
$v = getHostUrl() . str_replace('/app/admin/upload', '/upload', $v);
}
return implode(",", $arr);
} else {
return getHostUrl() . str_replace('/app/admin/upload', '/upload', $str);
}
} else {
if (str_contains($str, '/upload/') && str_starts_with($str, "/upload")) {
$arr = explode(",", $str);
foreach ($arr as &$v) {
$v = getHostUrl() . $v;
}
return implode(",", $arr);
}
}
return $str;
}
//图片域名
function ImgSrcByArr(array $arr): array
{
foreach ($arr as &$v) {
$v = ImgSrc($v);
};
return $arr;
}
//上传url 转全路径
function UploadUrl(string $str): string
{
return str_replace('/app/admin/upload', '/upload', $str);
}

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 $handler): Response
{
// Access to files beginning with. Is prohibited
if (str_contains($request->path(), '/.')) {
return response('<h1>403 forbidden</h1>', 403);
}
/** @var Response $response */
$response = $handler($request);
// Add cross domain HTTP header
/*$response->withHeaders([
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Credentials' => 'true',
]);*/
return $response;
}
}

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;
}

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

@ -0,0 +1,10 @@
<?php
namespace app\process;
class Gateway
{
}

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

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

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

@ -0,0 +1,305 @@
<?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;
}
// send SIGUSR1 signal to master process for reload
if (DIRECTORY_SEPARATOR === '/') {
if ($masterPid = $this->getMasterPid()) {
echo $file . " updated and reload\n";
posix_kill($masterPid, SIGUSR1);
} else {
echo "Master process has gone away and can not reload\n";
}
return true;
}
echo $file . " updated and reload\n";
return true;
}
}
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 int
*/
public function getMasterPid(): int
{
if ($this->ppid === 0) {
return 0;
}
if (function_exists('posix_kill') && !posix_kill($this->ppid, 0)) {
echo "Master process has gone away\n";
return $this->ppid = 0;
}
if (PHP_OS_FAMILY !== 'Linux') {
return $this->ppid;
}
$cmdline = "/proc/$this->ppid/cmdline";
if (!is_readable($cmdline) || !($content = file_get_contents($cmdline)) || (!str_contains($content, 'WorkerMan') && !str_contains($content, 'php'))) {
// Process not exist
$this->ppid = 0;
}
return $this->ppid;
}
/**
* @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;
}
$masterPid = $this->getMasterPid();
if ($masterPid <= 0) {
echo "Master process has gone away\n";
return;
}
$childrenFile = "/proc/$masterPid/task/$masterPid/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;
}
}

13
app/process/Schedule.php Normal file
View File

@ -0,0 +1,13 @@
<?php
namespace app\process;
use Workerman\Crontab\Crontab;
//定时任务
class Schedule
{
public function onWorkerStart(): void
{
}
}

46
app/process/Websocket.php Normal file
View File

@ -0,0 +1,46 @@
<?php
/**
* Created by PhpStorm.
* User: zguangjian
* CreateDate: 2025/6/3 下午4:32
* Email: zguangjian@outlook.com
*/
namespace app\process;
use support\Request;
use Workerman\Connection\TcpConnection;
use Workerman\Timer;
class Websocket
{
//存储socket连接得用户
public static array $userList = [];
public function onConnect(TcpConnection $connection): void
{
$connection->send("connection success");
}
//连接成功后回调
public function onWebSocketConnect(TcpConnection $connection, \Workerman\Protocols\Http\Request $request): void
{
self::$userList[$connection->id] = $request->header('token');
}
public function onMessage(TcpConnection $connection, $data): void
{
$connection->send(json_encode(['time'=>time()]));
}
public function onClose(TcpConnection $connection): void
{
//断开连接 释放
unset(self::$userList[$connection->id]);
$connection->close();
}
private function getUserId(TcpConnection $connection)
{
return self::$userList[$connection->id];
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace app\queue\redis;
use Webman\RedisQueue\Consumer;
class MySendMail implements Consumer
{
// 要消费的队列名
public string $queue = 'send-mail';
// 连接名,对应 plugin/webman/redis-queue/redis.php 里的连接`
public string $connection = 'default';
// 消费
public function consume($data): void
{
// 无需反序列化
var_export($data);
}
}

17
app/view/index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0"> -->
<meta name="viewport" content="width=device-width, initial-scale=1,maximum-scale=1, user-scalable=no">
<title><?=htmlspecialchars($logo['seo-title'])?></title>
<meta name="description" property=og:description content="<?=htmlspecialchars($logo['seo-description'])?>">
<meta name="keywords" property=og:description content="<?=htmlspecialchars($logo['seo-keywords'])?>">
<script type="module" crossorigin src="/assets/index-6c6992da.js"></script>
<link rel="stylesheet" href="/assets/index-327c2488.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

14
app/view/index/index.html Normal file
View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>任务模块</title>
<script type="module" crossorigin src="/assets/index-d84fafe9.js"></script>
<link rel="stylesheet" href="/assets/index-2a502b15.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

34
app/view/sse.html Normal file
View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSE Test</title>
</head>
<body>
<h1>SSE Test</h1>
<div id="output"></div>
<script>
if (typeof(EventSource) !== "undefined") {
var source = new EventSource("http://127.0.0.1:8787/index/header");
source.onopen = function(event) {
console.log("Connection to server opened.");
};
source.onmessage = function(event) {
var output = document.getElementById("output");
output.innerHTML += event.data + "<br>";
};
source.onerror = function(event) {
console.log("EventSource failed:", event);
source.close();
};
} else {
document.getElementById("output").innerHTML = "Sorry, your browser does not support server-sent events...";
}
</script>
</body>
</html>