├── resources ├── lang │ └── .gitkeep ├── assets │ ├── js │ │ └── app.js │ └── sass │ │ └── app.scss └── views │ ├── layouts │ └── master.blade.php │ ├── commons │ ├── toast.blade.php │ └── head.blade.php │ ├── setting.blade.php │ └── index.blade.php ├── tests ├── Feature │ └── .gitkeep └── Unit │ └── .gitkeep ├── database ├── factories │ └── .gitkeep ├── seeders │ └── DatabaseSeeder.php └── migrations │ ├── init_market_manager_config.php │ └── 0001_01_01_000000_create_apps_table.php ├── app ├── Models │ ├── TempCallbackContent.php │ ├── Traits │ │ ├── IsEnabledTrait.php │ │ └── FsidTrait.php │ ├── AppUsage.php │ ├── AppBadge.php │ ├── Model.php │ └── App.php ├── Http │ ├── Middleware │ │ ├── PluginAuthenticate.php │ │ └── MarketManagerAuthenticate.php │ └── Controllers │ │ ├── MarketManagerController.php │ │ └── MarketManagerApiController.php ├── Utilities │ ├── DataUtility.php │ ├── MarketUtility.php │ ├── StrUtility.php │ ├── PluginUtility.php │ └── DateUtility.php ├── Providers │ ├── ExceptionServiceProvider.php │ ├── CommandServiceProvider.php │ ├── RouteServiceProvider.php │ └── MarketManagerServiceProvider.php ├── Utils │ └── LaravelCache.php ├── MarketManager.php └── Traits │ └── DataTime.php ├── .gitignore ├── webpack.mix.js ├── plugin.json ├── config └── market-manager.php ├── package.json ├── routes ├── api.php └── web.php ├── composer.json └── README.md /resources/lang/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/Feature/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/Unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /database/factories/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/assets/js/app.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/assets/sass/app.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/Models/TempCallbackContent.php: -------------------------------------------------------------------------------- 1 | 'json', 9 | ]; 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public/build 3 | public/hot 4 | public/storage 5 | storage/*.key 6 | vendor 7 | .env 8 | .env.backup 9 | .phpunit.result.cache 10 | Homestead.json 11 | Homestead.yaml 12 | npm-debug.log 13 | yarn-error.log 14 | .idea/ 15 | .vscode/ 16 | *lock 17 | -------------------------------------------------------------------------------- /app/Models/Traits/IsEnabledTrait.php: -------------------------------------------------------------------------------- 1 | where('is_enabled', $isEnabled); 10 | } 11 | } -------------------------------------------------------------------------------- /app/Models/AppUsage.php: -------------------------------------------------------------------------------- 1 | 'json', 11 | ]; 12 | 13 | public function scopeType($query, int $type) 14 | { 15 | return $query->where('usage_type', $type); 16 | } 17 | 18 | public function plugin() 19 | { 20 | return $this->belongsTo(Plugin::class, 'plugin_fskey', 'fskey'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Models/AppBadge.php: -------------------------------------------------------------------------------- 1 | '红点', 12 | AppBadge::TYPE_NUMBER => '数字', 13 | AppBadge::TYPE_TEXT => '文字', 14 | ]; 15 | 16 | use Traits\IsEnabledTrait; 17 | 18 | public function app() 19 | { 20 | return $this->belongsTo(App::class, 'app_fskey', 'fskey'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Http/Middleware/PluginAuthenticate.php: -------------------------------------------------------------------------------- 1 | call("OthersTableSeeder"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const dotenvExpand = require('dotenv-expand'); 2 | dotenvExpand(require('dotenv').config({ path: '../../.env'/*, debug: true*/})); 3 | 4 | const mix = require('laravel-mix'); 5 | require('laravel-mix-merge-manifest'); 6 | 7 | mix.setPublicPath('../../public').mergeManifest(); 8 | 9 | mix.js(__dirname + '/resources/assets/js/app.js', 'assets/plugins/MarketManager/js/market-manager.js') 10 | .sass( __dirname + '/resources/assets/sass/app.scss', 'assets/plugins/MarketManager/css/market-manager.css'); 11 | 12 | if (mix.inProduction()) { 13 | mix.version(); 14 | } 15 | -------------------------------------------------------------------------------- /app/Utilities/DataUtility.php: -------------------------------------------------------------------------------- 1 | format($this->dateFormat ?: 'Y-m-d H:i:s'); 24 | } 25 | } -------------------------------------------------------------------------------- /resources/views/layouts/master.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @include('MarketManager::commons.head', [ 5 | 'title' => 'Plugin MarketManager', 6 | ]) 7 | 8 | {{-- Laravel Mix - CSS File --}} 9 | {{-- --}} 10 | 11 | 12 | 13 |
14 | @yield('content') 15 | 16 | @include('MarketManager::commons.toast') 17 |
18 | 19 | @yield('bodyjs') 20 | 21 | {{-- Laravel Mix - JS File --}} 22 | {{-- --}} 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/Models/App.php: -------------------------------------------------------------------------------- 1 | 'array', 16 | ]; 17 | 18 | public function getPanelUsagesAttribute($value) 19 | { 20 | if (is_string($value)) { 21 | $value = json_decode($value, true); 22 | } 23 | 24 | return $value ?? []; 25 | } 26 | 27 | public function scopeType($query, $value) 28 | { 29 | return $query->where('type', $value); 30 | } 31 | } -------------------------------------------------------------------------------- /config/market-manager.php: -------------------------------------------------------------------------------- 1 | 'MarketManager', 11 | 12 | /* 13 | |-------------------------------------------------------------------------- 14 | | Composer File Template 15 | |-------------------------------------------------------------------------- 16 | | 17 | | YOU COULD CUSTOM HERE 18 | | 19 | */ 20 | 'composer' => [ 21 | 'vendor' => 'plugins-world', 22 | 'author' => [ 23 | [ 24 | 'name' => 'MouYong', 25 | 'email' => 'my24251325@gmail.com', 26 | 'homepage' => 'https://plugins-world.cn/', 27 | 'role' => 'Creator', 28 | ], 29 | ], 30 | ], 31 | ]; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 6 | "watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 7 | "watch-poll": "npm run watch -- --watch-poll", 8 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 9 | "prod": "npm run production", 10 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 11 | }, 12 | "devDependencies": { 13 | "cross-env": "^7.0", 14 | "laravel-mix": "^5.0.1", 15 | "laravel-mix-merge-manifest": "^0.1.2" 16 | } 17 | } -------------------------------------------------------------------------------- /resources/views/commons/toast.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 | 8 | 19 |
20 |
21 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | get('/market-manager', function (Request $request) { 25 | // return $request->user(); 26 | // }); 27 | 28 | Route::prefix('market-manager')->group(function() { 29 | Route::post('/', [ApiController\MarketManagerApiController::class, 'install'])->name('app.install'); 30 | Route::post('app-update', [ApiController\MarketManagerApiController::class, 'update'])->name('app.update'); 31 | Route::post('app-uninstall', [ApiController\MarketManagerApiController::class, 'uninstall'])->name('app.uninstall'); 32 | }); 33 | -------------------------------------------------------------------------------- /app/Models/Traits/FsidTrait.php: -------------------------------------------------------------------------------- 1 | {$model->getFsidKey()} = $model->{$model->getFsidKey()} ?? static::generateFsid(8); 14 | } 15 | }); 16 | } 17 | 18 | // generate fsid 19 | public static function generateFsid($digit): string 20 | { 21 | $fsid = Str::random($digit); 22 | 23 | $checkFsid = static::fsid($fsid)->first(); 24 | 25 | if (! $checkFsid) { 26 | return $fsid; 27 | } else { 28 | $newFsid = Str::random($digit); 29 | $checkNewFsid = static::fsid($newFsid)->first(); 30 | if (! $checkNewFsid) { 31 | return $newFsid; 32 | } 33 | } 34 | 35 | return static::generateFsid($digit + 1); 36 | } 37 | 38 | public function scopeFsid($query, string $fsid): mixed 39 | { 40 | return $query->where($this->getFsidKey(), $fsid); 41 | } 42 | 43 | /** 44 | * fsid 45 | */ 46 | // public function getFsidKey() 47 | // { 48 | // return 'fsid'; 49 | // } 50 | } -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | group(function() { 24 | Route::get('/', [WebController\MarketManagerController::class, 'index'])->name('market-manager.index'); 25 | Route::get('setting', [WebController\MarketManagerController::class, 'showSettingView'])->name('market-manager.setting'); 26 | Route::post('setting', [WebController\MarketManagerController::class, 'saveSetting']); 27 | }); 28 | 29 | // without VerifyCsrfToken 30 | // Route::prefix('market-manager')->withoutMiddleware([ 31 | // \App\Http\Middleware\EncryptCookies::class, 32 | // \App\Http\Middleware\VerifyCsrfToken::class, 33 | // ])->group(function() { 34 | // Route::get('/', [WebController\MarketManagerController::class, 'index']); 35 | // }); -------------------------------------------------------------------------------- /app/Providers/ExceptionServiceProvider.php: -------------------------------------------------------------------------------- 1 | reportable($this->reportable()); 27 | } 28 | 29 | if (method_exists($handler, 'renderable')) { 30 | $handler->renderable($this->renderable()); 31 | } 32 | } 33 | 34 | /** 35 | * Register a reportable callback. 36 | * 37 | * @param callable $reportUsing 38 | * @return \Illuminate\Foundation\Exceptions\ReportableHandler 39 | */ 40 | public function reportable() 41 | { 42 | return function (\Throwable $e) { 43 | // 44 | }; 45 | } 46 | 47 | /** 48 | * Register a renderable callback. 49 | * 50 | * @param callable $renderUsing 51 | * @return $this 52 | */ 53 | public function renderable() 54 | { 55 | return function (\Throwable $e) { 56 | // 57 | }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/Providers/CommandServiceProvider.php: -------------------------------------------------------------------------------- 1 | load($commandsDirectory); 29 | } 30 | } 31 | 32 | /** 33 | * Register all of the commands in the given directory. 34 | * 35 | * @param array|string $paths 36 | * 37 | * @return void 38 | */ 39 | protected function load($paths) 40 | { 41 | $paths = array_unique(Arr::wrap($paths)); 42 | 43 | $paths = array_filter($paths, function ($path) { 44 | return is_dir($path); 45 | }); 46 | 47 | if (empty($paths)) { 48 | return; 49 | } 50 | 51 | $commands = []; 52 | foreach ((new Finder)->in($paths)->files() as $command) { 53 | $commandClass = Str::before(self::class, 'Providers\\') . 'Console\\Commands\\' . str_replace('.php', '', $command->getBasename()); 54 | if (class_exists($commandClass)) { 55 | $commands[] = $commandClass; 56 | } 57 | } 58 | 59 | $this->commands($commands); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugins-world/market-manager", 3 | "description": "MarketManager plugin made by fresns", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Jarvis Tang", 8 | "email": "jarvis.okay@gmail.com", 9 | "homepage": "https://github.com/jarvis-tang", 10 | "role": "Creator" 11 | }, 12 | { 13 | "name": "mouyong", 14 | "email": "my24251325@gmail.com", 15 | "homepage": "https://github.com/mouyong", 16 | "role": "Developer" 17 | } 18 | ], 19 | "autoload": { 20 | "psr-4": { 21 | "Plugins\\MarketManager\\": "app", 22 | "Plugins\\MarketManager\\Database\\Factories\\": "database/factories/", 23 | "Plugins\\MarketManager\\Database\\Seeders\\": "database/seeders/" 24 | } 25 | }, 26 | "autoload-dev": { 27 | "psr-4": { 28 | "Plugins\\MarketManager\\Tests\\": "tests/" 29 | } 30 | }, 31 | "require": { 32 | "php": ">= 8.0", 33 | "plugins-world/laravel-config": "^2.0", 34 | "fresns/cmd-word-manager": "^1.0 | dev-master", 35 | "fresns/market-manager": "^4.0 | 4.x-dev", 36 | "fresns/plugin-manager": "^3.0 | 3.x-dev", 37 | "plugins-world/php-support": "dev-master" 38 | }, 39 | "require-dev": { 40 | "orangehill/iseed": "^3.0" 41 | }, 42 | "extra": { 43 | "laravel": { 44 | "providers": [ 45 | "Plugins\\MarketManager\\Providers\\MarketManagerServiceProvider", 46 | "Plugins\\MarketManager\\Providers\\ExceptionServiceProvider" 47 | ] 48 | } 49 | }, 50 | "repositories": { 51 | "cmd-word-manager": { 52 | "type": "vcs", 53 | "url": "git@gitee.com:fresns/cmd-word-manager.git" 54 | }, 55 | "plugin-manager": { 56 | "type": "vcs", 57 | "url": "git@gitee.com:fresns/plugin-manager.git" 58 | }, 59 | "market-manager": { 60 | "type": "vcs", 61 | "url": "git@gitee.com:fresns/market-manager.git" 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /app/Http/Controllers/MarketManagerController.php: -------------------------------------------------------------------------------- 1 | has('status')) { // 1-active, 0-inactive 25 | $where['is_enabled'] = \request('status') == 'active' ? 1 : 0; 26 | } 27 | 28 | $apps = App::query()->where($where)->get(); 29 | 30 | return view('MarketManager::index', [ 31 | 'configs' => $configs, 32 | 'apps' => $apps, 33 | ]); 34 | } 35 | 36 | public function showSettingView() 37 | { 38 | $itemKeys = [ 39 | 'market_server_host', 40 | 'system_url', 41 | 'settings_path', 42 | 'install_datetime', 43 | 'build_type', 44 | 'github_token', 45 | ]; 46 | 47 | $configs = ConfigHelper::fresnsConfigByItemKeys($itemKeys); 48 | 49 | return view('MarketManager::setting', [ 50 | 'configs' => $configs, 51 | ]); 52 | } 53 | 54 | public function saveSetting() 55 | { 56 | \request()->validate([ 57 | 'market_server_host' => 'required|url', 58 | 'system_url' => 'nullable|url', 59 | 'settings_path' => 'required|string', 60 | 'github_token' => 'nullable|string', 61 | ]); 62 | 63 | $itemKeys = [ 64 | 'market_server_host', 65 | 'system_url', 66 | 'settings_path', 67 | 'github_token', 68 | ]; 69 | 70 | ConfigUtility::updateConfigs($itemKeys, 'market_manager'); 71 | 72 | return redirect(route('market-manager.setting')); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | where('fskey', $fskey)->first(); 47 | 48 | // Cache::put($cacheKey, $pluginModel, now()->addMinutes(30)); 49 | // } 50 | 51 | // $pluginHost = $pluginModel?->plugin_host ?? ''; 52 | 53 | // $host = str_replace(['http://', 'https://'], '', rtrim($pluginHost, '/')); 54 | // } 55 | // } catch (\Throwable $e) { 56 | // info("get plugin host failed: " . $e->getMessage()); 57 | // } 58 | 59 | Route::group([ 60 | 'domain' => $host, 61 | ], function () { 62 | $this->mapApiRoutes(); 63 | 64 | $this->mapWebRoutes(); 65 | }); 66 | } 67 | 68 | /** 69 | * Define the "web" routes for the application. 70 | * 71 | * These routes all receive session state, CSRF protection, etc. 72 | * 73 | * @return void 74 | */ 75 | protected function mapWebRoutes() 76 | { 77 | Route::middleware(['web', 'market-manager.auth']) 78 | ->group(dirname(__DIR__, 2) . '/routes/web.php'); 79 | } 80 | 81 | /** 82 | * Define the "api" routes for the application. 83 | * 84 | * These routes are typically stateless. 85 | * 86 | * @return void 87 | */ 88 | protected function mapApiRoutes() 89 | { 90 | Route::prefix('api', 'market-manager.auth') 91 | ->middleware('api') 92 | ->group(dirname(__DIR__, 2) . '/routes/api.php'); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/Providers/MarketManagerServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 27 | dirname(__DIR__, 2) . '/resources/assets/' => public_path('assets/plugins/MarketManager'), 28 | ]); 29 | 30 | $this->registerTranslations(); 31 | $this->registerConfig(); 32 | $this->registerViews(); 33 | 34 | $this->loadMigrationsFrom(dirname(__DIR__, 2) . '/database/migrations'); 35 | 36 | Route::aliasMiddleware('market-manager.auth', MarketManagerAuthenticate::class); 37 | Route::aliasMiddleware('plugin.auth', PluginAuthenticate::class); 38 | $this->app->register(RouteServiceProvider::class); 39 | 40 | MarketUtility::macroMarketHeaders(); 41 | 42 | // Event::listen(UserCreated::class, UserCreatedListener::class); 43 | } 44 | 45 | /** 46 | * Register the service provider. 47 | * 48 | * @return void 49 | */ 50 | public function register() 51 | { 52 | // if ($this->app->runningInConsole()) { 53 | $this->app->register(CommandServiceProvider::class); 54 | // } 55 | } 56 | 57 | /** 58 | * Register config. 59 | * 60 | * @return void 61 | */ 62 | protected function registerConfig() 63 | { 64 | $this->mergeConfigFrom( 65 | dirname(__DIR__, 2) . '/config/market-manager.php', 'market-manager' 66 | ); 67 | 68 | config([ 69 | 'plugins.composer' => config('market-manager.composer'), 70 | ]); 71 | } 72 | 73 | /** 74 | * Register views. 75 | * 76 | * @return void 77 | */ 78 | public function registerViews() 79 | { 80 | $this->loadViewsFrom(dirname(__DIR__, 2) . '/resources/views', 'MarketManager'); 81 | } 82 | 83 | /** 84 | * Register translations. 85 | * 86 | * @return void 87 | */ 88 | public function registerTranslations() 89 | { 90 | $this->loadTranslationsFrom(dirname(__DIR__, 2) . '/resources/lang', 'MarketManager'); 91 | } 92 | 93 | /** 94 | * Get the services provided by the provider. 95 | * 96 | * @return array 97 | */ 98 | public function provides() 99 | { 100 | return []; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /app/Utilities/MarketUtility.php: -------------------------------------------------------------------------------- 1 | static::version, 19 | 'versionInt' => static::versionInt, 20 | ]; 21 | } 22 | 23 | public static function macroMarketHeaders() 24 | { 25 | Http::macro('market', function () { 26 | $httpProxy = config('app.http_proxy'); 27 | $http = Http::baseUrl(MarketUtility::getApiHost()) 28 | ->withHeaders(MarketUtility::getMarketHeaders()) 29 | ->withOptions([ 30 | 'proxy' => [ 31 | 'http' => $httpProxy, 32 | 'https' => $httpProxy, 33 | ], 34 | ]); 35 | 36 | return $http; 37 | }); 38 | } 39 | 40 | public static function getApiHost() 41 | { 42 | $apiHost = ConfigHelper::fresnsConfigByItemKey('market_server_host'); 43 | 44 | if (!$apiHost) { 45 | return static::defaultMarketHost; 46 | } 47 | 48 | return $apiHost; 49 | } 50 | 51 | public static function getMarketHeaders(): array 52 | { 53 | $appConfig = ConfigHelper::fresnsConfigByItemKeys([ 54 | 'install_datetime', 55 | 'build_type', 56 | 'site_url', 57 | 'site_name', 58 | 'site_desc', 59 | 'site_copyright', 60 | 'default_timezone', 61 | 'default_language', 62 | ]); 63 | 64 | $isHttps = \request()->getScheme() === 'https'; 65 | 66 | $systemUrl = $appConfig['system_url'] ?? config('app.url'); 67 | 68 | config([ 69 | 'app.url' => trim($systemUrl, '/'), 70 | ]); 71 | 72 | return [ 73 | 'panelLangTag' => App::getLocale(), 74 | 'installDatetime' => $appConfig['install_datetime'], 75 | 'buildType' => $appConfig['build_type'], 76 | 'version' => static::currentVersion()['version'], 77 | 'versionInt' => static::currentVersion()['versionInt'], 78 | 'httpSsl' => $isHttps ? 1 : 0, 79 | 'httpHost' => \request()->getHost(), 80 | 'httpPort' => \request()->getPort(), 81 | 'systemUrl' => config('app.url'), 82 | 'siteUrl' => $appConfig['site_url'], 83 | 'siteName' => base64_encode($appConfig['site_name']), 84 | 'siteDesc' => base64_encode($appConfig['site_desc']), 85 | 'siteCopyright' => base64_encode($appConfig['site_copyright']), 86 | 'siteTimezone' => $appConfig['default_timezone'], 87 | 'siteLanguage' => $appConfig['default_language'], 88 | ]; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/Utils/LaravelCache.php: -------------------------------------------------------------------------------- 1 | LaravelCache::NULL_KEY_NUM) { 42 | return null; 43 | } 44 | 45 | // 使用默认缓存时间 46 | if (is_callable($cacheTime)) { 47 | $callable = $cacheTime; 48 | 49 | // 防止缓存雪崩,对不同数据随机缓存时间。从半小时到 1天 50 | $index = rand(0, 100) % count(LaravelCache::DEFAULT_CACHE_TIME); 51 | $cacheSeconds = LaravelCache::DEFAULT_CACHE_TIME[$index]; 52 | $cacheTime = now()->addSeconds($cacheSeconds); 53 | } 54 | 55 | if (!is_callable($callable)) { 56 | return null; 57 | } 58 | 59 | if ($forever) { 60 | $data = Cache::rememberForever($cacheKey, fn () => $callable($cacheTime, $cacheKey)); 61 | } else { 62 | $data = Cache::remember($cacheKey, $cacheTime, fn() => $callable($cacheTime, $cacheKey)); 63 | } 64 | 65 | if (!$data) { 66 | Cache::pull($cacheKey); 67 | 68 | $currentCacheKeyNullNum = (int) Cache::get($nullCacheKey); 69 | 70 | Cache::put($nullCacheKey, ++$currentCacheKeyNullNum, now()->addSeconds(LaravelCache::NULL_KEY_CACHE_TIME)); 71 | } 72 | 73 | return $data; 74 | } 75 | 76 | /** 77 | * 执行指定函数并永久缓存 78 | * 79 | * @param string $cacheKey 80 | * @param callable|Carbon|null $cacheTime 81 | * @param callable|null $callable 82 | * @return mixed 83 | */ 84 | public static function rememberForever(string $cacheKey, callable|Carbon|null $cacheTime = null, ?callable $callable = null) 85 | { 86 | return LaravelCache::remember($cacheKey, $cacheTime, $callable, true); 87 | } 88 | 89 | /** 90 | * 转发调用 91 | * 92 | * @param mixed $method 93 | * @param mixed $args 94 | * @return mixed 95 | */ 96 | public static function __callStatic(mixed $method, mixed $args) 97 | { 98 | return Cache::$method(...$args); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /resources/views/setting.blade.php: -------------------------------------------------------------------------------- 1 | @extends('MarketManager::layouts.master') 2 | 3 | @section('content') 4 |
5 |
6 |
7 |

Market Manage 设置

8 | 9 |
10 | @csrf 11 | 12 |
13 | 14 |
15 | 16 |
17 |
18 | 19 |
20 | 21 |
22 | 23 |
24 |
25 | 26 |
27 | 28 |
29 | 30 |
31 |
32 | 33 |
34 | 35 |
36 | 37 |
38 |
39 | 40 |
41 | 42 |
43 | 44 |
45 |
46 | 47 |
48 | 49 |
50 | 51 |
52 |
53 | 54 | 55 |
56 |
57 |
58 |
59 | @endsection -------------------------------------------------------------------------------- /app/MarketManager.php: -------------------------------------------------------------------------------- 1 | checkAuth([ 39 | 'request' => $request, 40 | 'next' => $next, 41 | ]); 42 | 43 | return $resp->isSuccessResponse(); 44 | } 45 | } 46 | 47 | if (app()->environment(['local', 'develop'])) { 48 | return $next($request); 49 | } 50 | 51 | return abort(403); 52 | })($request, $next); 53 | } 54 | 55 | /** 56 | * Determine if the given request can access the MarketManager dashboard. 57 | * 58 | * @param \Illuminate\Http\Request $request 59 | * @return bool 60 | */ 61 | public static function checkPluginAuth($request, $next) 62 | { 63 | return (static::$pluginAuthUsing ?: function ($request, $next) { 64 | $pluginsCmds = \FresnsCmdWord::all(); 65 | if (array_key_exists('Manager', $pluginsCmds)) { 66 | $marketManagerCmds = $pluginsCmds['Manager']; 67 | 68 | if (array_key_exists('checkPluginAuth', $marketManagerCmds)) { 69 | /** @var \Fresns\CmdWordManager\CmdWordResponse */ 70 | $resp = \FresnsCmdWord::plugin('Manager')->checkPluginAuth([ 71 | 'request' => $request, 72 | 'next' => $next, 73 | ]); 74 | 75 | return $resp->isSuccessResponse(); 76 | } 77 | } 78 | 79 | return $next($request); 80 | })($request, $next); 81 | } 82 | 83 | /** 84 | * Set the callback that should be used to authenticate MarketManager users. 85 | * 86 | * @param \Closure $callback 87 | * @return static 88 | */ 89 | public static function auth(Closure $callback) 90 | { 91 | static::$authUsing = $callback; 92 | 93 | return new static; 94 | } 95 | 96 | /** 97 | * Set the callback that should be used to authenticate MarketManager users. 98 | * 99 | * @param \Closure $callback 100 | * @return static 101 | */ 102 | public static function pluginAuth(Closure $callback) 103 | { 104 | static::$pluginAuthUsing = $callback; 105 | 106 | return new static; 107 | } 108 | } -------------------------------------------------------------------------------- /app/Utilities/StrUtility.php: -------------------------------------------------------------------------------- 1 | str_repeat('*', 3), 23 | $len > 3 => str_repeat('*', bcsub($len, 3)), 24 | }; 25 | 26 | $offset = match (true) { 27 | default => 1, 28 | $len > 3 => 3, 29 | }; 30 | 31 | $maskUser = substr_replace($user, $mask, $offset); 32 | 33 | return "{$maskUser}{$domain}"; 34 | } 35 | 36 | // number 37 | public static function maskNumber(?int $number = null): ?string 38 | { 39 | if (empty($number)) { 40 | return null; 41 | } 42 | 43 | $len = mb_strlen($number); 44 | if ($len <= 4) { 45 | return $number; 46 | } 47 | 48 | $head = substr($number, 0, 2); 49 | $tail = substr($number, -2); 50 | $starCount = strlen($number) - 4; 51 | $star = str_repeat('*', $starCount); 52 | 53 | return $head.$star.$tail; 54 | } 55 | 56 | // name 57 | public static function maskName(?string $name = null): ?string 58 | { 59 | if (empty($name)) { 60 | return null; 61 | } 62 | 63 | $len = mb_strlen($name); 64 | if ($len < 1) { 65 | return $name; 66 | } 67 | 68 | $last = mb_substr($name, -1, 1); 69 | $lastName = str_repeat('*', $len - 1); 70 | 71 | return $lastName.$last; 72 | } 73 | 74 | // qualify table name 75 | public static function qualifyTableName(mixed $model): string 76 | { 77 | $modelName = $model; 78 | 79 | if (class_exists($model)) { 80 | $model = new $model; 81 | 82 | if (! ($model instanceof Model)) { 83 | throw new \LogicException("unknown table name of $model"); 84 | } 85 | 86 | $modelName = $model->getTable(); 87 | } 88 | 89 | return str_replace(config('database.connections.mysql.prefix'), '', $modelName); 90 | } 91 | 92 | // qualify url 93 | public static function qualifyUrl(?string $uri = null, ?string $domain = null): ?string 94 | { 95 | if (empty($uri)) { 96 | return null; 97 | } 98 | 99 | if (str_contains($uri, '://')) { 100 | return $uri; 101 | } 102 | 103 | if (! $domain) { 104 | return sprintf('%s/%s', config('app.url'), ltrim($uri, '/')); 105 | } 106 | 107 | return sprintf('%s/%s', rtrim($domain, '/'), ltrim($uri, '/')); 108 | } 109 | 110 | // Whether it is a pure number 111 | public static function isPureInt(mixed $variable): bool 112 | { 113 | return preg_match('/^\d+?$/', $variable); 114 | } 115 | 116 | public static function slug(?string $text): ?string 117 | { 118 | if (!$text) { 119 | return null; 120 | } 121 | 122 | if (preg_match("/^[A-Za-z\s]+$/", $text)) { 123 | $slug = Str::slug($text, '-'); 124 | } else { 125 | $slug = rawurlencode($text); 126 | } 127 | 128 | $slug = Str::lower($slug); 129 | 130 | return $slug; 131 | } 132 | } -------------------------------------------------------------------------------- /database/migrations/init_market_manager_config.php: -------------------------------------------------------------------------------- 1 | SubscribeUtility::TYPE_USER_ACTIVITY, 20 | // 'fskey' => 'market_manager', 21 | // 'cmdWord' => 'stats', 22 | ]; 23 | 24 | protected $fresnsConfigItems = [ 25 | // [ 26 | // 'item_tag' => 'market_manager', 27 | // 'item_key' => 'access_key', 28 | // 'item_type' => 'string', 29 | // 'item_value' => null, 30 | // ], 31 | [ 32 | 'item_tag' => 'market_manager', 33 | 'item_key' => 'market_server_host', 34 | 'item_type' => 'string', 35 | 'item_value' => 'https://marketplace.plugins-world.cn', 36 | ], 37 | [ 38 | 'item_tag' => 'market_manager', 39 | 'item_key' => 'system_url', 40 | 'item_type' => 'string', 41 | 'item_value' => null, 42 | ], 43 | [ 44 | 'item_tag' => 'market_manager', 45 | 'item_key' => 'settings_path', 46 | 'item_type' => 'string', 47 | 'item_value' => null, 48 | ], 49 | [ 50 | 'item_tag' => 'market_manager', 51 | 'item_key' => 'install_datetime', 52 | 'item_type' => 'string', 53 | 'item_value' => null, 54 | ], 55 | [ 56 | 'item_tag' => 'market_manager', 57 | 'item_key' => 'build_type', 58 | 'item_type' => 'number', 59 | 'item_value' => 1, 60 | ], 61 | [ 62 | 'item_tag' => 'market_manager', 63 | 'item_key' => 'github_token', 64 | 'item_type' => 'string', 65 | 'item_value' => null, 66 | ], 67 | ]; 68 | 69 | /** 70 | * Run the migrations. 71 | */ 72 | public function up(): void 73 | { 74 | foreach ($this->fresnsConfigItems as $index => $configItem) { 75 | if ($configItem['item_key'] == 'install_datetime') { 76 | $configItem['item_value'] = date('Y-m-d H:i:s'); 77 | } 78 | 79 | if ($configItem['item_key'] == 'settings_path') { 80 | $contents = file_get_contents(dirname(__DIR__, 2) . '/plugin.json'); 81 | $data = json_decode($contents, true); 82 | 83 | $configItem['item_value'] = $data['settingsPath']; 84 | } 85 | 86 | $this->fresnsConfigItems[$index] = $configItem; 87 | } 88 | 89 | // addSubscribeItem 90 | // \FresnsCmdWord::plugin()->addSubscribeItem($this->fresnsWordBody); 91 | 92 | // addKeyValues to Config table 93 | ConfigUtility::addFresnsConfigItems($this->fresnsConfigItems); 94 | } 95 | 96 | /** 97 | * Reverse the migrations. 98 | */ 99 | public function down(): void 100 | { 101 | // removeSubscribeItem 102 | // \FresnsCmdWord::plugin()->removeSubscribeItem($this->fresnsWordBody); 103 | 104 | // removeKeyValues from Config table 105 | ConfigUtility::removeFresnsConfigItems($this->fresnsConfigItems); 106 | } 107 | }; 108 | -------------------------------------------------------------------------------- /resources/views/commons/head.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ $title ?? '' }} 7 | 8 | @stack('headcss') 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 28 | 29 | 100 | 101 | @stack('headjs') 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 插件管理器 2 | 3 | [![Latest Stable Version](http://poser.pugx.org/plugins-world/market-manager/v)](https://packagist.org/packages/plugins-world/market-manager) 4 | [![Total Downloads](http://poser.pugx.org/plugins-world/market-manager/downloads)](https://packagist.org/packages/plugins-world/market-manager) 5 | [![Latest Unstable Version](http://poser.pugx.org/plugins-world/market-manager/v/unstable)](https://packagist.org/packages/plugins-world/market-manager) [![License](http://poser.pugx.org/plugins-world/market-manager/license)](https://packagist.org/packages/plugins-world/market-manager) 6 | [![PHP Version Require](http://poser.pugx.org/plugins-world/market-manager/require/php)](https://packagist.org/packages/plugins-world/market-manager) 7 | 8 | - 应用市场:https://marketplace.plugins-world.cn 9 | 10 | 11 | ## 安装 12 | 13 | 你可以通过 composer 安装这个扩展包,与应用插件不同的是,此扩展会安装到 `vendor/` 目录下。 14 | 15 | ⚠️注意,安装的时候,会询问是以下内容,请输入: `y` 16 | > wikimedia/composer-merge-plugin contains a Composer plugin which is currently not in your allow-plugins config. See https://getcomposer.org/allow-plugins 17 | > 18 | > Do you trust "wikimedia/composer-merge-plugin" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [y,n,d,?] 19 | 20 | 21 | 下面是初始化项目并引入插件管理器的操作步骤: 22 | 23 | 1. 创建项目 24 | ```bash 25 | # 创建新项目 laravel-test 26 | composer create-project --prefer-dist laravel/laravel laravel-test 27 | # 进入项目目录 28 | cd laravel-test 29 | # 初始化 git 仓库 30 | git init 31 | git add . 32 | git commit -m "feat: Init." 33 | # 配置应用市场管理器, 插件管理器, 命令字管理器的安装源(此步骤仅在需要最新管理器功能时配置) 34 | # composer config repositories.plugin-manager vcs https://gitee.com/fresns/plugin-manager 35 | # composer config repositories.market-manager vcs https://gitee.com/fresns/market-manager 36 | # composer config repositories.cmd-word-manager vcs https://gitee.com/fresns/cmd-word-manager 37 | ``` 38 | 39 | 2. 修改依赖包约束 40 | ⚠️注意:需要确保项目 `composer.json` 允许安装稳定性依赖为 `dev` 的扩展包 41 | ```js 42 | { 43 | ... 44 | "minimum-stability": "dev", 45 | "prefer-stable": true, 46 | ... 47 | } 48 | ``` 49 | 50 | 3. 安装插件管理器,并完成初始化。 51 | ```bash 52 | # 安装 Laravel 的应用市场管理器 53 | composer require plugins-world/market-manager 54 | # 配置 .env 中的数据库与项目信息 55 | APP_NAME= 56 | APP_URL= 57 | 58 | DB_HOST= 59 | DB_DATABASE= 60 | DB_USERNAME= 61 | DB_PASSWORD= 62 | 63 | # 执行迁移,增加 plugins 表 64 | php artisan migrate 65 | 66 | # 提交仓库变动。方便查看 saas 初始化的文件 67 | git add . 68 | git commit -m "feat: Install laravel market-manager." 69 | ``` 70 | 71 | 4. 正确配置项目权限 72 | 宝塔:`chown www:www -R /path/to/laravel-test` 73 | 74 | 5. 访问:`http://域名/market-manager`,查看安装结果 75 | 76 | 77 | ## 挑选插件 78 | 79 | 访问 `http://域名/market-manager`,打开左侧的插件市场菜单,并在其中查找需要的插件。进入插件详情页后点击安装。 80 | 81 | ⚠️注意:独立打开插件市场,不会显示安装按钮。 82 | 83 | 84 | ## 插件管理页的访问限制 85 | 86 | ⚠️注意: 87 | - MarketManager 默认只允许 `local` 与 `develop` 环境访问。 88 | - Plugin 默认全部放行访问。 89 | - 如果需要限制访问权限,可以在 `app/Providers/AppServiceProvider.php` 的 `boot` 函数中,通过指定 MarketManager 如何进行认证来完成限制,参考如下: 90 | 91 | 92 | 操作步骤: 93 | 1. 安装 SanctumAuth 插件 94 | 2. 通过 artisan 命令 app:user-add 创建一个初始账号 95 | 3. 正确配置主程序。下面是配置参考 96 | 97 | - 通过 `AppServiceProvider` 授权 98 | ```php 99 | \Plugins\MarketManager\MarketManager::auth(function ($request, $next) { 100 | // return \Illuminate\Support\Facades\Auth::onceBasic() ?: $next($request); 101 | }); 102 | 103 | 104 | \Plugins\MarketManager\MarketManager::pluginAuth(function ($request, $next) { 105 | // return \Illuminate\Support\Facades\Auth::onceBasic() ?: $next($request); 106 | }); 107 | 108 | # 配置首页默认路由,并进行 basic 认证(需要在数据库创建 users 信息,通过 email, password 登录) 109 | Route::domain(parse_url(config('app.url'), PHP_URL_HOST))->get('/', function () { 110 | return redirect('/market-manager'); 111 | return view('welcome'); 112 | })->middleware('auth.basic'); 113 | ``` 114 | 115 | - 通过命令字 `\FresnsCmdWord::plugin('Manager')->checkAuth([])` 授权 MarketManager 访问,需要自行实现 `Manager` 的 `checkAuth` 命令字。 116 | - 通过命令字 `\FresnsCmdWord::plugin('Manager')->checkPluginAuth([])` 授权 Plugin 访问,需要自行实现 `Manager` 的 `checkPluginAuth` 命令字。 117 | 118 | 119 | ## 说明 120 | 121 | 1. 符合插件管理器开发规范的插件可以被安装 122 | 2. 插件安装方式: 123 | 1. 从 url 安装 zip 插件 124 | 2. 从 github url 安装私有插件 125 | 3. 从 github url 安装公开插件 126 | 4. 从 下载站获取 url 安装插件:https://apps.plugins-world.cn 127 | 5. 从 https://packagist.org 通过安命令完成插件安装 128 | 6. 上传 zip 插件到服务器进行安装 129 | 7. 从指定目录安装 130 | 8. 从插件市场安装插件(开发中) 131 | 3. 目前,官方插件代码仓库为:https://github.com/plugins-world/plugins 132 | 4. 项目需要配置好权限,避免插件无法下载,解压,安装。插件的安装需要 web 程序的用户读取、创建目录。 133 | 5. 每次安装后,插件默认关闭,需要进行启用操作。 134 | 135 | 136 | ## 遇到问题 137 | 138 | 通过此处联系我:https://plugins-world.cn/contributing/feedback.html 139 | -------------------------------------------------------------------------------- /app/Utilities/PluginUtility.php: -------------------------------------------------------------------------------- 1 | value('plugin_host'); 28 | }); 29 | } 30 | 31 | // Get the plugin access url 32 | public static function fresnsPluginUrlByFskey(?string $fskey = null): ?string 33 | { 34 | if (empty($fskey)) { 35 | return null; 36 | } 37 | 38 | $cacheKey = "fresns_plugin_url_{$fskey}"; 39 | 40 | return LaravelCache::remember($cacheKey, function () use ($fskey) { 41 | $plugin = App::where('fskey', $fskey)->first(); 42 | 43 | $pluginUrl = null; 44 | if ($plugin) { 45 | $url = empty($plugin->plugin_host) ? config('app.url') : $plugin->plugin_host; 46 | 47 | $pluginUrl = StrUtility::qualifyUrl($plugin->access_path, $url); 48 | } 49 | 50 | return $pluginUrl; 51 | }); 52 | } 53 | 54 | // Get the url of the plugin that has replaced the custom parameters 55 | public static function fresnsPluginUsageUrl(string $fskey, ?string $parameter = null): ?string 56 | { 57 | $url = PluginUtility::fresnsPluginUrlByFskey($fskey); 58 | 59 | if (empty($parameter) || empty($url)) { 60 | return $url; 61 | } 62 | 63 | return str_replace('{parameter}', $parameter, $url); 64 | } 65 | 66 | // get plugin callback 67 | public static function fresnsPluginCallback(string $fskey, string $ulid): array 68 | { 69 | $callbackArr = [ 70 | 'code' => 0, 71 | 'data' => [], 72 | ]; 73 | 74 | $plugin = App::where('fskey', $fskey)->first(); 75 | 76 | if (empty($plugin)) { 77 | $callbackArr['code'] = 32101; 78 | 79 | return $callbackArr; 80 | } 81 | 82 | if (! $plugin->is_enabled) { 83 | $callbackArr['code'] = 32102; 84 | 85 | return $callbackArr; 86 | } 87 | 88 | $callback = TempCallbackContent::where('ulid', $ulid)->first(); 89 | 90 | if (empty($callback)) { 91 | $callbackArr['code'] = 32303; 92 | 93 | return $callbackArr; 94 | } 95 | 96 | if ($callback->is_used) { 97 | $callbackArr['code'] = 32204; 98 | 99 | return $callbackArr; 100 | } 101 | 102 | if (empty($callback->content)) { 103 | $callbackArr['code'] = 32206; 104 | 105 | return $callbackArr; 106 | } 107 | 108 | $timeDifference = time() - strtotime($callback->created_at); 109 | // 30 minutes 110 | if ($timeDifference > 1800) { 111 | $callbackArr['code'] = 32203; 112 | 113 | return $callbackArr; 114 | } 115 | 116 | $callback->is_used = 1; 117 | $callback->used_plugin_fskey = $fskey; 118 | $callback->save(); 119 | 120 | $data = [ 121 | 'ulid' => $callback->ulid, 122 | 'type' => $callback->type, 123 | 'content' => $callback->content, 124 | ]; 125 | 126 | $callbackArr['data'] = $data; 127 | 128 | return $callbackArr; 129 | } 130 | 131 | // get plugin version 132 | public static function fresnsPluginVersionByFskey(string $fskey): ?string 133 | { 134 | $cacheKey = "fresns_plugin_version_{$fskey}"; 135 | 136 | $version = LaravelCache::remember($cacheKey, function () use ($fskey) { 137 | return App::where('fskey', $fskey)->value('version'); 138 | }); 139 | 140 | return $version; 141 | } 142 | 143 | // get plugin upgrade code 144 | public static function fresnsPluginUpgradeCodeByFskey(string $fskey): ?string 145 | { 146 | $upgradeCode = App::where('fskey', $fskey)->value('upgrade_code'); 147 | 148 | if (empty($upgradeCode)) { 149 | return null; 150 | } 151 | 152 | return $upgradeCode; 153 | } 154 | } -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000000_create_apps_table.php: -------------------------------------------------------------------------------- 1 | comment('应用表'); 17 | 18 | $table->integer('id', true); 19 | $table->string('fskey', 64)->unique('plugin_fskey'); 20 | $table->unsignedTinyInteger('type')->default(1); 21 | $table->string('name', 64); 22 | $table->string('description'); 23 | $table->string('version', 16); 24 | $table->string('author', 64); 25 | $table->string('author_link', 128)->nullable(); 26 | $table->json('panel_usages')->nullable(); 27 | $table->string('app_host', 128)->nullable(); 28 | $table->string('access_path')->nullable(); 29 | $table->string('settings_path', 128)->nullable(); 30 | $table->boolean('is_upgrade')->default(0); 31 | $table->string('upgrade_code', 32)->nullable(); 32 | $table->string('upgrade_version', 16)->nullable(); 33 | $table->unsignedTinyInteger('is_enabled')->default(0); 34 | $table->timestamp('created_at')->useCurrent(); 35 | $table->timestamp('updated_at')->nullable(); 36 | $table->softDeletes(); 37 | }); 38 | 39 | Schema::create('app_usages', function (Blueprint $table) { 40 | $table->comment('插件关联使用表'); 41 | 42 | $table->increments('id'); 43 | $table->unsignedTinyInteger('usage_type')->index('app_usage_type'); 44 | $table->string('app_fskey', 64); 45 | $table->string('name', 128); 46 | $table->unsignedBigInteger('icon_file_id')->nullable(); 47 | $table->string('icon_file_url')->nullable(); 48 | $table->string('scene', 16)->nullable(); 49 | $table->unsignedTinyInteger('editor_toolbar')->default(0); 50 | $table->unsignedTinyInteger('editor_number')->nullable(); 51 | $table->unsignedTinyInteger('is_group_admin')->nullable()->default(0); 52 | $table->unsignedInteger('group_id')->nullable()->index('plugin_usage_group_id'); 53 | $table->string('roles', 128)->nullable(); 54 | $table->string('parameter', 128)->nullable(); 55 | $table->unsignedSmallInteger('sort_order')->default(9); 56 | $table->unsignedTinyInteger('can_delete')->default(1); 57 | $table->unsignedTinyInteger('is_enabled')->default(1); 58 | $table->timestamp('created_at')->useCurrent(); 59 | $table->timestamp('updated_at')->nullable(); 60 | $table->softDeletes(); 61 | }); 62 | 63 | Schema::create('app_badges', function (Blueprint $table) { 64 | $table->comment('插件徽标数据表'); 65 | 66 | $table->bigIncrements('id'); 67 | $table->string('app_fskey', 64); 68 | $table->unsignedBigInteger('user_id'); 69 | $table->unsignedTinyInteger('display_type')->default(1)->comment('1.红点 / 2.数字 / 3.文字'); 70 | $table->unsignedSmallInteger('value_number')->nullable(); 71 | $table->string('value_text', 8)->nullable(); 72 | $table->timestamp('created_at')->useCurrent(); 73 | $table->timestamp('updated_at')->nullable(); 74 | $table->softDeletes(); 75 | 76 | $table->unique(['app_fskey', 'user_id'], 'app_badge_user_id'); 77 | }); 78 | 79 | Schema::create('temp_callback_contents', function (Blueprint $table) { 80 | $table->comment('回调内容表'); 81 | 82 | $table->bigIncrements('id'); 83 | $table->string('app_fskey', 64); 84 | $table->string('key', 64); 85 | $table->unsignedSmallInteger('type')->default(1); 86 | switch (config('database.default')) { 87 | case 'pgsql': 88 | $table->jsonb('content')->nullable(); 89 | break; 90 | 91 | case 'sqlsrv': 92 | $table->nvarchar('content', 'max')->nullable(); 93 | break; 94 | 95 | default: 96 | $table->json('content')->nullable(); 97 | } 98 | $table->unsignedTinyInteger('retention_days')->default(1); 99 | $table->unsignedTinyInteger('is_enabled')->default(0); 100 | $table->timestamp('created_at')->useCurrent(); 101 | $table->timestamp('updated_at')->nullable(); 102 | $table->softDeletes(); 103 | }); 104 | } 105 | 106 | /** 107 | * Reverse the migrations. 108 | */ 109 | public function down(): void 110 | { 111 | Schema::dropIfExists('apps'); 112 | Schema::dropIfExists('app_usages'); 113 | Schema::dropIfExists('app_badges'); 114 | Schema::dropIfExists('temp_callback_contents'); 115 | } 116 | }; 117 | -------------------------------------------------------------------------------- /app/Http/Controllers/MarketManagerApiController.php: -------------------------------------------------------------------------------- 1 | validate([ 25 | 'install_type' => 'nullable', // plugin, theme 26 | 'install_method' => 'required|in:app_fskey,app_package,app_url,app_directory,app_zipball', 27 | 28 | 'app_fskey' => 'required_if:install_method,app_fskey', 29 | 'app_package' => 'required_if:install_method,app_package', 30 | 'app_url' => 'required_if:install_method,app_url', 31 | 'app_directory' => 'required_if:install_method,app_directory', 32 | 'app_zipball' => 'required_if:install_method,app_zipball', 33 | ]); 34 | 35 | $install_type = \request('install_type', 'plugin'); 36 | $install_method = \request('install_method'); 37 | $installValue = \request($install_method); 38 | 39 | switch ($install_method) { 40 | // fskey 41 | case 'app_fskey': 42 | case 'app_package': 43 | case 'app_url': 44 | if ($install_method == 'app_url') { 45 | $configs = ConfigHelper::fresnsConfigByItemKeys([ 46 | 'github_token', 47 | ]); 48 | 49 | // 下载 github 私有插件 50 | if (str_starts_with($installValue, 'https://github.com')) { 51 | $installValue = str_replace('https://', 'https://' . $configs['github_token'] . ':@', $installValue); 52 | 53 | if (!str_ends_with($installValue, 'zip')) { 54 | $installValue = $installValue . '/archive/master.zip'; 55 | } 56 | } 57 | } 58 | 59 | // market-manager 60 | $exitCode = Artisan::call('market:require', [ 61 | 'fskey' => $installValue, 62 | ]); 63 | $output = Artisan::output(); 64 | break; 65 | 66 | // directory 67 | case 'app_directory': 68 | $pluginDirectory = $installValue; 69 | 70 | // plugin-manager or theme-manager 71 | $exitCode = Artisan::call("{$install_type}:install", [ 72 | 'path' => $pluginDirectory, 73 | '--is_dir' => true, 74 | ]); 75 | $output = Artisan::output(); 76 | break; 77 | 78 | // app_zipball 79 | case 'app_zipball': 80 | $pluginZipball = null; 81 | $file = $installValue; 82 | 83 | if ($file && $file->isValid()) { 84 | $dir = storage_path('extensions'); 85 | $filename = $file->hashName(); 86 | $file->move($dir, $filename); 87 | 88 | $pluginZipball = "$dir/$filename"; 89 | } 90 | 91 | if (empty($pluginZipball)) { 92 | return $this->fail('插件安装失败,请选择插件压缩包'); 93 | } 94 | 95 | // plugin-manager or theme-manager 96 | $exitCode = Artisan::call("{$install_type}:install", [ 97 | 'path' => $pluginZipball, 98 | ]); 99 | $output = Artisan::output(); 100 | break; 101 | } 102 | 103 | if ($exitCode != 0) { 104 | if ($output == '') { 105 | $output = "请查看安装日志 storage/logs/laravel.log"; 106 | } 107 | 108 | return \response($output . "\n 安装失败"); 109 | } 110 | 111 | return \response($output . "\n 安装成功"); 112 | } 113 | 114 | public function update() 115 | { 116 | \request()->validate([ 117 | 'plugin' => 'required|string', 118 | 'is_enabled' => 'required|boolean' 119 | ]); 120 | 121 | $fskey = \request('plugin'); 122 | 123 | if (\request()->get('is_enabled') != 0) { 124 | $exitCode = Artisan::call('plugin:activate', ['fskey' => $fskey]); 125 | } else { 126 | $exitCode = Artisan::call('plugin:deactivate', ['fskey' => $fskey]); 127 | } 128 | 129 | if ($exitCode !== 0) { 130 | return $this->fail(Artisan::output()); 131 | } 132 | 133 | $app = App::where('fskey', $fskey)->first(); 134 | if ($app) { 135 | $app->update([ 136 | 'is_enabled' => !$app->is_enabled, 137 | ]); 138 | } 139 | 140 | return \response()->json([ 141 | 'err_code' => 0, 142 | 'err_msg' => 'success', 143 | 'data' => null, 144 | ]); 145 | } 146 | 147 | public function uninstall() 148 | { 149 | \request()->validate([ 150 | 'plugin' => 'required|string', 151 | 'clearData' => 'nullable|bool', 152 | ]); 153 | 154 | $fskey = \request('plugin'); 155 | if (\request()->get('clearData') == 1) { 156 | $exitCode = Artisan::call('plugin:uninstall', ['fskey' => $fskey, '--cleardata' => true]); 157 | } else { 158 | $exitCode = Artisan::call('plugin:uninstall', ['fskey' => $fskey, '--cleardata' => false]); 159 | } 160 | 161 | if ($exitCode == 0) { 162 | App::where('fskey', $fskey)->delete(); 163 | } 164 | 165 | $message = '卸载成功'; 166 | if ($exitCode != 0) { 167 | $message = Artisan::output() . "\n卸载失败"; 168 | } 169 | 170 | return \response($message); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /app/Traits/DataTime.php: -------------------------------------------------------------------------------- 1 | 'feature', 24 | 'feature' => 'feature', 25 | 'past' => 'past', 26 | }; 27 | 28 | // 获取当前日期 29 | $currentDate = new \DateTime(); 30 | 31 | // 循环获取接下来的一周日期 32 | $data = []; 33 | if ($direction == 'past') { 34 | if ($excludeToday) { 35 | $subDayNum = $dayNum; 36 | } else { 37 | $subDayNum = $dayNum - 1; 38 | } 39 | 40 | $dateInterval = new \DateInterval('P' . $subDayNum . 'D'); // 创建一个新的日期间隔 41 | $currentDate = $currentDate->sub($dateInterval); 42 | } else { 43 | if ($excludeToday) { 44 | $addDayNum = 1; 45 | } else { 46 | $addDayNum = 0; 47 | } 48 | 49 | $dateInterval = new \DateInterval('P' . $addDayNum . 'D'); // 创建一个新的日期间隔 50 | $currentDate = $currentDate->add($dateInterval); 51 | } 52 | 53 | for ($i = 0; $i < $dayNum; $i++) { 54 | $dateInterval = new \DateInterval('P' . $i . 'D'); // 创建一个新的日期间隔 55 | $day = $currentDate->add($dateInterval); // 获取未来的日期 56 | 57 | $item = DateUtility::getDateInfo($day->format('Y-m-d')); 58 | if (!$item) { 59 | continue; 60 | } 61 | 62 | // 为下一次迭代重设$currentDate 63 | $currentDate = $currentDate->sub($dateInterval); 64 | 65 | $data[] = $item; 66 | } 67 | 68 | // 按日期排序 69 | array_multisort(array_column($data, 'date'), SORT_ASC, $data); 70 | 71 | return $data; 72 | } 73 | 74 | public static function getDateInfo(?string $date) 75 | { 76 | if (!$date) { 77 | return null; 78 | } 79 | 80 | $day = new \DateTime($date); 81 | 82 | $yesterdayDateStr = date('Y-m-d', strtotime('yesterday')); 83 | $currentDateStr = date('Y-m-d'); 84 | $tomorrowDateStr = date('Y-m-d', strtotime('tomorrow')); 85 | 86 | $item['is_yesterday_day'] = false; 87 | $item['is_today'] = false; 88 | $item['is_tomorrow_day'] = false; 89 | $item['date_desc'] = ''; 90 | if ($day->format('Y-m-d') == $yesterdayDateStr) { 91 | $item['is_yesterday_day'] = true; 92 | $item['date_desc'] = '昨天'; 93 | } 94 | if ($day->format('Y-m-d') == $currentDateStr) { 95 | $item['is_today'] = true; 96 | $item['date_desc'] = '今天'; 97 | } 98 | if ($day->format('Y-m-d') == $tomorrowDateStr) { 99 | $item['is_tomorrow_day'] = true; 100 | $item['date_desc'] = '明天'; 101 | } 102 | $item['date'] = $day->format('Y-m-d'); 103 | $item['day'] = DateUtility::getDateDay($item['date']); 104 | $item['zhou'] = DateUtility::getChineseWeekday($item['day']); 105 | $item['xingqi'] = str_replace('周', '星期', $item['zhou']); 106 | 107 | return $item; 108 | } 109 | 110 | // 定义一个函数将星期数字转化为中文 111 | public static function getChineseWeekday($weekdayNum) 112 | { 113 | $weekdayNumIndex = $weekdayNum - 1; 114 | 115 | $weekdays = array("周一", "周二", "周三", "周四", "周五", "周六", "周日"); 116 | return $weekdays[$weekdayNumIndex]; 117 | } 118 | 119 | public static function getDateDay(?string $date) 120 | { 121 | if (!$date) { 122 | return null; 123 | } 124 | try { 125 | $date = new \DateTime($date); 126 | } catch (\Throwable $e) { 127 | info("{$date} 数据不是正确的日期格式, 错误信息: " . $e->getMessage()); 128 | return null; 129 | } 130 | $dayOfWeek = intval($date->format('w') ?: '7'); 131 | 132 | return $dayOfWeek; 133 | } 134 | 135 | public static function getYearMonth($year = null, $limitToCurrentMonth = true) 136 | { 137 | $currentYear = $year; 138 | 139 | // 获取当前年份 140 | if (!$currentYear) { 141 | $currentYear = date("Y"); 142 | } 143 | 144 | // 创建一个数组来保存12个月份 145 | $monthsList = []; 146 | for ($i = 1; $i <= 12; $i++) { 147 | // 数字月份前面补零,比如 '01','02',...,'12' 148 | $month = str_pad($i, 2, '0', STR_PAD_LEFT); 149 | 150 | // 将年份和月份结合,格式为 'YYYY-MM' 151 | $monthsList[] = "{$currentYear}-{$month}"; 152 | } 153 | 154 | $months = []; 155 | foreach ($monthsList as $monthItem) { 156 | $currentMonthItem = \Carbon\Carbon::createFromDate($monthItem); 157 | 158 | if ($limitToCurrentMonth) { 159 | if ($currentMonthItem->gt(now())) { 160 | continue; 161 | } 162 | } 163 | 164 | $months[] = $monthItem; 165 | } 166 | 167 | return $months; 168 | } 169 | 170 | public static function getYearMonthRange(array $monthRange) 171 | { 172 | $data = []; 173 | foreach ($monthRange as $item) { 174 | $startDay = static::getYearMonthDay($item, 'first'); 175 | $endDay = static::getYearMonthDay($item, 'last'); 176 | 177 | $data[] = [ 178 | $startDay, 179 | $endDay, 180 | ]; 181 | } 182 | 183 | return $data; 184 | } 185 | 186 | public static function getYearMonthAndMonthRange($year, $limitToCurrentMonth = true) 187 | { 188 | $months = DateUtility::getYearMonth($year, $limitToCurrentMonth); 189 | $monthRangeList = DateUtility::getYearMonthRange($months); 190 | 191 | return [ 192 | 'months' => $months, 193 | 'monthRangeList' => $monthRangeList, 194 | ]; 195 | } 196 | 197 | public static function getYearMonthDay($monthDay = null, $type = 'first') 198 | { 199 | $type = match ($type) { 200 | default => 'first', 201 | 'first' => 'first', 202 | 'last' => 'last', 203 | }; 204 | 205 | $date = new \DateTime($monthDay); 206 | $date->modify("{$type} day of this month"); 207 | 208 | $dateString = $date->format('Y-m-d'); 209 | 210 | $dateTimeString = match ($type) { 211 | default => $dateString . ' 00:00:00', 212 | 'first' => $dateString . ' 00:00:00', 213 | 'last' => $dateString . ' 23:59:59', 214 | }; 215 | 216 | return $dateTimeString; 217 | } 218 | 219 | /** 220 | * 仪表盘日志使用 221 | * 根据类型获取开始结束时间戳数组 222 | * 223 | * 224 | * @param string $type today-今天;yesterday-昨天;week-本周;lastWeek-上一周;month-本月;lastMonth-上一个月;quarter-本季度;lastQuarter-上季度;year-本年;lastYear-去年;recent60-最近60天;recent30-最近30天; 225 | * 226 | * @return array [0 => "start_time timestamp", 1 => "end_time timestamp", "last_time" => [0 => "previous cycle start_time", 1 => "previous cycle end_time"]] 227 | */ 228 | public static function getDateTimeInfoByDateType($type = 'today') 229 | { 230 | switch ($type) { 231 | case 'yesterday': 232 | $timeArr = DataTime::yesterday(); 233 | $timeArr['last_time'] = DataTime::yesterday(1); 234 | break; 235 | case 'week': 236 | $timeArr = DataTime::week(); 237 | $timeArr['last_time'] = DataTime::lastWeek(); 238 | break; 239 | case 'lastWeek': 240 | $timeArr = DataTime::lastWeek(); 241 | $timeArr['last_time'] = DataTime::lastWeek(1); 242 | break; 243 | case 'month': 244 | $timeArr = DataTime::month(); 245 | $timeArr['last_time'] = DataTime::lastMonth(); 246 | break; 247 | case 'lastMonth': 248 | $timeArr = DataTime::lastMonth(); 249 | $timeArr['last_time'] = DataTime::lastMonth(1); 250 | break; 251 | case 'quarter': 252 | //本季度 253 | $month = date('m'); 254 | if ($month == 1 || $month == 2 || $month == 3) { 255 | $daterange_start_time = strtotime(date('Y-01-01 00:00:00')); 256 | $daterange_end_time = strtotime(date("Y-03-31 23:59:59")); 257 | } elseif ($month == 4 || $month == 5 || $month == 6) { 258 | $daterange_start_time = strtotime(date('Y-04-01 00:00:00')); 259 | $daterange_end_time = strtotime(date("Y-06-30 23:59:59")); 260 | } elseif ($month == 7 || $month == 8 || $month == 9) { 261 | $daterange_start_time = strtotime(date('Y-07-01 00:00:00')); 262 | $daterange_end_time = strtotime(date("Y-09-30 23:59:59")); 263 | } else { 264 | $daterange_start_time = strtotime(date('Y-10-01 00:00:00')); 265 | $daterange_end_time = strtotime(date("Y-12-31 23:59:59")); 266 | } 267 | 268 | //上季度 269 | $month = date('m'); 270 | if ($month == 1 || $month == 2 || $month == 3) { 271 | $year = date('Y') - 1; 272 | $daterange_start_time_last_time = strtotime(date($year . '-10-01 00:00:00')); 273 | $daterange_end_time_last_time = strtotime(date($year . '-12-31 23:59:59')); 274 | } elseif ($month == 4 || $month == 5 || $month == 6) { 275 | $daterange_start_time_last_time = strtotime(date('Y-01-01 00:00:00')); 276 | $daterange_end_time_last_time = strtotime(date("Y-03-31 23:59:59")); 277 | } elseif ($month == 7 || $month == 8 || $month == 9) { 278 | $daterange_start_time_last_time = strtotime(date('Y-04-01 00:00:00')); 279 | $daterange_end_time_last_time = strtotime(date("Y-06-30 23:59:59")); 280 | } else { 281 | $daterange_start_time_last_time = strtotime(date('Y-07-01 00:00:00')); 282 | $daterange_end_time_last_time = strtotime(date("Y-09-30 23:59:59")); 283 | } 284 | $timeArr = array($daterange_start_time, $daterange_end_time); 285 | $timeArr['last_time'] = array($daterange_start_time_last_time, $daterange_end_time_last_time); 286 | break; 287 | case 'lastQuarter': 288 | //上季度 289 | $month = date('m'); 290 | if ($month == 1 || $month == 2 || $month == 3) { 291 | $year = date('Y') - 1; 292 | $daterange_start_time = strtotime(date($year . '-10-01 00:00:00')); 293 | $daterange_end_time = strtotime(date($year . '-12-31 23:59:59')); 294 | } elseif ($month == 4 || $month == 5 || $month == 6) { 295 | $daterange_start_time = strtotime(date('Y-01-01 00:00:00')); 296 | $daterange_end_time = strtotime(date("Y-03-31 23:59:59")); 297 | } elseif ($month == 7 || $month == 8 || $month == 9) { 298 | $daterange_start_time = strtotime(date('Y-04-01 00:00:00')); 299 | $daterange_end_time = strtotime(date("Y-06-30 23:59:59")); 300 | } else { 301 | $daterange_start_time = strtotime(date('Y-07-01 00:00:00')); 302 | $daterange_end_time = strtotime(date("Y-09-30 23:59:59")); 303 | } 304 | //上季度 305 | $month = date('m'); 306 | if ($month == 1 || $month == 2 || $month == 3) { 307 | $year = date('Y') - 2; 308 | $daterange_start_time_last_time = strtotime(date($year . '-10-01 00:00:00')); 309 | $daterange_end_time_last_time = strtotime(date($year . '-12-31 23:59:59')); 310 | } elseif ($month == 4 || $month == 5 || $month == 6) { 311 | $daterange_start_time_last_time = strtotime(date('Y-01-01 00:00:00')); 312 | $daterange_end_time_last_time = strtotime(date("Y-03-31 23:59:59")); 313 | } elseif ($month == 7 || $month == 8 || $month == 9) { 314 | $daterange_start_time_last_time = strtotime(date('Y-04-01 00:00:00')); 315 | $daterange_end_time_last_time = strtotime(date("Y-06-30 23:59:59")); 316 | } else { 317 | $daterange_start_time_last_time = strtotime(date('Y-07-01 00:00:00')); 318 | $daterange_end_time_last_time = strtotime(date("Y-09-30 23:59:59")); 319 | } 320 | $timeArr = array($daterange_start_time, $daterange_end_time); 321 | $timeArr['last_time'] = array($daterange_start_time_last_time, $daterange_end_time_last_time); 322 | break; 323 | case 'year': 324 | $timeArr = DataTime::year(); 325 | $timeArr['last_time'] = DataTime::lastYear(); 326 | break; 327 | case 'lastYear': 328 | $timeArr = DataTime::lastYear(); 329 | $timeArr['last_time'] = DataTime::lastYear(1); 330 | break; 331 | case 'recent60': 332 | $timeArr = DataTime::recent60(); 333 | break; 334 | case 'recent30': 335 | $timeArr = DataTime::recent30(); 336 | break; 337 | case 'today': 338 | default: 339 | $timeArr = DataTime::today(); 340 | $timeArr['last_time'] = DataTime::yesterday(); 341 | break; 342 | } 343 | return $timeArr; 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /resources/views/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('MarketManager::layouts.master') 2 | 3 | @php 4 | use \Plugins\MarketManager\Utilities\PluginUtility; 5 | @endphp 6 | 7 | @section('content') 8 | 17 | 18 |
19 |
20 | 25 | 26 | 30 | 31 | 34 | 35 | @if(auth()->check()) 36 | 54 | @endif 55 |
56 |
57 | 58 |
59 |
60 |
61 |

应用中心

62 | 63 | 86 |
87 | 88 |
89 | @if(!str_contains($configs['market_server_host'], 'packagist.org')) 90 | 93 | @endif 94 | 95 | 98 | 99 | 102 | 103 | 165 |
166 |
167 |
168 | 169 | 170 | 209 | 210 | 230 | 231 | 281 | 282 | 415 | 416 | 540 | @endsection --------------------------------------------------------------------------------