├── windows.bat ├── config ├── plugin │ └── webman │ │ ├── cors │ │ ├── app.php │ │ └── middleware.php │ │ ├── gateway-worker │ │ ├── app.php │ │ └── process.php │ │ ├── log │ │ ├── app.php │ │ └── middleware.php │ │ └── console │ │ └── app.php ├── dependence.php ├── container.php ├── exception.php ├── middleware.php ├── bootstrap.php ├── autoload.php ├── static.php ├── view.php ├── translation.php ├── redis.php ├── cache.php ├── app.php ├── server.php ├── log.php ├── process.php ├── route.php ├── database.php └── session.php ├── public ├── favicon.ico ├── 404.html └── asset │ └── toastr │ └── 2.1.3 │ ├── toastr.min.js │ └── toastr.min.css ├── support ├── helpers.php ├── Request.php ├── Response.php └── bootstrap.php ├── .env.example ├── .gitignore ├── start.php ├── app ├── controller │ ├── IndexController.php │ ├── WindowController.php │ └── QueueController.php ├── view │ ├── index │ │ └── view.html │ ├── queue │ │ ├── status.html │ │ ├── take.html │ │ ├── display.html │ │ └── admin.html │ └── window │ │ └── index.html ├── model │ ├── Test.php │ ├── Window.php │ └── QueueNumber.php ├── common │ └── LogFormatter.php ├── middleware │ ├── QueueRateLimit.php │ └── StaticFile.php ├── bootstrap │ └── MemRepor.php └── functions.php ├── LICENSE ├── composer.json ├── webman ├── sql └── queue.sql ├── README.md ├── windows.php ├── plugin └── webman │ └── gateway │ └── Events.php └── process └── Monitor.php /windows.bat: -------------------------------------------------------------------------------- 1 | CHCP 65001 2 | php windows.php 3 | pause -------------------------------------------------------------------------------- /config/plugin/webman/cors/app.php: -------------------------------------------------------------------------------- 1 | true, 4 | ]; -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glitter1105/queue/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /config/plugin/webman/gateway-worker/app.php: -------------------------------------------------------------------------------- 1 | true, 4 | ]; -------------------------------------------------------------------------------- /support/helpers.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 404 Not Found - webman 4 | 5 | 6 |
7 |

404 Not Found

8 |
9 |
10 |
webman
11 | 12 | 13 | -------------------------------------------------------------------------------- /app/controller/IndexController.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | webman 9 | 10 | 11 | 12 | hello 13 | 14 | 15 | -------------------------------------------------------------------------------- /config/plugin/webman/log/app.php: -------------------------------------------------------------------------------- 1 | true, 4 | 'exception' => [ 5 | // 是否记录异常到日志 6 | 'enable' => true, 7 | // 不会记录到日志的异常类 8 | 'dontReport' => [ 9 | support\exception\BusinessException::class 10 | ] 11 | ], 12 | 'dontReport' => [ 13 | 'app' => [ 14 | ], 15 | 'controller' => [ 16 | ], 17 | 'action' => [ 18 | ], 19 | 'path' => [ 20 | ] 21 | ] 22 | ]; 23 | -------------------------------------------------------------------------------- /config/dependence.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return []; -------------------------------------------------------------------------------- /config/container.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return new Webman\Container; -------------------------------------------------------------------------------- /config/exception.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return [ 16 | '' => support\exception\Handler::class, 17 | ]; -------------------------------------------------------------------------------- /app/model/Test.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return [ 16 | '' => [ 17 | \Webman\Cors\CORS::class 18 | ] 19 | ]; -------------------------------------------------------------------------------- /config/middleware.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | use Webman\Middleware; 16 | 17 | return [ 18 | 19 | // 全局中间件 20 | '' => [ 21 | 22 | ] 23 | ]; -------------------------------------------------------------------------------- /config/plugin/webman/log/middleware.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | use Webman\Log\Middleware; 16 | 17 | return [ 18 | '' => [ 19 | Middleware::class 20 | ] 21 | ]; -------------------------------------------------------------------------------- /config/bootstrap.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return [ 16 | support\bootstrap\Session::class, 17 | support\bootstrap\LaravelDb::class, 18 | //app\bootstrap\MemRepor::class, 19 | ]; 20 | -------------------------------------------------------------------------------- /support/Request.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | namespace support; 16 | 17 | /** 18 | * Class Request 19 | * @package support 20 | */ 21 | class Request extends \Webman\Http\Request 22 | { 23 | 24 | } -------------------------------------------------------------------------------- /support/Response.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | namespace support; 16 | 17 | /** 18 | * Class Response 19 | * @package support 20 | */ 21 | class Response extends \Webman\Http\Response 22 | { 23 | 24 | } -------------------------------------------------------------------------------- /config/autoload.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return [ 16 | 'files' => [ 17 | base_path() . '/app/functions.php', 18 | base_path() . '/support/Request.php', 19 | base_path() . '/support/Response.php', 20 | ] 21 | ]; 22 | -------------------------------------------------------------------------------- /config/static.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | /** 16 | * Static file settings 17 | */ 18 | return [ 19 | 'enable' => true, 20 | 'middleware' => [ // Static file Middleware 21 | app\middleware\StaticFile::class, 22 | ], 23 | ]; -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | use support\view\Raw; 16 | use support\view\Twig; 17 | use support\view\Blade; 18 | use support\view\ThinkPHP; 19 | 20 | return [ 21 | 'handler' => Raw::class 22 | ]; 23 | 24 | //return [ 25 | // 'handler' => Blade::class 26 | //]; -------------------------------------------------------------------------------- /config/translation.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | /** 16 | * Multilingual configuration 17 | */ 18 | return [ 19 | // Default language 20 | 'locale' => 'zh_CN', 21 | // Fallback language 22 | 'fallback_locale' => ['zh_CN', 'en'], 23 | // Folder where language files are stored 24 | 'path' => base_path() . '/resource/translations', 25 | ]; -------------------------------------------------------------------------------- /app/common/LogFormatter.php: -------------------------------------------------------------------------------- 1 | self::INFO, 21 | strtolower(LogLevel::WARNING) => self::WARNING, 22 | strtolower(LogLevel::ERROR) => self::ERROR, 23 | default => '', 24 | }; 25 | return $color . parent::format($record) . self::END; 26 | } 27 | } -------------------------------------------------------------------------------- /config/redis.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return [ 16 | 'default' => [ 17 | 'host' => '127.0.0.1', 18 | 'password' => null, 19 | 'port' => 6379, 20 | 'database' => 0, 21 | ], 22 | 'cache' => [ 23 | 'password' => '', 24 | 'host' => '127.0.0.1', 25 | 'port' => 6379, 26 | 'database' => 1, 27 | 'prefix' => 'webman_cache-', 28 | ] 29 | ]; 30 | -------------------------------------------------------------------------------- /config/plugin/webman/console/app.php: -------------------------------------------------------------------------------- 1 | true, 4 | 5 | 'build_dir' => BASE_PATH . DIRECTORY_SEPARATOR . 'build', 6 | 7 | 'phar_filename' => 'webman.phar', 8 | 9 | 'bin_filename' => 'webman.bin', 10 | 11 | 'signature_algorithm' => Phar::SHA256, //set the signature algorithm for a phar and apply it. The signature algorithm must be one of Phar::MD5, Phar::SHA1, Phar::SHA256, Phar::SHA512, or Phar::OPENSSL. 12 | 13 | 'private_key_file' => '', // The file path for certificate or OpenSSL private key file. 14 | 15 | 'exclude_pattern' => '#^(?!.*(composer.json|/.github/|/.idea/|/.git/|/.setting/|/runtime/|/vendor-bin/|/build/|/vendor/webman/admin/))(.*)$#', 16 | 17 | 'exclude_files' => [ 18 | '.env', 'LICENSE', 'composer.json', 'composer.lock', 'start.php', 'webman.phar', 'webman.bin' 19 | ], 20 | 21 | 'custom_ini' => 'memory_limit = 256M', 22 | ]; 23 | -------------------------------------------------------------------------------- /config/cache.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return [ 16 | 'default' => 'redis', 17 | 'stores' => [ 18 | 'file' => [ 19 | 'driver' => 'file', 20 | 'path' => runtime_path('cache') 21 | ], 22 | 'redis' => [ 23 | 'driver' => 'redis', 24 | 'connection' => 'cache' 25 | ], 26 | 'array' => [ 27 | 'driver' => 'array' 28 | ] 29 | ] 30 | ]; 31 | -------------------------------------------------------------------------------- /app/middleware/QueueRateLimit.php: -------------------------------------------------------------------------------- 1 | path() === '/queue/take' && $request->method() === 'POST') { 14 | $mobile = $request->post('mobile'); 15 | $key = "queue_rate_limit:{$mobile}"; 16 | 17 | // 检查是否在限制时间内 18 | if (Cache::get($key)) { 19 | return json([ 20 | 'code' => 1, 21 | 'msg' => '取号太频繁,请稍后再试' 22 | ]); 23 | } 24 | 25 | // 设置限制时间(5分钟) 26 | Cache::set($key, 1, 300); 27 | } 28 | 29 | return $handler($request); 30 | } 31 | } -------------------------------------------------------------------------------- /app/bootstrap/MemRepor.php: -------------------------------------------------------------------------------- 1 | name == 'monitor') { 33 | return; 34 | } 35 | 36 | // 每隔10秒执行一次 37 | Timer::add(10, function () { 38 | // 为了方便演示,这里使用输出代替上报过程 39 | echo memory_get_usage() . "\n"; 40 | }); 41 | 42 | 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /config/app.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | use support\Request; 16 | 17 | return [ 18 | 'name' => 'webman', 19 | 'app_url' => 'http://localhost:8787', 20 | 'debug' => true, 21 | 'error_reporting' => E_ALL, 22 | 'default_timezone' => 'Asia/Shanghai', 23 | 'request_class' => Request::class, 24 | 'public_path' => base_path() . DIRECTORY_SEPARATOR . 'public', 25 | 'runtime_path' => base_path(false) . DIRECTORY_SEPARATOR . 'runtime', 26 | 'controller_suffix' => 'Controller', 27 | 'controller_reuse' => false, 28 | ]; 29 | -------------------------------------------------------------------------------- /config/server.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return [ 16 | 'listen' => 'http://0.0.0.0:8787', 17 | 'transport' => 'tcp', 18 | 'context' => [], 19 | 'name' => 'webman', 20 | 'count' => cpu_count() * 4, 21 | 'user' => '', 22 | 'group' => '', 23 | 'reusePort' => false, 24 | 'event_loop' => '', 25 | 'stop_timeout' => 2, 26 | 'pid_file' => runtime_path() . '/webman.pid', 27 | 'status_file' => runtime_path() . '/webman.status', 28 | 'stdout_file' => runtime_path() . '/logs/stdout.log', 29 | 'log_file' => runtime_path() . '/logs/workerman.log', 30 | 'max_package_size' => 10 * 1024 * 1024 31 | ]; 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 walkor and contributors (see https://github.com/walkor/webman/contributors) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/middleware/StaticFile.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | namespace app\middleware; 16 | 17 | use Webman\MiddlewareInterface; 18 | use Webman\Http\Response; 19 | use Webman\Http\Request; 20 | 21 | /** 22 | * Class StaticFile 23 | * @package app\middleware 24 | */ 25 | class StaticFile implements MiddlewareInterface 26 | { 27 | public function process(Request $request, callable $handler): Response 28 | { 29 | // 禁止访问.开头的隐藏文件 30 | if (str_contains($request->path(), '/.')) { 31 | return response('

403 forbidden

', 403); 32 | } 33 | /** @var Response $response */ 34 | $response = $handler($request); 35 | // 增加跨域http头 36 | /*$response->withHeaders([ 37 | 'Access-Control-Allow-Origin' => '*', 38 | 'Access-Control-Allow-Credentials' => 'true', 39 | ]);*/ 40 | return $response; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/log.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | use app\common\LogFormatter; 16 | use Monolog\Handler\StreamHandler; 17 | 18 | return [ 19 | 'default' => [ 20 | 'handlers' => [ 21 | [ 22 | 'class' => Monolog\Handler\RotatingFileHandler::class, 23 | 'constructor' => [ 24 | runtime_path() . DIRECTORY_SEPARATOR . 'logs' . DIRECTORY_SEPARATOR . 'webman.log', 25 | 7, //$maxFiles 26 | Monolog\Logger::DEBUG, 27 | ], 28 | 'formatter' => [ 29 | 'class' => Monolog\Formatter\LineFormatter::class, 30 | 'constructor' => [null, 'Y-m-d H:i:s', true], 31 | ], 32 | ], 33 | [ 34 | 'class' => StreamHandler::class, 35 | 'constructor' => [ 36 | STDOUT, 37 | ], 38 | 'formatter' => [ 39 | 'class' => LogFormatter::class, 40 | 'constructor' => [null, 'Y-m-d H:i:s', true, true], 41 | ], 42 | ] 43 | ], 44 | ], 45 | ]; 46 | -------------------------------------------------------------------------------- /config/process.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | global $argv; 16 | 17 | return [ 18 | // File update detection and automatic reload 19 | 'monitor' => [ 20 | 'handler' => process\Monitor::class, 21 | 'reloadable' => false, 22 | 'constructor' => [ 23 | // Monitor these directories 24 | 'monitorDir' => array_merge([ 25 | app_path(), 26 | config_path(), 27 | base_path() . '/process', 28 | base_path() . '/support', 29 | base_path() . '/resource', 30 | base_path() . '/.env', 31 | base_path() . '/plugin/webman/gateway', 32 | ], glob(base_path() . '/plugin/*/app'), glob(base_path() . '/plugin/*/config'), glob(base_path() . '/plugin/*/api')), 33 | // Files with these suffixes will be monitored 34 | 'monitorExtensions' => [ 35 | 'php', 'html', 'htm', 'env' 36 | ], 37 | 'options' => [ 38 | 'enable_file_monitor' => !in_array('-d', $argv) && DIRECTORY_SEPARATOR === '/', 39 | 'enable_memory_monitor' => DIRECTORY_SEPARATOR === '/', 40 | ] 41 | ] 42 | ] 43 | ]; 44 | -------------------------------------------------------------------------------- /config/plugin/webman/gateway-worker/process.php: -------------------------------------------------------------------------------- 1 | [ 11 | 'handler' => Gateway::class, 12 | 'listen' => 'websocket://0.0.0.0:7272', 13 | 'count' => 2, 14 | 'reloadable' => false, 15 | 'constructor' => [ 16 | 'config' => [ 17 | 'lanIp' => '127.0.0.1', 18 | 'startPort' => 2300, 19 | 'pingInterval' => 25, 20 | 'pingNotResponseLimit' => 0, //其中pingNotResponseLimit = 0代表服务端允许客户端不发送心跳,服务端不会因为客户端长时间没发送数据而断开连接。如果pingNotResponseLimit = 1,则代表客户端必须定时发送数据给服务端,否则pingNotResponseLimit*pingInterval=55秒内没有任何数据发来则关闭对应连接,并触发onClose。 21 | 'pingData' => '{"type":"ping"}', 22 | 'registerAddress' => '127.0.0.1:1236', 23 | 'onConnect' => function () { 24 | }, 25 | ] 26 | ] 27 | ], 28 | 'worker' => [ 29 | 'handler' => BusinessWorker::class, 30 | 'count' => cpu_count() * 2, 31 | 'constructor' => [ 32 | 'config' => [ 33 | 'eventHandler' => plugin\webman\gateway\Events::class, 34 | 'name' => 'ChatBusinessWorker', 35 | 'registerAddress' => '127.0.0.1:1236', 36 | ] 37 | ] 38 | ], 39 | 'register' => [ 40 | 'handler' => Register::class, 41 | 'listen' => 'text://127.0.0.1:1236', 42 | 'count' => 1, // Must be 1 43 | 'reloadable' => false, 44 | 'constructor' => [] 45 | ], 46 | ]; 47 | -------------------------------------------------------------------------------- /config/route.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | use Webman\Route; 16 | 17 | 18 | 19 | // 窗口管理路由 20 | Route::get('/window', [app\controller\WindowController::class, 'index']); // 窗口列表 21 | Route::post('/window/save', [app\controller\WindowController::class, 'save']); // 保存 22 | Route::post('/window/delete', [app\controller\WindowController::class, 'delete']); 23 | 24 | 25 | Route::get('/', [app\controller\QueueController::class, 'admin']); // 管理员页面 26 | Route::get('/queue/display', [app\controller\QueueController::class, 'display']); // 叫号屏幕 27 | Route::get('/queue/take', [app\controller\QueueController::class, 'takePage']); // 取号页面 28 | Route::post('/queue/take', [app\controller\QueueController::class, 'take']); // 取号 29 | Route::post('/queue/call', [app\controller\QueueController::class, 'call']); // 叫号 30 | Route::post('/queue/complete', [app\controller\QueueController::class, 'complete']); // 完成 31 | Route::post('/queue/pass', [app\controller\QueueController::class, 'pass']); // 跳过 32 | Route::post('/queue/cancel', [app\controller\QueueController::class, 'cancel']); // 取消 33 | Route::get('/queue/qrcode', [app\controller\QueueController::class, 'qrcode']); // 二维码 34 | Route::get('/queue/status/{number}', [app\controller\QueueController::class, 'status']); // 状态 35 | Route::get('/queue/current', [app\controller\QueueController::class, 'current']); // 当前叫号 36 | 37 | 38 | Route::disableDefaultRoute(); -------------------------------------------------------------------------------- /app/model/Window.php: -------------------------------------------------------------------------------- 1 | status) { 27 | self::STATUS_ENABLED => '启用', 28 | self::STATUS_DISABLED => '禁用', 29 | default => '未知状态', 30 | }; 31 | } 32 | 33 | /** 34 | * 获取当前窗口的叫号记录 35 | */ 36 | public function queueNumbers() 37 | { 38 | return $this->hasMany(QueueNumber::class, 'window_id', 'id'); 39 | } 40 | 41 | /** 42 | * 获取今日该窗口处理的号码数量 43 | */ 44 | public function getTodayProcessedCount(): int 45 | { 46 | return $this->queueNumbers() 47 | ->whereDate('created_at', date('Y-m-d')) 48 | ->whereIn('status', [ 49 | QueueNumber::STATUS_COMPLETED, 50 | QueueNumber::STATUS_PASSED 51 | ]) 52 | ->count(); 53 | } 54 | 55 | /** 56 | * 获取该窗口当前等待人数 57 | */ 58 | public function getWaitingCount(): int 59 | { 60 | return $this->queueNumbers() 61 | ->whereDate('created_at', date('Y-m-d')) 62 | ->where('status', QueueNumber::STATUS_WAITING) 63 | ->count(); 64 | } 65 | 66 | /** 67 | * 获取该窗口当前正在处理的号码 68 | */ 69 | public function getCurrentNumber() 70 | { 71 | return $this->queueNumbers() 72 | ->whereDate('created_at', date('Y-m-d')) 73 | ->where('status', QueueNumber::STATUS_CALLING) 74 | ->first(); 75 | } 76 | } -------------------------------------------------------------------------------- /config/database.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return [ 16 | // 默认数据库 17 | 'default' => 'mysql', 18 | // 各种数据库配置 19 | 'connections' => [ 20 | 21 | 'mysql' => [ 22 | 'driver' => 'mysql', 23 | 'host' => getenv('DB_HOST'), 24 | 'port' => getenv('DB_PORT'), 25 | 'database' => getenv('DB_NAME'), 26 | 'username' => getenv('DB_USER'), 27 | 'password' => getenv('DB_PASSWORD'), 28 | 'unix_socket' => '', 29 | 'charset' => 'utf8', 30 | 'collation' => 'utf8_unicode_ci', 31 | 'prefix' => '', 32 | 'strict' => true, 33 | 'engine' => null, 34 | 'options' => [ 35 | \PDO::ATTR_TIMEOUT => 3 36 | ] 37 | ], 38 | 39 | 'sqlite' => [ 40 | 'driver' => 'sqlite', 41 | 'database' => '', 42 | 'prefix' => '', 43 | ], 44 | 45 | 'pgsql' => [ 46 | 'driver' => 'pgsql', 47 | 'host' => '127.0.0.1', 48 | 'port' => 5432, 49 | 'database' => 'webman', 50 | 'username' => 'webman', 51 | 'password' => '', 52 | 'charset' => 'utf8', 53 | 'prefix' => '', 54 | 'schema' => 'public', 55 | 'sslmode' => 'prefer', 56 | ], 57 | 58 | 'sqlsrv' => [ 59 | 'driver' => 'sqlsrv', 60 | 'host' => 'localhost', 61 | 'port' => 1433, 62 | 'database' => 'webman', 63 | 'username' => 'webman', 64 | 'password' => '', 65 | 'charset' => 'utf8', 66 | 'prefix' => '', 67 | ], 68 | ], 69 | ]; -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workerman/webman", 3 | "type": "project", 4 | "keywords": [ 5 | "high performance", 6 | "http service" 7 | ], 8 | "homepage": "https://www.workerman.net", 9 | "license": "MIT", 10 | "description": "High performance HTTP Service Framework.", 11 | "authors": [ 12 | { 13 | "name": "walkor", 14 | "email": "walkor@workerman.net", 15 | "homepage": "https://www.workerman.net", 16 | "role": "Developer" 17 | } 18 | ], 19 | "support": { 20 | "email": "walkor@workerman.net", 21 | "issues": "https://github.com/walkor/webman/issues", 22 | "forum": "https://wenda.workerman.net/", 23 | "wiki": "https://workerman.net/doc/webman", 24 | "source": "https://github.com/walkor/webman" 25 | }, 26 | "require": { 27 | "php": ">=7.2", 28 | "workerman/webman-framework": "^1.6", 29 | "monolog/monolog": "^2.0", 30 | "psr/container": "1.1.1", 31 | "illuminate/database": "^10.0", 32 | "illuminate/events": "^10.0", 33 | "illuminate/pagination": "^10.0", 34 | "illuminate/redis": "^10.0", 35 | "symfony/var-dumper": "^7.1", 36 | "laravel/serializable-closure": "^1.3", 37 | "psr/simple-cache": "^3.0", 38 | "vlucas/phpdotenv": "^5.6", 39 | "webman/console": "^1.3", 40 | "webman/cors": "^1.0", 41 | "webman/log": "^1.1", 42 | "phpoffice/phpspreadsheet": "^3.3", 43 | "phpmailer/phpmailer": "^6.9", 44 | "ext-fileinfo": "*", 45 | "ext-pdo": "*", 46 | "ext-curl": "*", 47 | "webman/blade": "^1.5", 48 | "webman/gateway-worker": "^1.0", 49 | "endroid/qr-code": "^6.0" 50 | }, 51 | "suggest": { 52 | "ext-event": "For better performance. " 53 | }, 54 | "autoload": { 55 | "psr-4": { 56 | "": "./", 57 | "app\\": "./app", 58 | "App\\": "./app", 59 | "app\\View\\Components\\": "./app/view/components" 60 | }, 61 | "files": [ 62 | "./support/helpers.php" 63 | ] 64 | }, 65 | "scripts": { 66 | "post-package-install": [ 67 | "support\\Plugin::install" 68 | ], 69 | "post-package-update": [ 70 | "support\\Plugin::install" 71 | ], 72 | "pre-package-uninstall": [ 73 | "support\\Plugin::uninstall" 74 | ] 75 | }, 76 | "minimum-stability": "dev", 77 | "prefer-stable": true 78 | } 79 | -------------------------------------------------------------------------------- /config/session.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | use Webman\Session\FileSessionHandler; 16 | use Webman\Session\RedisSessionHandler; 17 | use Webman\Session\RedisClusterSessionHandler; 18 | 19 | return [ 20 | 21 | // handler为FileSessionHandler::class时值为file, 22 | // handler为RedisSessionHandler::class时值为redis 23 | // handler为RedisClusterSessionHandler::class时值为redis_cluster 既redis集群 24 | 'type' => 'file', // or redis or redis_cluster 25 | 26 | // FileSessionHandler::class 或者 RedisSessionHandler::class 或者 RedisClusterSessionHandler::class 27 | 28 | 'handler' => FileSessionHandler::class, 29 | 30 | 'config' => [ 31 | 'file' => [ 32 | 'save_path' => runtime_path() . '/sessions', 33 | ], 34 | 'redis' => [ 35 | 'host' => '127.0.0.1', 36 | 'port' => 6379, 37 | 'auth' => '', 38 | 'timeout' => 2, 39 | 'database' => '', 40 | 'prefix' => 'redis_session_', 41 | ], 42 | 'redis_cluster' => [ 43 | 'host' => ['127.0.0.1:7000', '127.0.0.1:7001', '127.0.0.1:7001'], 44 | 'timeout' => 2, 45 | 'auth' => '', 46 | 'prefix' => 'redis_session_', 47 | ] 48 | ], 49 | 50 | 'session_name' => 'PHPSID', //// 存储session_id的cookie名 51 | 52 | 'auto_update_timestamp' => false, // 是否自动刷新session,默认关闭 53 | 54 | 'lifetime' => 24*60*60, // session过期时间 55 | 56 | 'cookie_lifetime' => 365*24*60*60, // 存储session_id的cookie过期时间 57 | 58 | 'cookie_path' => '/', // 存储session_id的cookie路径 59 | 60 | 'domain' => '', // 存储session_id的cookie域名 61 | 62 | 'http_only' => true, // 是否开启httpOnly,默认开启 63 | 64 | 'secure' => false, // 仅在https下开启session,默认关闭 65 | 66 | 'same_site' => '', // 用于防止CSRF攻击和用户追踪,可选值strict/lax/none 67 | 68 | 'gc_probability' => [1, 1000], // 回收session的几率 69 | 70 | ]; 71 | -------------------------------------------------------------------------------- /webman: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | setName('webman cli'); 33 | $cli->installInternalCommands(); 34 | if (is_dir($command_path = Util::guessPath(app_path(), '/command', true))) { 35 | $cli->installCommands($command_path); 36 | } 37 | 38 | foreach (config('plugin', []) as $firm => $projects) { 39 | if (isset($projects['app'])) { 40 | foreach (['', '/app'] as $app) { 41 | if ($command_str = Util::guessPath(base_path() . "/plugin/$firm{$app}", 'command')) { 42 | $command_path = base_path() . "/plugin/$firm{$app}/$command_str"; 43 | $cli->installCommands($command_path, "plugin\\$firm" . str_replace('/', '\\', $app) . "\\$command_str"); 44 | } 45 | } 46 | } 47 | foreach ($projects as $name => $project) { 48 | if (!is_array($project)) { 49 | continue; 50 | } 51 | foreach ($project['command'] ?? [] as $class_name) { 52 | $reflection = new \ReflectionClass($class_name); 53 | if ($reflection->isAbstract()) { 54 | continue; 55 | } 56 | $properties = $reflection->getStaticProperties(); 57 | $name = $properties['defaultName']; 58 | if (!$name) { 59 | throw new RuntimeException("Command {$class_name} has no defaultName"); 60 | } 61 | $description = $properties['defaultDescription'] ?? ''; 62 | $command = Container::get($class_name); 63 | $command->setName($name)->setDescription($description); 64 | $cli->add($command); 65 | } 66 | } 67 | } 68 | 69 | $cli->run(); 70 | -------------------------------------------------------------------------------- /app/functions.php: -------------------------------------------------------------------------------- 1 | where('window_id', $window) 19 | ->whereIn('status', [QueueNumber::STATUS_WAITING, QueueNumber::STATUS_CALLING]) 20 | ->where('call_count', '>', 0) 21 | ->whereDate('created_at', date('Y-m-d')) 22 | ->get(); 23 | 24 | if ($overCalledNumbers->isEmpty()) { 25 | return; 26 | } 27 | 28 | foreach ($overCalledNumbers as $number) { 29 | $number->status = QueueNumber::STATUS_PASSED; 30 | $number->save(); 31 | 32 | // 推送消息通知显示屏更新 33 | try { 34 | // 使用Gateway发送状态更新通知 35 | $message = [ 36 | 'type' => 'status_change', 37 | 'data' => [ 38 | 'number' => $number->number, 39 | 'name' => hideNameCharacters($number->name), 40 | 'status' => QueueNumber::STATUS_PASSED 41 | ] 42 | ]; 43 | 44 | Gateway::sendToUid('display', json_encode($message)); 45 | 46 | // 使用Gateway发送状态更新通知 47 | $message = [ 48 | 'type' => 'status_change', 49 | 'data' => [ 50 | 'number' => $number->number, 51 | 'name' => $number->name, 52 | 'mobile' => $number->mobile, 53 | 'call_count' => $number->call_count, 54 | 'window' => $number->window_id, 55 | 'window_name' => optional($number->window)->name, 56 | 'status' => QueueNumber::STATUS_PASSED 57 | ] 58 | ]; 59 | Gateway::sendToUid('window', json_encode($message)); 60 | } catch (\Webman\Push\PushException $e) { 61 | // 记录推送异常 62 | \support\Log::error($e->getMessage()); 63 | } 64 | } 65 | } 66 | 67 | 68 | function hideNameCharacters(string $name): string 69 | { 70 | $length = mb_strlen($name, 'UTF-8'); // 获取字符串长度,使用 mb_strlen 处理 UTF-8 字符 71 | 72 | if ($length <= 1) { 73 | return $name; // 名字只有一个字或者为空,不处理 74 | } 75 | 76 | $firstChar = mb_substr($name, 0, 1, 'UTF-8'); // 获取第一个字符 77 | $lastChar = mb_substr($name, $length - 1, 1, 'UTF-8'); // 获取最后一个字符 78 | 79 | if ($length == 2) { 80 | return $firstChar . "*"; // 名字只有两个字,隐藏第二个字 81 | } 82 | 83 | $hiddenChars = str_repeat('*', $length - 2); // 生成中间的 * 字符串 84 | return $firstChar . $hiddenChars . $lastChar; // 拼接最终的字符串 85 | } 86 | 87 | -------------------------------------------------------------------------------- /sql/queue.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : localhost 5 | Source Server Type : MySQL 6 | Source Server Version : 50726 (5.7.26) 7 | Source Host : localhost:3306 8 | Source Schema : webman 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 50726 (5.7.26) 12 | File Encoding : 65001 13 | 14 | Date: 27/12/2024 10:45:08 15 | */ 16 | 17 | SET NAMES utf8mb4; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for queue_numbers 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `queue_numbers`; 24 | CREATE TABLE `queue_numbers` ( 25 | `id` int(11) NOT NULL AUTO_INCREMENT, 26 | `number` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '号码', 27 | `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '姓名', 28 | `mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号', 29 | `call_count` int(11) NULL DEFAULT 0 COMMENT '叫号次数', 30 | `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态:0待叫号,1已取消,2已过号,3已完成', 31 | `window_id` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '窗口号', 32 | `qrcode_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '二维码链接', 33 | `created_at` datetime NULL DEFAULT NULL, 34 | `updated_at` datetime NULL DEFAULT NULL, 35 | PRIMARY KEY (`id`) USING BTREE, 36 | INDEX `idx_number`(`number`) USING BTREE 37 | ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '叫号队列表' ROW_FORMAT = Dynamic; 38 | 39 | -- ---------------------------- 40 | -- Records of queue_numbers 41 | -- ---------------------------- 42 | 43 | -- ---------------------------- 44 | -- Table structure for windows 45 | -- ---------------------------- 46 | DROP TABLE IF EXISTS `windows`; 47 | CREATE TABLE `windows` ( 48 | `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, 49 | `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '窗口名称', 50 | `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '窗口描述', 51 | `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态 1:启用 0:禁用', 52 | `created_at` timestamp NULL DEFAULT NULL, 53 | `updated_at` timestamp NULL DEFAULT NULL, 54 | PRIMARY KEY (`id`) USING BTREE 55 | ) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '窗口表' ROW_FORMAT = Dynamic; 56 | 57 | -- ---------------------------- 58 | -- Records of windows 59 | -- ---------------------------- 60 | INSERT INTO `windows` VALUES (1, '1号窗口', '普通窗口', 1, '2024-12-23 14:50:45', '2024-12-23 16:15:42'); 61 | INSERT INTO `windows` VALUES (2, '2号窗口', '普通窗口', 1, '2024-12-23 14:50:52', '2024-12-23 16:15:47'); 62 | INSERT INTO `windows` VALUES (3, '3号窗口', '普通窗口', 1, '2024-12-23 14:51:09', '2024-12-23 16:15:51'); 63 | INSERT INTO `windows` VALUES (4, '4号窗口', '普通窗口', 1, '2024-12-23 14:51:19', '2024-12-23 14:51:19'); 64 | INSERT INTO `windows` VALUES (5, '5号窗口', 'VIP窗口', 1, '2024-12-23 15:10:51', '2024-12-23 15:10:51'); 65 | INSERT INTO `windows` VALUES (6, '6号窗口', 'VIP窗口', 1, '2024-12-23 15:12:34', '2024-12-23 17:17:06'); 66 | 67 | SET FOREIGN_KEY_CHECKS = 1; 68 | -------------------------------------------------------------------------------- /app/controller/WindowController.php: -------------------------------------------------------------------------------- 1 | get(); 18 | return view('window/index', ['windows' => $windows]); 19 | } 20 | 21 | /** 22 | * 保存窗口 23 | */ 24 | public function save(Request $request): Response 25 | { 26 | $id = $request->post('id'); 27 | 28 | $data = [ 29 | 'name' => $request->post('name'), 30 | 'description' => $request->post('description'), 31 | 'status' => $request->post('status', Window::STATUS_ENABLED), 32 | ]; 33 | 34 | // 验证数据 35 | if (!$data['name']) { 36 | return json(['code' => 1, 'msg' => '请填写窗口名称']); 37 | } 38 | 39 | if (strlen($data['name']) > 50) { 40 | return json(['code' => 1, 'msg' => '窗口名称不能超过50个字符']); 41 | } 42 | 43 | if (strlen($data['description']) > 255) { 44 | return json(['code' => 1, 'msg' => '描述不能超过255个字符']); 45 | } 46 | 47 | // 检查名称是否重复 48 | $exists = Window::where('name', $data['name']) 49 | ->when($id, function ($query) use ($id) { 50 | $query->where('id', '!=', $id); 51 | }) 52 | ->exists(); 53 | 54 | if ($exists) { 55 | return json(['code' => 1, 'msg' => '窗口名称已存在']); 56 | } 57 | 58 | try { 59 | if ($id) { 60 | Window::where('id', $id)->update($data); 61 | } else { 62 | Window::create($data); 63 | } 64 | 65 | return json(['code' => 0, 'msg' => '保存成功']); 66 | } catch (\Exception $e) { 67 | return json(['code' => 1, 'msg' => '保存失败:' . $e->getMessage()]); 68 | } 69 | } 70 | 71 | /** 72 | * 删除窗口 73 | */ 74 | public function delete(Request $request): Response 75 | { 76 | $id = $request->post('id'); 77 | 78 | if (!$id) { 79 | return json(['code' => 1, 'msg' => '参数错误']); 80 | } 81 | 82 | // 检查窗口是否有正在处理的号码 83 | $window = Window::find($id); 84 | 85 | if (!$window) { 86 | return json(['code' => 1, 'msg' => '窗口不存在']); 87 | } 88 | 89 | $hasProcessing = $window 90 | ->queueNumbers() 91 | ->whereDate('created_at', date('Y-m-d')) 92 | ->whereIn('status', [ 93 | QueueNumber::STATUS_CALLING, 94 | QueueNumber::STATUS_PASSED 95 | ]) 96 | ->exists(); 97 | 98 | if ($hasProcessing) { 99 | return json(['code' => 1, 'msg' => '该窗口有正在处理的号码,无法删除']); 100 | } 101 | 102 | try { 103 | $window->delete(); 104 | return json(['code' => 0, 'msg' => '删除成功']); 105 | } catch (\Exception $e) { 106 | return json(['code' => 1, 'msg' => '删除失败:' . $e->getMessage()]); 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Queue Management System (排队叫号系统) 2 | 3 | 4 | A simple queue management and number calling system built with the high-performance [Webman](https://www.workerman.net/webman) framework and [GatewayWorker](https://www.workerman.net/gatewaydoc). 5 | 6 | 一个基于高性能PHP框架 [Webman](https://www.workerman.net/webman) 和 [GatewayWorker](https://www.workerman.net/gatewaydoc) 构建的简单排队叫号系统。 7 | 8 | ## Features (功能特性) 9 | 10 | * **Real-time Updates (实时更新):** Utilizes WebSocket (via GatewayWorker) for real-time updates on the queue display and administration screens. (使用 WebSocket (通过 GatewayWorker) 实现叫号、排队状态的实时更新。) 11 | * **Multiple Windows (多窗口支持):** Supports multiple service windows for calling numbers. (支持多个服务窗口同时叫号。) 12 | * **Simple Interface (简洁的用户界面):** 13 | * Customer-facing ticket dispensing page. (面向顾客的取号页面。) 14 | * Administration page for staff to manage the queue. (供工作人员使用的后台管理页面。) 15 | * Large screen display for public viewing. (用于大屏幕展示的公共显示页面。) 16 | * **Easy to Deploy (易于部署):** Built on Webman, it requires minimal server configuration and has no dependency on Nginx/Apache. (基于 Webman 构建,无Nginx/Apache依赖,部署简单。) 17 | 18 | ## Technology Stack (技术栈) 19 | 20 | * **Backend:** PHP, [Webman](https://www.workerman.net/webman), [GatewayWorker](https://www.workerman.net/gatewaydoc) 21 | * **Frontend:** HTML, Bootstrap, jQuery, Toastr.js 22 | * **Database:** MySQL (or other compatible SQL database), Redis 23 | 24 | ## Requirements (环境要求) 25 | 26 | * PHP >= 7.4 27 | * Composer 28 | * Redis 29 | * MySQL 30 | 31 | ## Installation (安装步骤) 32 | 33 | 1. **Clone the repository (克隆项目):** 34 | ```bash 35 | git clone https://github.com/zx2020-07/queue.git 36 | cd queue 37 | ``` 38 | 39 | 2. **Install dependencies (安装依赖):** 40 | ```bash 41 | composer install 42 | ``` 43 | 44 | 3. **Configure environment (配置环境):** 45 | Copy the example environment file and update it with your database and Redis connection details. 46 | (复制环境配置文件 `.env.example` 为 `.env`,并根据需要修改数据库和Redis连接信息。) 47 | ```bash 48 | cp .env.example .env 49 | ``` 50 | 51 | 4. **Import database schema (导入数据库):** 52 | Import the `sql/queue.sql` file into your MySQL database. 53 | (将 `sql/queue.sql` 文件导入到你的 MySQL 数据库中。) 54 | 55 | 5. **Start the application (启动应用):** 56 | ```bash 57 | php start.php start 58 | ``` 59 | To run as a daemon (守护进程模式运行): 60 | ```bash 61 | php start.php start -d 62 | ``` 63 | 64 | The application will be available at `http://localhost:8787`. (应用将运行在 `http://localhost:8787`) 65 | 66 | ## Usage (使用说明) 67 | 68 | * **Take a ticket (取号):** `http://localhost:8787/queue/take` 69 | * **Admin panel (管理后台):** `http://localhost:8787/queue/admin` 70 | * **Display screen (显示大屏):** `http://localhost:8787/queue/display` 71 | * **Queue status (排队状态):** `http://localhost:8787/queue/status` 72 | * **Window management (窗口管理):** `http://localhost:8787/window` 73 | 74 | ## Screenshots (应用截图) 75 | 76 |
77 | Click to expand (点击展开) 78 | 79 | **管理页面 (Admin Page):** 80 | ![管理页面](https://github.com/user-attachments/assets/c7e49976-ec04-4dfa-85af-f8ca4a598c91) 81 | 82 | **窗口管理 (Window Management):** 83 | ![窗口管理](https://github.com/user-attachments/assets/0aa1ea4d-cb62-4715-bdd5-9f4d250da236) 84 | 85 | **排队取号 (Take a Ticket):** 86 | ![排队取号](https://github.com/user-attachments/assets/f2a33d9a-e5fd-4a90-8e63-3e8ceb5f8264) 87 | ![排队取号2](https://github.com/user-attachments/assets/4cfa1008-965f-4454-a8fa-b1d6548a13b3) 88 | 89 | **排队状态 (Queue Status):** 90 | ![排队状态](https://github.com/user-attachments/assets/64c3b894-ac67-41a9-8368-b0bc5383240e) 91 | 92 | **显示大屏 (Display Screen):** 93 | ![显示大屏](https://github.com/user-attachments/assets/77584d69-cd1d-428b-8bbb-f982f2503ddf) 94 | 95 |
96 | 97 | ## License (许可证) 98 | 99 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. -------------------------------------------------------------------------------- /windows.php: -------------------------------------------------------------------------------- 1 | load(); 18 | } else { 19 | Dotenv::createMutable(base_path())->load(); 20 | } 21 | } 22 | 23 | App::loadAllConfig(['route']); 24 | 25 | $errorReporting = config('app.error_reporting'); 26 | if (isset($errorReporting)) { 27 | error_reporting($errorReporting); 28 | } 29 | 30 | $runtimeProcessPath = runtime_path() . DIRECTORY_SEPARATOR . '/windows'; 31 | $paths = [ 32 | $runtimeProcessPath, 33 | runtime_path('logs'), 34 | runtime_path('views') 35 | ]; 36 | foreach ($paths as $path) { 37 | if (!is_dir($path)) { 38 | mkdir($path, 0777, true); 39 | } 40 | } 41 | 42 | $processFiles = []; 43 | if (config('server.listen')) { 44 | $processFiles[] = __DIR__ . DIRECTORY_SEPARATOR . 'start.php'; 45 | } 46 | foreach (config('process', []) as $processName => $config) { 47 | $processFiles[] = write_process_file($runtimeProcessPath, $processName, ''); 48 | } 49 | 50 | foreach (config('plugin', []) as $firm => $projects) { 51 | foreach ($projects as $name => $project) { 52 | if (!is_array($project)) { 53 | continue; 54 | } 55 | foreach ($project['process'] ?? [] as $processName => $config) { 56 | $processFiles[] = write_process_file($runtimeProcessPath, $processName, "$firm.$name"); 57 | } 58 | } 59 | foreach ($projects['process'] ?? [] as $processName => $config) { 60 | $processFiles[] = write_process_file($runtimeProcessPath, $processName, $firm); 61 | } 62 | } 63 | 64 | function write_process_file($runtimeProcessPath, $processName, $firm): string 65 | { 66 | $processParam = $firm ? "plugin.$firm.$processName" : $processName; 67 | $configParam = $firm ? "config('plugin.$firm.process')['$processName']" : "config('process')['$processName']"; 68 | $fileContent = << true]); 119 | if (!$resource) { 120 | exit("Can not execute $cmd\r\n"); 121 | } 122 | return $resource; 123 | } 124 | 125 | $resource = popen_processes($processFiles); 126 | echo "\r\n"; 127 | while (1) { 128 | sleep(1); 129 | if (!empty($monitor) && $monitor->checkAllFilesChange()) { 130 | $status = proc_get_status($resource); 131 | $pid = $status['pid']; 132 | shell_exec("taskkill /F /T /PID $pid"); 133 | proc_close($resource); 134 | $resource = popen_processes($processFiles); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /app/model/QueueNumber.php: -------------------------------------------------------------------------------- 1 | status) { 35 | self::STATUS_WAITING => '待叫号', 36 | self::STATUS_CANCELLED => '已取消', 37 | self::STATUS_PASSED => '已过号', 38 | self::STATUS_COMPLETED => '已完成', 39 | self::STATUS_CALLING => '叫号中', 40 | default => '未知状态', 41 | }; 42 | } 43 | 44 | /** 45 | * 生成新号码 46 | * @param string $name 47 | * @param string $mobile 48 | * @return array 49 | */ 50 | public static function generateNumber(string $name, string $mobile): array 51 | { 52 | // 检查是否存在未完成的号码 53 | $existingNumber = self::where('mobile', $mobile) 54 | ->whereDate('created_at', date('Y-m-d')) 55 | ->whereIn('status', [self::STATUS_WAITING]) 56 | ->first(); 57 | 58 | if ($existingNumber) { 59 | // 将已存在的号码标记为已取消 60 | $existingNumber->status = self::STATUS_CANCELLED; 61 | $existingNumber->save(); 62 | } 63 | 64 | // 生成新号码 65 | $date = date('Ymd'); 66 | $lastNumber = self::whereDate('created_at', $date) 67 | ->orderBy('number', 'desc') 68 | ->value('number'); 69 | 70 | if (!$lastNumber) { 71 | $newNumber = '001'; 72 | } else { 73 | $sequence = (int)$lastNumber + 1; 74 | $newNumber = str_pad($sequence, 3, '0', STR_PAD_LEFT); 75 | } 76 | 77 | // 生成二维码链接 78 | $qrcodeUrl = '/queue/status/' . $newNumber; 79 | 80 | 81 | //获取正在等待叫号的人数 82 | $waitingCount = self::where('status', self::STATUS_WAITING) 83 | ->whereDate('created_at', date('Y-m-d')) 84 | ->count(); 85 | 86 | // 创建新记录 87 | self::create([ 88 | 'name' => $name, 89 | 'number' => $newNumber, 90 | 'mobile' => $mobile, 91 | 'status' => self::STATUS_WAITING, 92 | 'call_count' => 0, 93 | 'qrcode_url' => $qrcodeUrl 94 | ]); 95 | 96 | 97 | return [ 98 | 'number' => $newNumber, 99 | 'qrcode_url' => $qrcodeUrl, 100 | 'cancelled_number' => $existingNumber?->number, 101 | 'waiting_count' => $waitingCount 102 | ]; 103 | } 104 | 105 | /** 106 | * 重新排队(后移5位) 107 | * @return bool 108 | */ 109 | public function requeue(): bool 110 | { 111 | $waitingQueue = self::whereIn('status', [self::STATUS_WAITING]) 112 | ->orderBy('created_at', 'asc') 113 | ->get(); 114 | 115 | if ($waitingQueue->isEmpty()) { 116 | return true; 117 | } 118 | 119 | $currentPosition = $waitingQueue->search(function ($item) { 120 | return $item->id === $this->id; 121 | }); 122 | 123 | if ($currentPosition === false) { 124 | if ($waitingQueue->isNotEmpty()) { 125 | $lastItem = $waitingQueue->last(); 126 | $this->created_at = date('Y-m-d H:i:s', strtotime($lastItem->created_at) + 1); 127 | $this->save(); 128 | } 129 | return true; 130 | } 131 | 132 | $newPosition = min($currentPosition + 5, $waitingQueue->count() - 1); 133 | $targetItem = $waitingQueue[$newPosition]; 134 | 135 | $this->created_at = date('Y-m-d H:i:s', strtotime($targetItem->created_at) + 1); 136 | $this->save(); 137 | 138 | return true; 139 | } 140 | 141 | 142 | 143 | public function window() 144 | { 145 | return $this->belongsTo(Window::class, 'window_id', 'id'); 146 | } 147 | } -------------------------------------------------------------------------------- /support/bootstrap.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | use Dotenv\Dotenv; 16 | use support\Log; 17 | use Webman\Bootstrap; 18 | use Webman\Config; 19 | use Webman\Middleware; 20 | use Webman\Route; 21 | use Webman\Util; 22 | use Workerman\Events\Select; 23 | use Workerman\Worker; 24 | 25 | $worker = $worker ?? null; 26 | 27 | if (empty(Worker::$eventLoopClass)) { 28 | Worker::$eventLoopClass = Select::class; 29 | } 30 | 31 | set_error_handler(function ($level, $message, $file = '', $line = 0) { 32 | if (error_reporting() & $level) { 33 | throw new ErrorException($message, 0, $level, $file, $line); 34 | } 35 | }); 36 | 37 | if ($worker) { 38 | register_shutdown_function(function ($startTime) { 39 | if (time() - $startTime <= 0.1) { 40 | sleep(1); 41 | } 42 | }, time()); 43 | } 44 | 45 | if (class_exists('Dotenv\Dotenv') && file_exists(base_path(false) . '/.env')) { 46 | if (method_exists('Dotenv\Dotenv', 'createUnsafeMutable')) { 47 | Dotenv::createUnsafeMutable(base_path(false))->load(); 48 | } else { 49 | Dotenv::createMutable(base_path(false))->load(); 50 | } 51 | } 52 | 53 | Config::clear(); 54 | support\App::loadAllConfig(['route']); 55 | if ($timezone = config('app.default_timezone')) { 56 | date_default_timezone_set($timezone); 57 | } 58 | 59 | foreach (config('autoload.files', []) as $file) { 60 | include_once $file; 61 | } 62 | foreach (config('plugin', []) as $firm => $projects) { 63 | foreach ($projects as $name => $project) { 64 | if (!is_array($project)) { 65 | continue; 66 | } 67 | foreach ($project['autoload']['files'] ?? [] as $file) { 68 | include_once $file; 69 | } 70 | } 71 | foreach ($projects['autoload']['files'] ?? [] as $file) { 72 | include_once $file; 73 | } 74 | } 75 | 76 | Middleware::load(config('middleware', [])); 77 | foreach (config('plugin', []) as $firm => $projects) { 78 | foreach ($projects as $name => $project) { 79 | if (!is_array($project) || $name === 'static') { 80 | continue; 81 | } 82 | Middleware::load($project['middleware'] ?? []); 83 | } 84 | Middleware::load($projects['middleware'] ?? [], $firm); 85 | if ($staticMiddlewares = config("plugin.$firm.static.middleware")) { 86 | Middleware::load(['__static__' => $staticMiddlewares], $firm); 87 | } 88 | } 89 | Middleware::load(['__static__' => config('static.middleware', [])]); 90 | 91 | foreach (config('bootstrap', []) as $className) { 92 | if (!class_exists($className)) { 93 | $log = "Warning: Class $className setting in config/bootstrap.php not found\r\n"; 94 | echo $log; 95 | Log::error($log); 96 | continue; 97 | } 98 | /** @var Bootstrap $className */ 99 | $className::start($worker); 100 | } 101 | 102 | foreach (config('plugin', []) as $firm => $projects) { 103 | foreach ($projects as $name => $project) { 104 | if (!is_array($project)) { 105 | continue; 106 | } 107 | foreach ($project['bootstrap'] ?? [] as $className) { 108 | if (!class_exists($className)) { 109 | $log = "Warning: Class $className setting in config/plugin/$firm/$name/bootstrap.php not found\r\n"; 110 | echo $log; 111 | Log::error($log); 112 | continue; 113 | } 114 | /** @var Bootstrap $className */ 115 | $className::start($worker); 116 | } 117 | } 118 | foreach ($projects['bootstrap'] ?? [] as $className) { 119 | /** @var string $className */ 120 | if (!class_exists($className)) { 121 | $log = "Warning: Class $className setting in plugin/$firm/config/bootstrap.php not found\r\n"; 122 | echo $log; 123 | Log::error($log); 124 | continue; 125 | } 126 | /** @var Bootstrap $className */ 127 | $className::start($worker); 128 | } 129 | } 130 | 131 | $directory = base_path() . '/plugin'; 132 | $paths = [config_path()]; 133 | foreach (Util::scanDir($directory) as $path) { 134 | if (is_dir($path = "$path/config")) { 135 | $paths[] = $path; 136 | } 137 | } 138 | Route::load($paths); 139 | 140 | -------------------------------------------------------------------------------- /plugin/webman/gateway/Events.php: -------------------------------------------------------------------------------- 1 | 'connected', 52 | 'client_id' => $client_id 53 | ])); 54 | } 55 | 56 | /** 57 | * @param $client_id 58 | * client_id固定为20个字符的字符串,用来全局标记一个socket连接,每个客户端连接都会被分配一个全局唯一的client_id。 59 | * @param $data 60 | * websocket握手时的http头数据,包含get、server等变量 61 | * var ws = new WebSocket('ws://127.0.0.1:7272/?token=kjxdvjkasfh'); 62 | * @return void 63 | */ 64 | public static function onWebSocketConnect($client_id, $data): void 65 | { 66 | // if (!isset($data['get']['token'])) { 67 | // Gateway::closeClient($client_id); 68 | // } 69 | } 70 | 71 | 72 | public static function onMessage($client_id, $message) 73 | { 74 | try { 75 | $data = json_decode($message, true); 76 | if (!$data || !isset($data['type'])) { 77 | return; 78 | } 79 | 80 | switch ($data['type']) { 81 | case 'bind_display': 82 | self::handleBindDisplay($client_id); 83 | break; 84 | 85 | case 'bind_window': 86 | self::handleBindWindow($client_id); 87 | break; 88 | 89 | case 'heartbeat': 90 | Gateway::sendToClient($client_id, json_encode([ 91 | 'type' => 'heartbeat', 92 | 'time' => time() 93 | ])); 94 | break; 95 | } 96 | } catch (\Exception $e) { 97 | Gateway::sendToClient($client_id, json_encode([ 98 | 'type' => 'error', 99 | 'message' => $e->getMessage() 100 | ])); 101 | } 102 | } 103 | 104 | /** 105 | * 当用户断开连接时触发 106 | * 注意:onClose回调里无法使用Gateway::getSession()来获得当前用户的session数据,但是仍然可以使用$_SESSION变量获得。 107 | * 108 | * 注意:onClose回调里无法使用Gateway::getUidByClientId()接口来获得uid, 109 | * 解决办法是在Gateway::bindUid()时记录一个$_SESSION['uid'], 110 | * onClose的时候用$_SESSION['uid']来获得uid。 111 | * 112 | * 注意:断网断电等极端情况可能无法及时触发onClose回调, 113 | * 因为这种情况客户端来不及给服务端发送断开连接的包(fin包), 114 | * 服务端就无法得知连接已经断开。 115 | * 检测这种极端情况需要心跳检测,并 116 | * 且必须设置$gateway->pingNotResponseLimit>0。 117 | * 这种断网断电的极端情况onClose将被延迟触发,延迟时间为小于$gateway->pingInterval*$gateway->pingNotResponseLimit秒, 118 | * 如果$gateway->pingInterval 和 $gateway->pingNotResponseLimit 中任何一个为0, 119 | * 则可能会无限延迟。 120 | * @throws Exception 121 | */ 122 | public static function onClose($client_id): void 123 | { 124 | // 可以在这里清理一些数据 125 | Gateway::sendToAll(json_encode([ 126 | 'type' => 'client_closed', 127 | 'client_id' => $client_id 128 | ])); 129 | } 130 | 131 | 132 | /** 133 | * 处理显示屏绑定 134 | */ 135 | private static function handleBindDisplay($client_id): void 136 | { 137 | Gateway::bindUid($client_id, 'display'); 138 | Gateway::sendToClient($client_id, json_encode([ 139 | 'type' => 'bind_success', 140 | 'display' => true 141 | ])); 142 | } 143 | 144 | /** 145 | * 处理窗口绑定 146 | */ 147 | private static function handleBindWindow($client_id): void 148 | { 149 | Gateway::bindUid($client_id, 'window'); 150 | Gateway::sendToClient($client_id, json_encode([ 151 | 'type' => 'bind_success', 152 | 'display' => true 153 | ])); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /public/asset/toastr/2.1.3/toastr.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Note that this is toastr v2.1.3, the "latest" version in url has no more maintenance, 3 | * please go to https://cdnjs.com/libraries/toastr.js and pick a certain version you want to use, 4 | * make sure you copy the url from the website since the url may change between versions. 5 | * */ 6 | !function(e){e(["jquery"],function(e){return function(){function t(e,t,n){return g({type:O.error,iconClass:m().iconClasses.error,message:e,optionsOverride:n,title:t})}function n(t,n){return t||(t=m()),v=e("#"+t.containerId),v.length?v:(n&&(v=d(t)),v)}function o(e,t,n){return g({type:O.info,iconClass:m().iconClasses.info,message:e,optionsOverride:n,title:t})}function s(e){C=e}function i(e,t,n){return g({type:O.success,iconClass:m().iconClasses.success,message:e,optionsOverride:n,title:t})}function a(e,t,n){return g({type:O.warning,iconClass:m().iconClasses.warning,message:e,optionsOverride:n,title:t})}function r(e,t){var o=m();v||n(o),u(e,o,t)||l(o)}function c(t){var o=m();return v||n(o),t&&0===e(":focus",t).length?void h(t):void(v.children().length&&v.remove())}function l(t){for(var n=v.children(),o=n.length-1;o>=0;o--)u(e(n[o]),t)}function u(t,n,o){var s=!(!o||!o.force)&&o.force;return!(!t||!s&&0!==e(":focus",t).length)&&(t[n.hideMethod]({duration:n.hideDuration,easing:n.hideEasing,complete:function(){h(t)}}),!0)}function d(t){return v=e("
").attr("id",t.containerId).addClass(t.positionClass),v.appendTo(e(t.target)),v}function p(){return{tapToDismiss:!0,toastClass:"toast",containerId:"toast-container",debug:!1,showMethod:"fadeIn",showDuration:300,showEasing:"swing",onShown:void 0,hideMethod:"fadeOut",hideDuration:1e3,hideEasing:"swing",onHidden:void 0,closeMethod:!1,closeDuration:!1,closeEasing:!1,closeOnHover:!0,extendedTimeOut:1e3,iconClasses:{error:"toast-error",info:"toast-info",success:"toast-success",warning:"toast-warning"},iconClass:"toast-info",positionClass:"toast-top-right",timeOut:5e3,titleClass:"toast-title",messageClass:"toast-message",escapeHtml:!1,target:"body",closeHtml:'',closeClass:"toast-close-button",newestOnTop:!0,preventDuplicates:!1,progressBar:!1,progressClass:"toast-progress",rtl:!1}}function f(e){C&&C(e)}function g(t){function o(e){return null==e&&(e=""),e.replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function s(){c(),u(),d(),p(),g(),C(),l(),i()}function i(){var e="";switch(t.iconClass){case"toast-success":case"toast-info":e="polite";break;default:e="assertive"}I.attr("aria-live",e)}function a(){E.closeOnHover&&I.hover(H,D),!E.onclick&&E.tapToDismiss&&I.click(b),E.closeButton&&j&&j.click(function(e){e.stopPropagation?e.stopPropagation():void 0!==e.cancelBubble&&e.cancelBubble!==!0&&(e.cancelBubble=!0),E.onCloseClick&&E.onCloseClick(e),b(!0)}),E.onclick&&I.click(function(e){E.onclick(e),b()})}function r(){I.hide(),I[E.showMethod]({duration:E.showDuration,easing:E.showEasing,complete:E.onShown}),E.timeOut>0&&(k=setTimeout(b,E.timeOut),F.maxHideTime=parseFloat(E.timeOut),F.hideEta=(new Date).getTime()+F.maxHideTime,E.progressBar&&(F.intervalId=setInterval(x,10)))}function c(){t.iconClass&&I.addClass(E.toastClass).addClass(y)}function l(){E.newestOnTop?v.prepend(I):v.append(I)}function u(){if(t.title){var e=t.title;E.escapeHtml&&(e=o(t.title)),M.append(e).addClass(E.titleClass),I.append(M)}}function d(){if(t.message){var e=t.message;E.escapeHtml&&(e=o(t.message)),B.append(e).addClass(E.messageClass),I.append(B)}}function p(){E.closeButton&&(j.addClass(E.closeClass).attr("role","button"),I.prepend(j))}function g(){E.progressBar&&(q.addClass(E.progressClass),I.prepend(q))}function C(){E.rtl&&I.addClass("rtl")}function O(e,t){if(e.preventDuplicates){if(t.message===w)return!0;w=t.message}return!1}function b(t){var n=t&&E.closeMethod!==!1?E.closeMethod:E.hideMethod,o=t&&E.closeDuration!==!1?E.closeDuration:E.hideDuration,s=t&&E.closeEasing!==!1?E.closeEasing:E.hideEasing;if(!e(":focus",I).length||t)return clearTimeout(F.intervalId),I[n]({duration:o,easing:s,complete:function(){h(I),clearTimeout(k),E.onHidden&&"hidden"!==P.state&&E.onHidden(),P.state="hidden",P.endTime=new Date,f(P)}})}function D(){(E.timeOut>0||E.extendedTimeOut>0)&&(k=setTimeout(b,E.extendedTimeOut),F.maxHideTime=parseFloat(E.extendedTimeOut),F.hideEta=(new Date).getTime()+F.maxHideTime)}function H(){clearTimeout(k),F.hideEta=0,I.stop(!0,!0)[E.showMethod]({duration:E.showDuration,easing:E.showEasing})}function x(){var e=(F.hideEta-(new Date).getTime())/F.maxHideTime*100;q.width(e+"%")}var E=m(),y=t.iconClass||E.iconClass;if("undefined"!=typeof t.optionsOverride&&(E=e.extend(E,t.optionsOverride),y=t.optionsOverride.iconClass||y),!O(E,t)){T++,v=n(E,!0);var k=null,I=e("
"),M=e("
"),B=e("
"),q=e("
"),j=e(E.closeHtml),F={intervalId:null,hideEta:null,maxHideTime:null},P={toastId:T,state:"visible",startTime:new Date,options:E,map:t};return s(),r(),a(),f(P),E.debug&&console&&console.log(P),I}}function m(){return e.extend({},p(),b.options)}function h(e){v||(v=n()),e.is(":visible")||(e.remove(),e=null,0===v.children().length&&(v.remove(),w=void 0))}var v,C,w,T=0,O={error:"error",info:"info",success:"success",warning:"warning"},b={clear:r,remove:c,error:t,getContainer:n,info:o,options:{},subscribe:s,success:i,version:"2.1.3",warning:a};return b}()})}("function"==typeof define&&define.amd?define:function(e,t){"undefined"!=typeof module&&module.exports?module.exports=t(require("jquery")):window.toastr=t(window.jQuery)}); 7 | //# sourceMappingURL=toastr.js.map 8 | -------------------------------------------------------------------------------- /app/view/queue/status.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 排队状态查询 5 | 6 | 7 | 8 | 87 | 88 | 89 |
90 |
91 |

排队状态查询

92 |

实时查看您的排队进度

93 |
94 | 95 | 96 |
97 |
98 | 99 |
100 | 101 | getStatusText() ?> 102 | 103 |
104 | 105 | 106 |
107 |
108 |
排队号码
109 |
number ?>
110 |
111 | 112 | window): ?> 113 |
114 |
窗口
115 |
window->name ?>
116 |
117 | 118 | 119 | 120 | status == \app\model\QueueNumber::STATUS_WAITING ): ?> 121 |
122 |
前方等待人数
123 |
waiting ?>
124 |
125 | 126 | 127 | 128 | call_count): ?> 129 |
130 |
叫号次数
131 |
132 | call_count ?>次 133 | call_count >= 2): ?> 134 | 多次叫号 135 | 136 |
137 |
138 | 139 | 140 |
141 |
取号时间
142 |
143 | created_at)) ?> 144 |
145 |
146 |
147 |
148 |
149 | 150 |
151 |
152 |
153 | 154 |

号码不存在或已过期

155 |
156 |
157 |
158 | 159 | 160 | 161 | 165 |
166 | 167 | 168 | 169 | 170 | 171 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /public/asset/toastr/2.1.3/toastr.min.css: -------------------------------------------------------------------------------- 1 | /* * Note that this is toastr v2.1.3, the "latest" version in url has no more maintenance, * please go to https://cdnjs.com/libraries/toastr.js and pick a certain version you want to use, * make sure you copy the url from the website since the url may change between versions. * */ .toast-title{font-weight:700}.toast-message{-ms-word-wrap:break-word;word-wrap:break-word}.toast-message a,.toast-message label{color:#FFF}.toast-message a:hover{color:#CCC;text-decoration:none}.toast-close-button{position:relative;right:-.3em;top:-.3em;float:right;font-size:20px;font-weight:700;color:#FFF;-webkit-text-shadow:0 1px 0 #fff;text-shadow:0 1px 0 #fff;opacity:.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80);line-height:1}.toast-close-button:focus,.toast-close-button:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}.rtl .toast-close-button{left:-.3em;float:left;right:.3em}button.toast-close-button{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.toast-top-center{top:0;right:0;width:100%}.toast-bottom-center{bottom:0;right:0;width:100%}.toast-top-full-width{top:0;right:0;width:100%}.toast-bottom-full-width{bottom:0;right:0;width:100%}.toast-top-left{top:12px;left:12px}.toast-top-right{top:12px;right:12px}.toast-bottom-right{right:12px;bottom:12px}.toast-bottom-left{bottom:12px;left:12px}#toast-container{position:fixed;z-index:999999;pointer-events:none}#toast-container *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}#toast-container>div{position:relative;pointer-events:auto;overflow:hidden;margin:0 0 6px;padding:15px 15px 15px 50px;width:300px;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;background-position:15px center;background-repeat:no-repeat;-moz-box-shadow:0 0 12px #999;-webkit-box-shadow:0 0 12px #999;box-shadow:0 0 12px #999;color:#FFF;opacity:.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80)}#toast-container>div.rtl{direction:rtl;padding:15px 50px 15px 15px;background-position:right 15px center}#toast-container>div:hover{-moz-box-shadow:0 0 12px #000;-webkit-box-shadow:0 0 12px #000;box-shadow:0 0 12px #000;opacity:1;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);filter:alpha(opacity=100);cursor:pointer}#toast-container>.toast-info{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=)!important}#toast-container>.toast-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=)!important}#toast-container>.toast-success{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==)!important}#toast-container>.toast-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=)!important}#toast-container.toast-bottom-center>div,#toast-container.toast-top-center>div{width:300px;margin-left:auto;margin-right:auto}#toast-container.toast-bottom-full-width>div,#toast-container.toast-top-full-width>div{width:96%;margin-left:auto;margin-right:auto}.toast{background-color:#030303}.toast-success{background-color:#51A351}.toast-error{background-color:#BD362F}.toast-info{background-color:#2F96B4}.toast-warning{background-color:#F89406}.toast-progress{position:absolute;left:0;bottom:0;height:4px;background-color:#000;opacity:.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}@media all and (max-width:240px){#toast-container>div{padding:8px 8px 8px 50px;width:11em}#toast-container>div.rtl{padding:8px 50px 8px 8px}#toast-container .toast-close-button{right:-.2em;top:-.2em}#toast-container .rtl .toast-close-button{left:-.2em;right:.2em}}@media all and (min-width:241px) and (max-width:480px){#toast-container>div{padding:8px 8px 8px 50px;width:18em}#toast-container>div.rtl{padding:8px 50px 8px 8px}#toast-container .toast-close-button{right:-.2em;top:-.2em}#toast-container .rtl .toast-close-button{left:-.2em;right:.2em}}@media all and (min-width:481px) and (max-width:768px){#toast-container>div{padding:15px 15px 15px 50px;width:25em}#toast-container>div.rtl{padding:15px 50px 15px 15px}} -------------------------------------------------------------------------------- /app/view/window/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 窗口管理 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
窗口管理
14 | 17 |
18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 41 | 53 | 54 | 55 | 56 |
ID名称描述状态操作
id ?>name ?>description ?> 37 | 38 | getStatusText() ?> 39 | 40 | 42 |
43 | 47 | 51 |
52 |
57 |
58 |
59 |
60 |
61 | 62 | 63 | 97 | 98 | 99 | 100 | 101 | 102 | 193 | 194 | -------------------------------------------------------------------------------- /process/Monitor.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | namespace process; 16 | 17 | use FilesystemIterator; 18 | use RecursiveDirectoryIterator; 19 | use RecursiveIteratorIterator; 20 | use SplFileInfo; 21 | use Workerman\Timer; 22 | use Workerman\Worker; 23 | 24 | /** 25 | * Class FileMonitor 26 | * @package process 27 | */ 28 | class Monitor 29 | { 30 | /** 31 | * @var array 32 | */ 33 | protected $paths = []; 34 | 35 | /** 36 | * @var array 37 | */ 38 | protected $extensions = []; 39 | 40 | /** 41 | * @var array 42 | */ 43 | protected $loadedFiles = []; 44 | 45 | /** 46 | * @var string 47 | */ 48 | public static $lockFile = __DIR__ . '/../runtime/monitor.lock'; 49 | 50 | /** 51 | * Pause monitor 52 | * @return void 53 | */ 54 | public static function pause() 55 | { 56 | file_put_contents(static::$lockFile, time()); 57 | } 58 | 59 | /** 60 | * Resume monitor 61 | * @return void 62 | */ 63 | public static function resume(): void 64 | { 65 | clearstatcache(); 66 | if (is_file(static::$lockFile)) { 67 | unlink(static::$lockFile); 68 | } 69 | } 70 | 71 | /** 72 | * Whether monitor is paused 73 | * @return bool 74 | */ 75 | public static function isPaused(): bool 76 | { 77 | clearstatcache(); 78 | return file_exists(static::$lockFile); 79 | } 80 | 81 | /** 82 | * FileMonitor constructor. 83 | * @param $monitorDir 84 | * @param $monitorExtensions 85 | * @param array $options 86 | */ 87 | public function __construct($monitorDir, $monitorExtensions, array $options = []) 88 | { 89 | static::resume(); 90 | $this->paths = (array)$monitorDir; 91 | $this->extensions = $monitorExtensions; 92 | foreach (get_included_files() as $index => $file) { 93 | $this->loadedFiles[$file] = $index; 94 | if (strpos($file, 'webman-framework/src/support/App.php')) { 95 | break; 96 | } 97 | } 98 | if (!Worker::getAllWorkers()) { 99 | return; 100 | } 101 | $disableFunctions = explode(',', ini_get('disable_functions')); 102 | if (in_array('exec', $disableFunctions, true)) { 103 | echo "\nMonitor file change turned off because exec() has been disabled by disable_functions setting in " . PHP_CONFIG_FILE_PATH . "/php.ini\n"; 104 | } else { 105 | if ($options['enable_file_monitor'] ?? true) { 106 | Timer::add(1, function () { 107 | $this->checkAllFilesChange(); 108 | }); 109 | } 110 | } 111 | 112 | $memoryLimit = $this->getMemoryLimit($options['memory_limit'] ?? null); 113 | if ($memoryLimit && ($options['enable_memory_monitor'] ?? true)) { 114 | Timer::add(60, [$this, 'checkMemory'], [$memoryLimit]); 115 | } 116 | } 117 | 118 | /** 119 | * @param $monitorDir 120 | * @return bool 121 | */ 122 | public function checkFilesChange($monitorDir): bool 123 | { 124 | static $lastMtime, $tooManyFilesCheck; 125 | if (!$lastMtime) { 126 | $lastMtime = time(); 127 | } 128 | clearstatcache(); 129 | if (!is_dir($monitorDir)) { 130 | if (!is_file($monitorDir)) { 131 | return false; 132 | } 133 | $iterator = [new SplFileInfo($monitorDir)]; 134 | } else { 135 | // recursive traversal directory 136 | $dirIterator = new RecursiveDirectoryIterator($monitorDir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS); 137 | $iterator = new RecursiveIteratorIterator($dirIterator); 138 | } 139 | $count = 0; 140 | foreach ($iterator as $file) { 141 | $count ++; 142 | /** var SplFileInfo $file */ 143 | if (is_dir($file->getRealPath())) { 144 | continue; 145 | } 146 | // check mtime 147 | if (in_array($file->getExtension(), $this->extensions, true) && $lastMtime < $file->getMTime()) { 148 | $lastMtime = $file->getMTime(); 149 | if (DIRECTORY_SEPARATOR === '/' && isset($this->loadedFiles[$file->getRealPath()])) { 150 | echo "$file updated but cannot be reloaded because only auto-loaded files support reload.\n"; 151 | continue; 152 | } 153 | $var = 0; 154 | exec('"'.PHP_BINARY . '" -l ' . $file, $out, $var); 155 | if ($var) { 156 | continue; 157 | } 158 | echo $file . " updated and reload\n"; 159 | // send SIGUSR1 signal to master process for reload 160 | if (DIRECTORY_SEPARATOR === '/') { 161 | posix_kill(posix_getppid(), SIGUSR1); 162 | } else { 163 | return true; 164 | } 165 | break; 166 | } 167 | } 168 | if (!$tooManyFilesCheck && $count > 1000) { 169 | echo "Monitor: There are too many files ($count files) in $monitorDir which makes file monitoring very slow\n"; 170 | $tooManyFilesCheck = 1; 171 | } 172 | return false; 173 | } 174 | 175 | /** 176 | * @return bool 177 | */ 178 | public function checkAllFilesChange(): bool 179 | { 180 | if (static::isPaused()) { 181 | return false; 182 | } 183 | foreach ($this->paths as $path) { 184 | if ($this->checkFilesChange($path)) { 185 | return true; 186 | } 187 | } 188 | return false; 189 | } 190 | 191 | /** 192 | * @param $memoryLimit 193 | * @return void 194 | */ 195 | public function checkMemory($memoryLimit) 196 | { 197 | if (static::isPaused() || $memoryLimit <= 0) { 198 | return; 199 | } 200 | $ppid = posix_getppid(); 201 | $childrenFile = "/proc/$ppid/task/$ppid/children"; 202 | if (!is_file($childrenFile) || !($children = file_get_contents($childrenFile))) { 203 | return; 204 | } 205 | foreach (explode(' ', $children) as $pid) { 206 | $pid = (int)$pid; 207 | $statusFile = "/proc/$pid/status"; 208 | if (!is_file($statusFile) || !($status = file_get_contents($statusFile))) { 209 | continue; 210 | } 211 | $mem = 0; 212 | if (preg_match('/VmRSS\s*?:\s*?(\d+?)\s*?kB/', $status, $match)) { 213 | $mem = $match[1]; 214 | } 215 | $mem = (int)($mem / 1024); 216 | if ($mem >= $memoryLimit) { 217 | posix_kill($pid, SIGINT); 218 | } 219 | } 220 | } 221 | 222 | /** 223 | * Get memory limit 224 | * @return float 225 | */ 226 | protected function getMemoryLimit($memoryLimit) 227 | { 228 | if ($memoryLimit === 0) { 229 | return 0; 230 | } 231 | $usePhpIni = false; 232 | if (!$memoryLimit) { 233 | $memoryLimit = ini_get('memory_limit'); 234 | $usePhpIni = true; 235 | } 236 | 237 | if ($memoryLimit == -1) { 238 | return 0; 239 | } 240 | $unit = strtolower($memoryLimit[strlen($memoryLimit) - 1]); 241 | if ($unit === 'g') { 242 | $memoryLimit = 1024 * (int)$memoryLimit; 243 | } else if ($unit === 'm') { 244 | $memoryLimit = (int)$memoryLimit; 245 | } else if ($unit === 'k') { 246 | $memoryLimit = ((int)$memoryLimit / 1024); 247 | } else { 248 | $memoryLimit = ((int)$memoryLimit / (1024 * 1024)); 249 | } 250 | if ($memoryLimit < 30) { 251 | $memoryLimit = 30; 252 | } 253 | if ($usePhpIni) { 254 | $memoryLimit = (int)(0.8 * $memoryLimit); 255 | } 256 | return $memoryLimit; 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /app/view/queue/take.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 排队取号 5 | 6 | 7 | 8 | 9 | 10 | 45 | 46 | 47 |
48 | 49 |
50 |
51 |

排队取号

52 |
53 |
54 |
55 |
56 | 57 | 63 |
64 | 请输入姓名 65 |
66 |
67 | 68 |
69 | 70 | 77 |
78 | 请输入正确的手机号 79 |
80 |
81 | 82 | 86 |
87 |
88 |
89 | 90 | 91 |
92 |
93 |
94 |
95 |
96 |
97 | 98 | 99 |
100 |
101 |
当前叫号
102 |
103 |
104 |
105 |
106 | Loading... 107 |
108 |
109 |
110 |
111 |
112 | 113 | 114 | 115 | 116 | 117 | 265 | 266 | -------------------------------------------------------------------------------- /app/view/queue/display.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 叫号显示屏 5 | 6 | 7 | 8 | 9 | 71 | 72 | 73 |
74 |
75 | 76 |
77 |
78 |

正在叫号

79 |
80 | 81 |
82 |
83 | number ?>号 84 | name) ?> 85 | window->name ?> 86 |
87 |
88 | 89 |
90 |
91 |
92 | 93 | 94 |
95 |
96 |

等待叫号

97 |
98 | 99 |
100 |
101 | number ?>号 102 | name) ?> 103 |
104 |
105 | 106 |
107 |
108 |
109 | 110 | 111 |
112 |
113 |

扫码取号

114 |
115 |
116 | 取号二维码 120 |
121 | 122 | 123 |

过号人员

124 |
125 | 126 |
127 |
128 | number ?>号 129 | name) ?> 130 |
131 |
132 | 133 |
134 |
135 |
136 |
137 |
138 |
139 | 140 | 141 | 142 | 295 | 296 | -------------------------------------------------------------------------------- /app/controller/QueueController.php: -------------------------------------------------------------------------------- 1 | whereDate('created_at', date('Y-m-d')) 29 | ->with('window') 30 | ->orderBy('updated_at', 'desc') 31 | ->get(); 32 | 33 | 34 | // 获取等待叫号的号码 35 | $waiting = QueueNumber::where('status', QueueNumber::STATUS_WAITING) 36 | ->where('call_count', 0) 37 | ->whereDate('created_at', date('Y-m-d')) 38 | ->orderBy('created_at', 'asc') 39 | ->limit(10) 40 | ->get(); 41 | 42 | 43 | // 获取过号的号码 44 | $passed = QueueNumber::where('status', QueueNumber::STATUS_PASSED) 45 | ->orderBy('updated_at', 'desc') 46 | ->whereDate('created_at', date('Y-m-d')) 47 | ->limit(10) 48 | ->get(); 49 | 50 | 51 | return view('queue/display', [ 52 | 'current' => $current, 53 | 'waiting' => $waiting, 54 | 'passed' => $passed 55 | ]); 56 | } 57 | 58 | /** 59 | * 管理界面 60 | */ 61 | public function admin(Request $request) 62 | { 63 | $windowId = $request->get('window'); 64 | $page = $request->get('page', 1); 65 | $perPage = (int)$request->get('per_page', 10); 66 | 67 | // 限制每页显示数量的范围 68 | $perPage = in_array($perPage, [10, 20, 50]) ? $perPage : 10; 69 | 70 | $query = QueueNumber::whereIn('status', [ 71 | QueueNumber::STATUS_CALLING, 72 | QueueNumber::STATUS_WAITING, 73 | QueueNumber::STATUS_PASSED 74 | ]) 75 | ->when($windowId, function ($query) use ($windowId) { 76 | $query->whereIn('window_id', [0, $windowId]); 77 | }) 78 | ->whereDate('created_at', date('Y-m-d')) 79 | ->orderBy('created_at', 'asc'); 80 | 81 | $waiting = $query->paginate($perPage, ['*'], 'page', $page); 82 | $windows = Window::where('status', Window::STATUS_ENABLED) 83 | ->orderBy('id', 'asc') 84 | ->get(); 85 | 86 | return view('queue/admin', [ 87 | 'waiting' => $waiting, 88 | 'windows' => $windows, 89 | 'currentWindow' => $windowId, 90 | 'perPage' => $perPage 91 | ]); 92 | } 93 | 94 | /** 95 | * 取号页面 96 | */ 97 | public function takePage(Request $request) 98 | { 99 | //是否重新取号 100 | $renumbering = $request->get('renumbering'); 101 | 102 | if ($renumbering) { 103 | $request->session()->delete('mobile'); 104 | } 105 | 106 | //获取session中的手机号码 107 | $mobile = $request->session()->get('mobile'); 108 | 109 | //如果存在手机号码 110 | if ($mobile) { 111 | $queue = QueueNumber::where('mobile', $mobile) 112 | ->whereDate('created_at', date('Y-m-d')) 113 | ->whereIn('status', [QueueNumber::STATUS_WAITING, QueueNumber::STATUS_PASSED, QueueNumber::STATUS_CALLING]) 114 | ->orderBy('created_at', 'desc') 115 | ->first(); 116 | 117 | if ($queue) { 118 | return redirect('/queue/status/' . $queue->number); 119 | } 120 | } 121 | 122 | return view('queue/take'); 123 | } 124 | 125 | /** 126 | * 获取当前叫号信息 127 | */ 128 | public function current(): Response 129 | { 130 | $current = QueueNumber::with('window') 131 | ->where('status', QueueNumber::STATUS_CALLING) 132 | ->whereDate('created_at', date('Y-m-d')) 133 | ->orderBy('updated_at', 'desc') 134 | ->first(); 135 | 136 | return json([ 137 | 'code' => 0, 138 | 'msg' => 'success', 139 | 'data' => $current ? [ 140 | 'number' => $current->number, 141 | 'name' => hideNameCharacters($current->name), 142 | 'window' => $current->window->name 143 | ] : null 144 | ]); 145 | } 146 | 147 | /** 148 | * 取号 149 | */ 150 | public function take(Request $request): Response 151 | { 152 | $name = $request->post('name'); 153 | $mobile = $request->post('mobile'); 154 | 155 | if (!$name || strlen($name) > 50) { 156 | return json(['code' => 1, 'msg' => '请输入正确的姓名']); 157 | } 158 | 159 | if (!preg_match('/^1[3-9]\d{9}$/', $mobile)) { 160 | return json(['code' => 1, 'msg' => '请输入正确的手机号']); 161 | } 162 | 163 | try { 164 | 165 | $result = QueueNumber::generateNumber($name, $mobile); 166 | 167 | Gateway::$registerAddress = '127.0.0.1:1236'; 168 | 169 | $message = [ 170 | 'type' => 'status_change', 171 | 'data' => [ 172 | 'name' => hideNameCharacters($name), 173 | 'number' => $result['number'], 174 | 'status' => QueueNumber::STATUS_WAITING 175 | ] 176 | ]; 177 | 178 | //发送显示屏消息 179 | Gateway::sendToUid('display', json_encode($message)); 180 | 181 | $message = [ 182 | 'type' => 'status_change', 183 | 'data' => [ 184 | 'number' => $result['number'], 185 | 'name' => $name, 186 | 'mobile' => $mobile, 187 | 'call_count' => 0, 188 | 'window_id' => 0, 189 | 'window_name' => '未分配', 190 | 'status' => QueueNumber::STATUS_WAITING 191 | ] 192 | ]; 193 | 194 | //发送窗口管理界面消息 195 | Gateway::sendToUid('window', json_encode($message)); 196 | 197 | return json(['code' => 0, 'msg' => 'success', 'data' => $result]); 198 | } catch (\Exception $e) { 199 | return json(['code' => 1, 'msg' => $e->getMessage()]); 200 | } 201 | } 202 | 203 | /** 204 | * 叫号 205 | */ 206 | public function call(Request $request): Response 207 | { 208 | $number = $request->post('number'); 209 | $window = $request->post('window'); 210 | 211 | if (!$window) { 212 | return json(['code' => 1, 'msg' => '请选择窗口']); 213 | } 214 | 215 | // 检查窗口是否有效 216 | $windowExists = Window::where('id', $window) 217 | ->where('status', Window::STATUS_ENABLED) 218 | ->first(); 219 | 220 | if (!$windowExists) { 221 | return json(['code' => 1, 'msg' => '无效的窗口号']); 222 | } 223 | 224 | // 自动将该窗口之前叫号的号码标记为过号 225 | try { 226 | autoPassOverCalledNumbers($window); 227 | } catch (\Exception $e) { 228 | Log::error($e->getMessage()); 229 | } 230 | 231 | $queue = QueueNumber::where('number', $number) 232 | ->orWhere('mobile', $number) 233 | ->whereIn('status', [QueueNumber::STATUS_WAITING, QueueNumber::STATUS_PASSED]) 234 | ->whereDate('created_at', date('Y-m-d')) 235 | ->first(); 236 | 237 | if (!$queue) { 238 | return json(['code' => 1, 'msg' => '号码不存在或状态错误']); 239 | } 240 | 241 | $queue->status = QueueNumber::STATUS_CALLING; 242 | $queue->window_id = $window; 243 | $queue->call_count++; 244 | $queue->save(); 245 | 246 | 247 | try { 248 | $message = [ 249 | 'type' => 'call_number', 250 | 'data' => [ 251 | 'number' => $queue->number, 252 | 'name' => hideNameCharacters($queue->name), 253 | 'call_count' => $queue->call_count, 254 | 'window_id' => $window, 255 | 'window_name' => $windowExists->name, 256 | 'status' => QueueNumber::STATUS_CALLING 257 | ] 258 | ]; 259 | // 发送给显示屏 260 | Gateway::$registerAddress = '127.0.0.1:1236'; 261 | Gateway::sendToUid('display', json_encode($message)); 262 | 263 | $message = [ 264 | 'type' => 'call_number', 265 | 'data' => [ 266 | 'number' => $queue->number, 267 | 'name' => $queue->name, 268 | 'mobile' => $queue->mobile, 269 | 'call_count' => $queue->call_count, 270 | 'window_id' => $window, 271 | 'window_name' => $windowExists->name, 272 | 'status' => QueueNumber::STATUS_CALLING 273 | ] 274 | ]; 275 | // 发送给窗口管理界面 276 | Gateway::sendToUid('window', json_encode($message)); 277 | } catch (\Exception $e) { 278 | Log::error($e->getMessage()); 279 | } 280 | 281 | return json(['code' => 0, 'msg' => 'success']); 282 | } 283 | 284 | /** 285 | * 完成办理 286 | */ 287 | public function complete(Request $request): Response 288 | { 289 | $number = $request->post('number'); 290 | 291 | $queue = QueueNumber::where('number', $number) 292 | ->whereIn('status', [QueueNumber::STATUS_CALLING, QueueNumber::STATUS_WAITING, QueueNumber::STATUS_PASSED]) 293 | ->whereDate('created_at', date('Y-m-d')) 294 | ->first(); 295 | 296 | if (!$queue) { 297 | return json(['code' => 1, 'msg' => '号码不存在或状态错误']); 298 | } 299 | 300 | $queue->status = QueueNumber::STATUS_COMPLETED; 301 | $queue->save(); 302 | 303 | 304 | try { 305 | 306 | Gateway::$registerAddress = '127.0.0.1:1236'; 307 | // 使用Gateway发送状态更新通知 308 | $message = [ 309 | 'type' => 'status_change', 310 | 'data' => [ 311 | 'number' => $number, 312 | 'name' => hideNameCharacters($queue->name), 313 | 'status' => QueueNumber::STATUS_COMPLETED 314 | ] 315 | ]; 316 | Gateway::sendToUid('display', json_encode($message)); 317 | 318 | // 使用Gateway发送状态更新通知 319 | $message = [ 320 | 'type' => 'status_change', 321 | 'data' => [ 322 | 'number' => $number, 323 | 'name' => $queue->name, 324 | 'mobile' => $queue->mobile, 325 | 'status' => QueueNumber::STATUS_COMPLETED 326 | ] 327 | ]; 328 | Gateway::sendToUid('window', json_encode($message)); 329 | } catch (\Exception $e) { 330 | \support\Log::error($e->getMessage()); 331 | } 332 | 333 | return json(['code' => 0, 'msg' => 'success']); 334 | } 335 | 336 | /** 337 | * 标记过号 338 | */ 339 | public function pass(Request $request): Response 340 | { 341 | $number = $request->post('number'); 342 | 343 | $queue = QueueNumber::with('window') 344 | ->where('number', $number) 345 | ->where('status', QueueNumber::STATUS_CALLING) 346 | ->whereDate('created_at', date('Y-m-d')) 347 | ->first(); 348 | 349 | if (!$queue) { 350 | return json(['code' => 1, 'msg' => '号码不存在或状态错误']); 351 | } 352 | 353 | $queue->status = QueueNumber::STATUS_PASSED; 354 | $queue->save(); 355 | 356 | 357 | try { 358 | Gateway::$registerAddress = '127.0.0.1:1236'; 359 | // 使用Gateway发送状态更新通知 360 | $message = [ 361 | 'type' => 'status_change', 362 | 'data' => [ 363 | 'number' => $number, 364 | 'name' => hideNameCharacters($queue->name), 365 | 'status' => QueueNumber::STATUS_PASSED 366 | ] 367 | ]; 368 | Gateway::sendToUid('display', json_encode($message)); 369 | 370 | 371 | // 使用Gateway发送状态更新通知 372 | $message = [ 373 | 'type' => 'status_change', 374 | 'data' => [ 375 | 'number' => $number, 376 | 'name' => $queue->name, 377 | 'mobile' => $queue->mobile, 378 | 'call_count' => $queue->call_count, 379 | 'window_id' => $queue->window_id, 380 | 'window_name' => optional($queue->window)->name, 381 | 'status' => QueueNumber::STATUS_PASSED 382 | ] 383 | ]; 384 | Gateway::sendToUid('window', json_encode($message)); 385 | 386 | } catch (\Exception $e) { 387 | \support\Log::error($e->getMessage()); 388 | } 389 | 390 | return json(['code' => 0, 'msg' => 'success']); 391 | } 392 | 393 | /** 394 | * 取消号码 395 | */ 396 | public function cancel(Request $request): Response 397 | { 398 | $number = $request->post('number'); 399 | 400 | $queue = QueueNumber::with('window') 401 | ->where('number', $number) 402 | ->whereDate('created_at', date('Y-m-d')) 403 | ->whereIn('status', [QueueNumber::STATUS_CALLING, QueueNumber::STATUS_WAITING, QueueNumber::STATUS_PASSED]) 404 | ->first(); 405 | 406 | if (!$queue) { 407 | return json(['code' => 1, 'msg' => '号码不存在或状态错误']); 408 | } 409 | 410 | $queue->status = QueueNumber::STATUS_CANCELLED; 411 | $queue->save(); 412 | 413 | 414 | try { 415 | Gateway::$registerAddress = '127.0.0.1:1236'; 416 | $message = [ 417 | 'type' => 'status_change', 418 | 'data' => [ 419 | 'number' => $number, 420 | 'name' => hideNameCharacters($queue->name), 421 | 'status' => QueueNumber::STATUS_CANCELLED 422 | ] 423 | ]; 424 | Gateway::sendToUid('display', json_encode($message)); 425 | 426 | $message = [ 427 | 'type' => 'status_change', 428 | 'data' => [ 429 | 'number' => $number, 430 | 'name' => $queue->name, 431 | 'mobile' => $queue->mobile, 432 | 'call_count' => $queue->call_count, 433 | 'window_id' => $queue->window_id, 434 | 'window_name' => optional($queue->window)->name, 435 | 'status' => QueueNumber::STATUS_CANCELLED 436 | ] 437 | ]; 438 | Gateway::sendToUid('window', json_encode($message)); 439 | } catch (\Exception $e) { 440 | \support\Log::error($e->getMessage()); 441 | } 442 | 443 | return json(['code' => 0, 'msg' => 'success']); 444 | } 445 | 446 | /** 447 | * 生成二维码 448 | */ 449 | public function qrcode(Request $request): Response 450 | { 451 | $url = $request->get('url', ''); 452 | 453 | $writer = new PngWriter(); 454 | 455 | $qrCode = new QrCode( 456 | data: $url, 457 | encoding: new Encoding('UTF-8'), 458 | errorCorrectionLevel: ErrorCorrectionLevel::Low, 459 | size: 300, 460 | margin: 10, 461 | roundBlockSizeMode: RoundBlockSizeMode::Margin, 462 | foregroundColor: new Color(0, 0, 0), 463 | backgroundColor: new Color(255, 255, 255) 464 | ); 465 | 466 | $result = $writer->write($qrCode); 467 | 468 | return response($result->getString()) 469 | ->withHeader('Content-Type', 'image/png'); 470 | } 471 | 472 | /** 473 | * 查询号码状态 474 | */ 475 | public function status(Request $request, $number) 476 | { 477 | $queue = QueueNumber::with('window') 478 | ->where('number', $number) 479 | ->whereDate('created_at', date('Y-m-d')) 480 | ->first(); 481 | 482 | //获取待叫号状态时 前面还有多少人数 483 | if ($queue->status == QueueNumber::STATUS_WAITING) { 484 | $queue->waiting = QueueNumber::where('status', QueueNumber::STATUS_WAITING) 485 | ->where('id', '<', $queue->id) 486 | ->whereDate('created_at', date('Y-m-d')) 487 | ->count(); 488 | } 489 | 490 | $badgeClass = $this->getStatusBadgeClass($queue->status); 491 | 492 | return view('queue/status', ['queue' => $queue, 'badgeClass' => $badgeClass]); 493 | } 494 | 495 | /** 496 | * 获取状态对应的徽章样式类 497 | */ 498 | private function getStatusBadgeClass($status): string 499 | { 500 | return match ($status) { 501 | QueueNumber::STATUS_WAITING => 'warning', 502 | QueueNumber::STATUS_CANCELLED => 'secondary', 503 | QueueNumber::STATUS_PASSED => 'danger', 504 | QueueNumber::STATUS_COMPLETED => 'success', 505 | QueueNumber::STATUS_CALLING => 'primary', 506 | default => 'info', 507 | }; 508 | } 509 | } -------------------------------------------------------------------------------- /app/view/queue/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 叫号管理系统 5 | 6 | 7 | 8 | 9 | 10 | 104 | 105 | 106 |
107 | 108 |
109 |
110 |

叫号管理系统

111 | 119 |
120 |
121 | 122 |
123 |
124 |
125 |
126 | 135 |
136 |
137 | 145 |
146 |
147 |
148 |
149 | 150 |
151 |
152 |
手动叫号
153 |
154 |
155 |
156 |
157 | 158 |
159 |
160 | 165 |
166 |
167 | 170 |
171 |
172 |
173 |
174 | 175 |
176 |
177 |
等待队列
178 | total() ?> 条记录 179 |
180 |
181 |
182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 213 | 216 | 226 | 267 | 268 | 269 | 270 |
号码姓名手机号状态叫号次数窗口操作
number ?>name ?>mobile ?> 201 | 210 | getStatusText() ?> 211 | 212 | 214 | call_count ?>次 215 | 217 | 225 | 227 |
228 | status == \app\model\QueueNumber::STATUS_WAITING || $item->status == 229 | \app\model\QueueNumber::STATUS_CALLING): ?> 230 | 239 | 243 | call_count > 0): ?> 244 | 247 | 248 | 251 | 252 | status == \app\model\QueueNumber::STATUS_PASSED): ?> 253 | 257 | 261 | 264 | 265 |
266 |
271 |
272 |
273 | 274 | 351 |
352 |
353 | 354 | 355 | 356 | 357 | 358 | 659 | 660 | --------------------------------------------------------------------------------