From f83ab6745cb0554b659e51e6812031b3a80a61ec Mon Sep 17 00:00:00 2001 From: CN-JS-HuiBai Date: Tue, 7 Apr 2026 17:53:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B4=E7=90=86=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Xboard/.docker/.data/.gitignore | 3 - Xboard/.docker/.data/redis/.gitignore | 2 - Xboard/.docker/supervisor/supervisord.conf | 81 -- Xboard/.dockerignore | 26 - Xboard/.editorconfig | 15 - Xboard/.env.example | 41 - Xboard/.gitattributes | 5 - Xboard/.github/ISSUE_TEMPLATE/bug-report.md | 39 - .../.github/ISSUE_TEMPLATE/feature-request.md | 28 - Xboard/.github/workflows/docker-publish.yml | 100 --- Xboard/.gitignore | 34 - Xboard/.gitmodules | 3 - Xboard/Dockerfile | 47 -- Xboard/LICENSE | 21 - Xboard/README.md | 100 --- .../app/Console/Commands/BackupDatabase.php | 100 --- .../app/Console/Commands/CheckCommission.php | 129 --- Xboard/app/Console/Commands/CheckOrder.php | 53 -- Xboard/app/Console/Commands/CheckServer.php | 64 -- Xboard/app/Console/Commands/CheckTicket.php | 51 -- .../Console/Commands/CheckTrafficExceeded.php | 63 -- Xboard/app/Console/Commands/ClearUser.php | 51 -- Xboard/app/Console/Commands/HookList.php | 42 - .../app/Console/Commands/MigrateFromV2b.php | 186 ----- .../Console/Commands/NodeWebSocketServer.php | 34 - Xboard/app/Console/Commands/ResetLog.php | 48 -- Xboard/app/Console/Commands/ResetPassword.php | 55 -- Xboard/app/Console/Commands/ResetTraffic.php | 289 ------- Xboard/app/Console/Commands/ResetUser.php | 58 -- .../app/Console/Commands/SendRemindMail.php | 103 --- Xboard/app/Console/Commands/Test.php | 41 - Xboard/app/Console/Commands/XboardInstall.php | 397 --------- .../app/Console/Commands/XboardRollback.php | 45 -- .../app/Console/Commands/XboardStatistics.php | 75 -- Xboard/app/Console/Commands/XboardUpdate.php | 65 -- Xboard/app/Console/Kernel.php | 68 -- Xboard/app/Contracts/PaymentInterface.php | 10 - Xboard/app/Exceptions/ApiException.php | 23 - Xboard/app/Exceptions/BusinessException.php | 19 - Xboard/app/Exceptions/Handler.php | 101 --- Xboard/app/Helpers/ApiResponse.php | 79 -- Xboard/app/Helpers/Functions.php | 82 -- Xboard/app/Helpers/ResponseEnum.php | 81 -- Xboard/app/Http/Controllers/Controller.php | 13 - .../app/Http/Controllers/PluginController.php | 26 - .../Controllers/V1/Client/AppController.php | 90 --- .../V1/Client/ClientController.php | 247 ------ .../Controllers/V1/Guest/CommController.php | 39 - .../V1/Guest/PaymentController.php | 52 -- .../Controllers/V1/Guest/PlanController.php | 25 - .../V1/Guest/TelegramController.php | 126 --- .../V1/Passport/AuthController.php | 175 ---- .../V1/Passport/CommController.php | 76 -- .../Server/ShadowsocksTidalabController.php | 65 -- .../V1/Server/TrojanTidalabController.php | 108 --- .../V1/Server/UniProxyController.php | 178 ---- .../Controllers/V1/User/CommController.php | 39 - .../Controllers/V1/User/CouponController.php | 25 - .../V1/User/GiftCardController.php | 193 ----- .../Controllers/V1/User/InviteController.php | 79 -- .../V1/User/KnowledgeController.php | 150 ---- .../Controllers/V1/User/NoticeController.php | 26 - .../Controllers/V1/User/OrderController.php | 212 ----- .../Controllers/V1/User/PlanController.php | 38 - .../Controllers/V1/User/ServerController.php | 31 - .../Controllers/V1/User/StatController.php | 26 - .../V1/User/TelegramController.php | 26 - .../Controllers/V1/User/TicketController.php | 154 ---- .../Controllers/V1/User/UserController.php | 223 ------ .../Controllers/V2/Admin/ConfigController.php | 300 ------- .../Controllers/V2/Admin/CouponController.php | 186 ----- .../V2/Admin/GiftCardController.php | 622 -------------- .../V2/Admin/KnowledgeController.php | 113 --- .../Controllers/V2/Admin/NoticeController.php | 101 --- .../Controllers/V2/Admin/OrderController.php | 252 ------ .../V2/Admin/PaymentController.php | 133 --- .../Controllers/V2/Admin/PlanController.php | 132 --- .../Controllers/V2/Admin/PluginController.php | 333 -------- .../V2/Admin/Server/GroupController.php | 66 -- .../V2/Admin/Server/ManageController.php | 219 ----- .../V2/Admin/Server/RouteController.php | 64 -- .../Controllers/V2/Admin/StatController.php | 508 ------------ .../Controllers/V2/Admin/SystemController.php | 144 ---- .../Controllers/V2/Admin/ThemeController.php | 150 ---- .../Controllers/V2/Admin/TicketController.php | 156 ---- .../V2/Admin/TrafficResetController.php | 235 ------ .../Controllers/V2/Admin/UpdateController.php | 28 - .../Controllers/V2/Admin/UserController.php | 682 ---------------- .../Controllers/V2/Client/AppController.php | 153 ---- .../V2/Server/ServerController.php | 138 ---- Xboard/app/Http/Kernel.php | 99 --- Xboard/app/Http/Middleware/Admin.php | 30 - .../Http/Middleware/ApplyRuntimeSettings.php | 25 - Xboard/app/Http/Middleware/Authenticate.php | 17 - .../Middleware/CheckForMaintenanceMode.php | 18 - Xboard/app/Http/Middleware/Client.php | 33 - Xboard/app/Http/Middleware/EncryptCookies.php | 16 - .../Middleware/EnsureTransactionState.php | 29 - Xboard/app/Http/Middleware/ForceJson.php | 22 - .../app/Http/Middleware/InitializePlugins.php | 37 - Xboard/app/Http/Middleware/Language.php | 17 - .../Middleware/RedirectIfAuthenticated.php | 26 - Xboard/app/Http/Middleware/RequestLog.php | 60 -- Xboard/app/Http/Middleware/Server.php | 59 -- Xboard/app/Http/Middleware/Staff.php | 30 - Xboard/app/Http/Middleware/TrimStrings.php | 19 - Xboard/app/Http/Middleware/TrustProxies.php | 47 -- Xboard/app/Http/Middleware/User.php | 27 - .../app/Http/Middleware/VerifyCsrfToken.php | 22 - Xboard/app/Http/Requests/Admin/ConfigSave.php | 146 ---- .../Http/Requests/Admin/CouponGenerate.php | 51 -- .../Requests/Admin/KnowledgeCategorySave.php | 29 - .../Requests/Admin/KnowledgeCategorySort.php | 28 - .../app/Http/Requests/Admin/KnowledgeSave.php | 35 - .../app/Http/Requests/Admin/KnowledgeSort.php | 28 - Xboard/app/Http/Requests/Admin/MailSend.php | 34 - Xboard/app/Http/Requests/Admin/NoticeSave.php | 33 - .../app/Http/Requests/Admin/OrderAssign.php | 34 - Xboard/app/Http/Requests/Admin/OrderFetch.php | 32 - .../app/Http/Requests/Admin/OrderUpdate.php | 29 - Xboard/app/Http/Requests/Admin/PlanSave.php | 157 ---- Xboard/app/Http/Requests/Admin/PlanSort.php | 28 - Xboard/app/Http/Requests/Admin/PlanUpdate.php | 29 - Xboard/app/Http/Requests/Admin/ServerSave.php | 212 ----- Xboard/app/Http/Requests/Admin/UserFetch.php | 33 - .../app/Http/Requests/Admin/UserGenerate.php | 33 - .../app/Http/Requests/Admin/UserSendMail.php | 29 - Xboard/app/Http/Requests/Admin/UserUpdate.php | 69 -- .../app/Http/Requests/Passport/AuthForget.php | 33 - .../app/Http/Requests/Passport/AuthLogin.php | 31 - .../Http/Requests/Passport/AuthRegister.php | 31 - .../Requests/Passport/CommSendEmailVerify.php | 28 - Xboard/app/Http/Requests/Staff/UserUpdate.php | 56 -- .../Requests/User/GiftCardCheckRequest.php | 28 - .../Requests/User/GiftCardRedeemRequest.php | 44 - Xboard/app/Http/Requests/User/OrderSave.php | 30 - Xboard/app/Http/Requests/User/TicketSave.php | 32 - .../app/Http/Requests/User/TicketWithdraw.php | 29 - .../Http/Requests/User/UserChangePassword.php | 30 - .../app/Http/Requests/User/UserTransfer.php | 29 - Xboard/app/Http/Requests/User/UserUpdate.php | 29 - .../Http/Resources/ComissionLogResource.php | 25 - Xboard/app/Http/Resources/CouponResource.php | 38 - .../app/Http/Resources/InviteCodeResource.php | 28 - .../app/Http/Resources/KnowledgeResource.php | 28 - Xboard/app/Http/Resources/MessageResource.php | 26 - Xboard/app/Http/Resources/NodeResource.php | 29 - Xboard/app/Http/Resources/OrderResource.php | 28 - Xboard/app/Http/Resources/PlanResource.php | 122 --- Xboard/app/Http/Resources/TicketResource.php | 31 - .../app/Http/Resources/TrafficLogResource.php | 26 - Xboard/app/Http/Routes/V1/ClientRoute.php | 23 - Xboard/app/Http/Routes/V1/GuestRoute.php | 27 - Xboard/app/Http/Routes/V1/PassportRoute.php | 27 - Xboard/app/Http/Routes/V1/ServerRoute.php | 45 -- Xboard/app/Http/Routes/V1/UserRoute.php | 83 -- Xboard/app/Http/Routes/V2/AdminRoute.php | 276 ------- Xboard/app/Http/Routes/V2/ClientRoute.php | 20 - Xboard/app/Http/Routes/V2/PassportRoute.php | 27 - Xboard/app/Http/Routes/V2/ServerRoute.php | 29 - Xboard/app/Http/Routes/V2/UserRoute.php | 20 - Xboard/app/Jobs/NodeUserSyncJob.php | 45 -- Xboard/app/Jobs/OrderHandleJob.php | 56 -- Xboard/app/Jobs/SendEmailJob.php | 42 - Xboard/app/Jobs/SendTelegramJob.php | 43 - Xboard/app/Jobs/StatServerJob.php | 174 ---- Xboard/app/Jobs/StatUserJob.php | 158 ---- Xboard/app/Jobs/TrafficFetchJob.php | 51 -- Xboard/app/Models/AdminAuditLog.php | 21 - Xboard/app/Models/CommissionLog.php | 16 - Xboard/app/Models/Coupon.php | 28 - Xboard/app/Models/GiftCardCode.php | 260 ------ Xboard/app/Models/GiftCardTemplate.php | 254 ------ Xboard/app/Models/GiftCardUsage.php | 112 --- Xboard/app/Models/InviteCode.php | 19 - Xboard/app/Models/Knowledge.php | 17 - Xboard/app/Models/MailLog.php | 16 - Xboard/app/Models/Notice.php | 18 - Xboard/app/Models/Order.php | 120 --- Xboard/app/Models/Payment.php | 18 - Xboard/app/Models/Plan.php | 353 -------- Xboard/app/Models/Plugin.php | 77 -- Xboard/app/Models/Server.php | 563 ------------- Xboard/app/Models/ServerGroup.php | 46 -- Xboard/app/Models/ServerLog.php | 16 - Xboard/app/Models/ServerRoute.php | 17 - Xboard/app/Models/ServerStat.php | 16 - Xboard/app/Models/Setting.php | 68 -- Xboard/app/Models/Stat.php | 16 - Xboard/app/Models/StatServer.php | 33 - Xboard/app/Models/StatUser.php | 28 - Xboard/app/Models/SubscribeTemplate.php | 46 -- Xboard/app/Models/Ticket.php | 60 -- Xboard/app/Models/TicketMessage.php | 56 -- Xboard/app/Models/TrafficResetLog.php | 149 ---- Xboard/app/Models/User.php | 215 ----- Xboard/app/Observers/PlanObserver.php | 35 - Xboard/app/Observers/ServerObserver.php | 37 - Xboard/app/Observers/ServerRouteObserver.php | 31 - Xboard/app/Observers/UserObserver.php | 53 -- Xboard/app/Protocols/Clash.php | 332 -------- Xboard/app/Protocols/ClashMeta.php | 708 ---------------- Xboard/app/Protocols/General.php | 448 ----------- Xboard/app/Protocols/Loon.php | 357 --------- Xboard/app/Protocols/QuantumultX.php | 232 ------ Xboard/app/Protocols/Shadowrocket.php | 415 ---------- Xboard/app/Protocols/Shadowsocks.php | 60 -- Xboard/app/Protocols/SingBox.php | 757 ------------------ Xboard/app/Protocols/Stash.php | 587 -------------- Xboard/app/Protocols/Surfboard.php | 229 ------ Xboard/app/Protocols/Surge.php | 319 -------- Xboard/app/Providers/AuthServiceProvider.php | 28 - .../Providers/BroadcastServiceProvider.php | 21 - Xboard/app/Providers/EventServiceProvider.php | 40 - .../app/Providers/HorizonServiceProvider.php | 43 - .../app/Providers/OctaneServiceProvider.php | 43 - .../app/Providers/PluginServiceProvider.php | 29 - .../app/Providers/ProtocolServiceProvider.php | 50 -- Xboard/app/Providers/RouteServiceProvider.php | 87 -- .../app/Providers/SettingServiceProvider.php | 33 - Xboard/app/Scope/FilterScope.php | 50 -- Xboard/app/Services/Auth/LoginService.php | 154 ---- Xboard/app/Services/Auth/MailLinkService.php | 100 --- Xboard/app/Services/Auth/RegisterService.php | 193 ----- Xboard/app/Services/AuthService.php | 87 -- Xboard/app/Services/CaptchaService.php | 112 --- Xboard/app/Services/CouponService.php | 122 --- Xboard/app/Services/DeviceStateService.php | 187 ----- Xboard/app/Services/GiftCardService.php | 334 -------- Xboard/app/Services/MailService.php | 295 ------- Xboard/app/Services/NodeRegistry.php | 77 -- Xboard/app/Services/NodeSyncService.php | 143 ---- Xboard/app/Services/OrderService.php | 429 ---------- Xboard/app/Services/PaymentService.php | 135 ---- Xboard/app/Services/PlanService.php | 194 ----- Xboard/app/Services/Plugin/AbstractPlugin.php | 222 ----- Xboard/app/Services/Plugin/HookManager.php | 286 ------- .../Plugin/InterceptResponseException.php | 22 - .../Services/Plugin/PluginConfigService.php | 111 --- Xboard/app/Services/Plugin/PluginManager.php | 727 ----------------- Xboard/app/Services/ServerService.php | 296 ------- Xboard/app/Services/SettingService.php | 18 - Xboard/app/Services/StatisticalService.php | 378 --------- Xboard/app/Services/TelegramService.php | 160 ---- Xboard/app/Services/ThemeService.php | 424 ---------- Xboard/app/Services/TicketService.php | 125 --- Xboard/app/Services/TrafficResetService.php | 415 ---------- Xboard/app/Services/UpdateService.php | 458 ----------- Xboard/app/Services/UserService.php | 288 ------- Xboard/app/Support/AbstractProtocol.php | 262 ------ Xboard/app/Support/ProtocolManager.php | 162 ---- Xboard/app/Support/Setting.php | 140 ---- Xboard/app/Traits/HasPluginConfig.php | 144 ---- Xboard/app/Traits/QueryOperators.php | 68 -- Xboard/app/Utils/CacheKey.php | 69 -- Xboard/app/Utils/Dict.php | 23 - Xboard/app/Utils/Helper.php | 243 ------ Xboard/app/WebSocket/NodeEventHandlers.php | 144 ---- Xboard/app/WebSocket/NodeWorker.php | 249 ------ Xboard/artisan | 53 -- Xboard/bootstrap/app.php | 55 -- Xboard/bootstrap/cache/.gitignore | 2 - Xboard/compose.sample.yaml | 44 - Xboard/composer.json | 98 --- Xboard/config/app.php | 193 ----- Xboard/config/auth.php | 103 --- Xboard/config/broadcasting.php | 59 -- Xboard/config/cache.php | 107 --- Xboard/config/cloud_storage.php | 10 - Xboard/config/cors.php | 34 - Xboard/config/database.php | 157 ---- Xboard/config/debugbar.php | 275 ------- Xboard/config/filesystems.php | 69 -- Xboard/config/hashing.php | 52 -- Xboard/config/hidden_features.php | 5 - Xboard/config/horizon.php | 228 ------ Xboard/config/logging.php | 64 -- Xboard/config/mail.php | 136 ---- Xboard/config/octane.php | 221 ----- Xboard/config/queue.php | 88 -- Xboard/config/sanctum.php | 83 -- Xboard/config/scribe.php | 270 ------- Xboard/config/services.php | 33 - Xboard/config/session.php | 199 ----- Xboard/config/theme/.gitignore | 2 - Xboard/config/view.php | 36 - Xboard/database/.gitignore | 2 - Xboard/database/factories/UserFactory.php | 28 - ..._08_19_000000_create_failed_jobs_table.php | 37 - ...01_create_personal_access_tokens_table.php | 34 - .../2023_03_19_000000_create_v2_tables.php | 496 ------------ ..._08_14_221234_create_v2_settings_table.php | 35 - ...23_add_column_excludes_to_server_table.php | 56 -- ..._195956_add_column_ips_to_server_table.php | 56 -- ...d_column_alpn_to_server_hysteria_table.php | 32 - ...d_network_settings_to_v2_server_trojan.php | 33 - ...n_and_is_obfs_to_server_hysteria_table.php | 33 - ...ange_column_value_to_v2_settings_table.php | 28 - ...2_12_212239_add_index_to_v2_user_table.php | 28 - ...modify_icon_column_to_v2_payment_table.php | 28 - ...fy_commission_status_in_v2_order_table.php | 28 - .../2025_01_04_optimize_plan_table.php | 129 --- ...25_01_05_131425_create_v2_server_table.php | 523 ------------ ...5_01_10_152139_add_device_limit_column.php | 31 - ..._12_190315_add_sort_to_v2_notice_table.php | 30 - ...fy_commission_status_in_v2_order_table.php | 33 - ..._13_000000_convert_order_period_fields.php | 56 -- ...15_000002_add_stat_performance_indexes.php | 93 --- ...add_updated_at_index_to_v2_order_table.php | 28 - ...2025_01_18_140511_create_plugins_table.php | 33 - ...6_21_000001_optimize_v2_settings_table.php | 37 - ...000002_create_traffic_reset_logs_table.php | 43 - ...0003_add_traffic_reset_fields_to_users.php | 39 - ...07_01_081556_add_tags_to_v2_plan_table.php | 28 - ...5_07_01_122908_create_gift_card_tables.php | 98 --- ...mn_rate_time_ranges_to_v2_server_table.php | 29 - ...07_26_000001_add_type_to_plugins_table.php | 24 - ...01_create_v2_subscribe_templates_table.php | 91 --- ...01_replace_v2_log_with_admin_audit_log.php | 28 - ...1_000002_add_stat_user_record_at_index.php | 22 - ...tom_config_and_cert_to_v2_server_table.php | 30 - ...8_161536_add_traffic_fields_to_servers.php | 46 -- Xboard/database/seeders/DatabaseSeeder.php | 17 - .../OriginV2bMigrationsTableSeeder.php | 170 ---- Xboard/docs/en/development/device-limit.md | 176 ---- Xboard/docs/en/development/performance.md | 100 --- .../development/plugin-development-guide.md | 691 ---------------- Xboard/docs/en/installation/1panel.md | 210 ----- Xboard/docs/en/installation/aapanel-docker.md | 151 ---- Xboard/docs/en/installation/aapanel.md | 210 ----- Xboard/docs/en/installation/docker-compose.md | 77 -- Xboard/docs/en/migration/config.md | 54 -- Xboard/docs/en/migration/v2board-1.7.3.md | 63 -- Xboard/docs/en/migration/v2board-1.7.4.md | 51 -- Xboard/docs/en/migration/v2board-dev.md | 61 -- Xboard/docs/images/admin.png | Bin 1423703 -> 0 bytes Xboard/docs/images/user.png | Bin 500933 -> 0 bytes Xboard/init.sh | 15 - Xboard/package.json | 5 - Xboard/phpstan.neon | 17 - Xboard/public/index.php | 60 -- Xboard/public/robots.txt | 2 - Xboard/public/theme/.gitignore | 2 - Xboard/public/web.config | 28 - Xboard/resources/js/app.js | 1 - Xboard/resources/js/bootstrap.js | 28 - Xboard/resources/lang/en-US.json | 148 ---- Xboard/resources/lang/ru-RU.json | 148 ---- Xboard/resources/lang/zh-CN.json | 148 ---- Xboard/resources/lang/zh-TW.json | 148 ---- Xboard/resources/rules/.gitignore | 1 - Xboard/resources/rules/app.clash.yaml | 557 ------------- Xboard/resources/rules/default.clash.yaml | 286 ------- Xboard/resources/rules/default.sing-box.json | 138 ---- Xboard/resources/rules/default.surfboard.conf | 576 ------------- Xboard/resources/rules/default.surge.conf | 595 -------------- Xboard/resources/sass/app.scss | 1 - Xboard/resources/views/admin.blade.php | 86 -- .../views/client/subscribe.blade.php | 296 ------- Xboard/resources/views/errors/500.blade.php | 5 - .../views/mail/classic/mailLogin.blade.php | 195 ----- .../views/mail/classic/notify.blade.php | 187 ----- .../views/mail/classic/remindExpire.blade.php | 187 ----- .../mail/classic/remindTraffic.blade.php | 187 ----- .../views/mail/classic/verify.blade.php | 195 ----- .../views/mail/default/mailLogin.blade.php | 43 - .../views/mail/default/notify.blade.php | 42 - .../views/mail/default/remindExpire.blade.php | 42 - .../mail/default/remindTraffic.blade.php | 42 - .../views/mail/default/verify.blade.php | 42 - Xboard/routes/channels.php | 19 - Xboard/routes/console.php | 19 - Xboard/routes/web.php | 92 --- Xboard/storage/backup/.gitignore | 2 - Xboard/storage/debugbar/.gitignore | 2 - Xboard/storage/framework/cache/.gitignore | 2 - Xboard/storage/framework/sessions/.gitignore | 2 - Xboard/storage/framework/views/.gitignore | 2 - Xboard/storage/logs/.gitignore | 2 - Xboard/storage/theme/.gitignore | 2 - Xboard/storage/tmp/.gitignore | 2 - Xboard/storage/views/.gitignore | 2 - Xboard/theme/.gitignore | 4 - .../theme/Xboard/assets/images/background.svg | 1 - Xboard/theme/Xboard/assets/umi.js | 2 - Xboard/theme/Xboard/config.json | 33 - Xboard/theme/Xboard/dashboard.blade.php | 276 ------- Xboard/theme/Xboard/env.example.js | 19 - Xboard/theme/Xboard/env.js | 18 - Xboard/theme/Xboard/index.html | 1 - Xboard/update.sh | 27 - {Xboard/plugins => plugins}/.gitignore | 0 .../UserOnlineDevicesController.php | 0 .../UserOnlineDevices/Plugin.php | 0 .../UserOnlineDevices/README.md | 0 .../UserOnlineDevices/config.json | 0 .../resources/views/admin-users.blade.php | 73 +- .../resources/views/panel.blade.php | 0 .../resources/views/userstatus.blade.php | 0 .../UserOnlineDevices/routes/api.php | 0 .../UserOnlineDevices/routes/web.php | 0 401 files changed, 70 insertions(+), 41568 deletions(-) delete mode 100644 Xboard/.docker/.data/.gitignore delete mode 100644 Xboard/.docker/.data/redis/.gitignore delete mode 100644 Xboard/.docker/supervisor/supervisord.conf delete mode 100644 Xboard/.dockerignore delete mode 100644 Xboard/.editorconfig delete mode 100644 Xboard/.env.example delete mode 100644 Xboard/.gitattributes delete mode 100644 Xboard/.github/ISSUE_TEMPLATE/bug-report.md delete mode 100644 Xboard/.github/ISSUE_TEMPLATE/feature-request.md delete mode 100644 Xboard/.github/workflows/docker-publish.yml delete mode 100644 Xboard/.gitignore delete mode 100644 Xboard/.gitmodules delete mode 100644 Xboard/Dockerfile delete mode 100644 Xboard/LICENSE delete mode 100644 Xboard/README.md delete mode 100644 Xboard/app/Console/Commands/BackupDatabase.php delete mode 100644 Xboard/app/Console/Commands/CheckCommission.php delete mode 100644 Xboard/app/Console/Commands/CheckOrder.php delete mode 100644 Xboard/app/Console/Commands/CheckServer.php delete mode 100644 Xboard/app/Console/Commands/CheckTicket.php delete mode 100644 Xboard/app/Console/Commands/CheckTrafficExceeded.php delete mode 100644 Xboard/app/Console/Commands/ClearUser.php delete mode 100644 Xboard/app/Console/Commands/HookList.php delete mode 100644 Xboard/app/Console/Commands/MigrateFromV2b.php delete mode 100644 Xboard/app/Console/Commands/NodeWebSocketServer.php delete mode 100644 Xboard/app/Console/Commands/ResetLog.php delete mode 100644 Xboard/app/Console/Commands/ResetPassword.php delete mode 100644 Xboard/app/Console/Commands/ResetTraffic.php delete mode 100644 Xboard/app/Console/Commands/ResetUser.php delete mode 100644 Xboard/app/Console/Commands/SendRemindMail.php delete mode 100644 Xboard/app/Console/Commands/Test.php delete mode 100644 Xboard/app/Console/Commands/XboardInstall.php delete mode 100644 Xboard/app/Console/Commands/XboardRollback.php delete mode 100644 Xboard/app/Console/Commands/XboardStatistics.php delete mode 100644 Xboard/app/Console/Commands/XboardUpdate.php delete mode 100644 Xboard/app/Console/Kernel.php delete mode 100644 Xboard/app/Contracts/PaymentInterface.php delete mode 100644 Xboard/app/Exceptions/ApiException.php delete mode 100644 Xboard/app/Exceptions/BusinessException.php delete mode 100644 Xboard/app/Exceptions/Handler.php delete mode 100644 Xboard/app/Helpers/ApiResponse.php delete mode 100644 Xboard/app/Helpers/Functions.php delete mode 100644 Xboard/app/Helpers/ResponseEnum.php delete mode 100644 Xboard/app/Http/Controllers/Controller.php delete mode 100644 Xboard/app/Http/Controllers/PluginController.php delete mode 100644 Xboard/app/Http/Controllers/V1/Client/AppController.php delete mode 100644 Xboard/app/Http/Controllers/V1/Client/ClientController.php delete mode 100644 Xboard/app/Http/Controllers/V1/Guest/CommController.php delete mode 100644 Xboard/app/Http/Controllers/V1/Guest/PaymentController.php delete mode 100644 Xboard/app/Http/Controllers/V1/Guest/PlanController.php delete mode 100644 Xboard/app/Http/Controllers/V1/Guest/TelegramController.php delete mode 100644 Xboard/app/Http/Controllers/V1/Passport/AuthController.php delete mode 100644 Xboard/app/Http/Controllers/V1/Passport/CommController.php delete mode 100644 Xboard/app/Http/Controllers/V1/Server/ShadowsocksTidalabController.php delete mode 100644 Xboard/app/Http/Controllers/V1/Server/TrojanTidalabController.php delete mode 100644 Xboard/app/Http/Controllers/V1/Server/UniProxyController.php delete mode 100644 Xboard/app/Http/Controllers/V1/User/CommController.php delete mode 100644 Xboard/app/Http/Controllers/V1/User/CouponController.php delete mode 100644 Xboard/app/Http/Controllers/V1/User/GiftCardController.php delete mode 100644 Xboard/app/Http/Controllers/V1/User/InviteController.php delete mode 100644 Xboard/app/Http/Controllers/V1/User/KnowledgeController.php delete mode 100644 Xboard/app/Http/Controllers/V1/User/NoticeController.php delete mode 100644 Xboard/app/Http/Controllers/V1/User/OrderController.php delete mode 100644 Xboard/app/Http/Controllers/V1/User/PlanController.php delete mode 100644 Xboard/app/Http/Controllers/V1/User/ServerController.php delete mode 100644 Xboard/app/Http/Controllers/V1/User/StatController.php delete mode 100644 Xboard/app/Http/Controllers/V1/User/TelegramController.php delete mode 100644 Xboard/app/Http/Controllers/V1/User/TicketController.php delete mode 100644 Xboard/app/Http/Controllers/V1/User/UserController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Admin/ConfigController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Admin/CouponController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Admin/GiftCardController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Admin/KnowledgeController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Admin/NoticeController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Admin/OrderController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Admin/PaymentController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Admin/PlanController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Admin/PluginController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Admin/Server/GroupController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Admin/Server/ManageController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Admin/Server/RouteController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Admin/StatController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Admin/SystemController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Admin/ThemeController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Admin/TicketController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Admin/TrafficResetController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Admin/UpdateController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Admin/UserController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Client/AppController.php delete mode 100644 Xboard/app/Http/Controllers/V2/Server/ServerController.php delete mode 100644 Xboard/app/Http/Kernel.php delete mode 100644 Xboard/app/Http/Middleware/Admin.php delete mode 100644 Xboard/app/Http/Middleware/ApplyRuntimeSettings.php delete mode 100644 Xboard/app/Http/Middleware/Authenticate.php delete mode 100644 Xboard/app/Http/Middleware/CheckForMaintenanceMode.php delete mode 100644 Xboard/app/Http/Middleware/Client.php delete mode 100644 Xboard/app/Http/Middleware/EncryptCookies.php delete mode 100644 Xboard/app/Http/Middleware/EnsureTransactionState.php delete mode 100644 Xboard/app/Http/Middleware/ForceJson.php delete mode 100644 Xboard/app/Http/Middleware/InitializePlugins.php delete mode 100644 Xboard/app/Http/Middleware/Language.php delete mode 100644 Xboard/app/Http/Middleware/RedirectIfAuthenticated.php delete mode 100644 Xboard/app/Http/Middleware/RequestLog.php delete mode 100644 Xboard/app/Http/Middleware/Server.php delete mode 100644 Xboard/app/Http/Middleware/Staff.php delete mode 100644 Xboard/app/Http/Middleware/TrimStrings.php delete mode 100644 Xboard/app/Http/Middleware/TrustProxies.php delete mode 100644 Xboard/app/Http/Middleware/User.php delete mode 100644 Xboard/app/Http/Middleware/VerifyCsrfToken.php delete mode 100644 Xboard/app/Http/Requests/Admin/ConfigSave.php delete mode 100644 Xboard/app/Http/Requests/Admin/CouponGenerate.php delete mode 100644 Xboard/app/Http/Requests/Admin/KnowledgeCategorySave.php delete mode 100644 Xboard/app/Http/Requests/Admin/KnowledgeCategorySort.php delete mode 100644 Xboard/app/Http/Requests/Admin/KnowledgeSave.php delete mode 100644 Xboard/app/Http/Requests/Admin/KnowledgeSort.php delete mode 100644 Xboard/app/Http/Requests/Admin/MailSend.php delete mode 100644 Xboard/app/Http/Requests/Admin/NoticeSave.php delete mode 100644 Xboard/app/Http/Requests/Admin/OrderAssign.php delete mode 100644 Xboard/app/Http/Requests/Admin/OrderFetch.php delete mode 100644 Xboard/app/Http/Requests/Admin/OrderUpdate.php delete mode 100644 Xboard/app/Http/Requests/Admin/PlanSave.php delete mode 100644 Xboard/app/Http/Requests/Admin/PlanSort.php delete mode 100644 Xboard/app/Http/Requests/Admin/PlanUpdate.php delete mode 100644 Xboard/app/Http/Requests/Admin/ServerSave.php delete mode 100644 Xboard/app/Http/Requests/Admin/UserFetch.php delete mode 100644 Xboard/app/Http/Requests/Admin/UserGenerate.php delete mode 100644 Xboard/app/Http/Requests/Admin/UserSendMail.php delete mode 100644 Xboard/app/Http/Requests/Admin/UserUpdate.php delete mode 100644 Xboard/app/Http/Requests/Passport/AuthForget.php delete mode 100644 Xboard/app/Http/Requests/Passport/AuthLogin.php delete mode 100644 Xboard/app/Http/Requests/Passport/AuthRegister.php delete mode 100644 Xboard/app/Http/Requests/Passport/CommSendEmailVerify.php delete mode 100644 Xboard/app/Http/Requests/Staff/UserUpdate.php delete mode 100644 Xboard/app/Http/Requests/User/GiftCardCheckRequest.php delete mode 100644 Xboard/app/Http/Requests/User/GiftCardRedeemRequest.php delete mode 100644 Xboard/app/Http/Requests/User/OrderSave.php delete mode 100644 Xboard/app/Http/Requests/User/TicketSave.php delete mode 100644 Xboard/app/Http/Requests/User/TicketWithdraw.php delete mode 100644 Xboard/app/Http/Requests/User/UserChangePassword.php delete mode 100644 Xboard/app/Http/Requests/User/UserTransfer.php delete mode 100644 Xboard/app/Http/Requests/User/UserUpdate.php delete mode 100644 Xboard/app/Http/Resources/ComissionLogResource.php delete mode 100644 Xboard/app/Http/Resources/CouponResource.php delete mode 100644 Xboard/app/Http/Resources/InviteCodeResource.php delete mode 100644 Xboard/app/Http/Resources/KnowledgeResource.php delete mode 100644 Xboard/app/Http/Resources/MessageResource.php delete mode 100644 Xboard/app/Http/Resources/NodeResource.php delete mode 100644 Xboard/app/Http/Resources/OrderResource.php delete mode 100644 Xboard/app/Http/Resources/PlanResource.php delete mode 100644 Xboard/app/Http/Resources/TicketResource.php delete mode 100644 Xboard/app/Http/Resources/TrafficLogResource.php delete mode 100644 Xboard/app/Http/Routes/V1/ClientRoute.php delete mode 100644 Xboard/app/Http/Routes/V1/GuestRoute.php delete mode 100644 Xboard/app/Http/Routes/V1/PassportRoute.php delete mode 100644 Xboard/app/Http/Routes/V1/ServerRoute.php delete mode 100644 Xboard/app/Http/Routes/V1/UserRoute.php delete mode 100644 Xboard/app/Http/Routes/V2/AdminRoute.php delete mode 100644 Xboard/app/Http/Routes/V2/ClientRoute.php delete mode 100644 Xboard/app/Http/Routes/V2/PassportRoute.php delete mode 100644 Xboard/app/Http/Routes/V2/ServerRoute.php delete mode 100644 Xboard/app/Http/Routes/V2/UserRoute.php delete mode 100644 Xboard/app/Jobs/NodeUserSyncJob.php delete mode 100644 Xboard/app/Jobs/OrderHandleJob.php delete mode 100644 Xboard/app/Jobs/SendEmailJob.php delete mode 100644 Xboard/app/Jobs/SendTelegramJob.php delete mode 100644 Xboard/app/Jobs/StatServerJob.php delete mode 100644 Xboard/app/Jobs/StatUserJob.php delete mode 100644 Xboard/app/Jobs/TrafficFetchJob.php delete mode 100644 Xboard/app/Models/AdminAuditLog.php delete mode 100644 Xboard/app/Models/CommissionLog.php delete mode 100644 Xboard/app/Models/Coupon.php delete mode 100644 Xboard/app/Models/GiftCardCode.php delete mode 100644 Xboard/app/Models/GiftCardTemplate.php delete mode 100644 Xboard/app/Models/GiftCardUsage.php delete mode 100644 Xboard/app/Models/InviteCode.php delete mode 100644 Xboard/app/Models/Knowledge.php delete mode 100644 Xboard/app/Models/MailLog.php delete mode 100644 Xboard/app/Models/Notice.php delete mode 100644 Xboard/app/Models/Order.php delete mode 100644 Xboard/app/Models/Payment.php delete mode 100644 Xboard/app/Models/Plan.php delete mode 100644 Xboard/app/Models/Plugin.php delete mode 100644 Xboard/app/Models/Server.php delete mode 100644 Xboard/app/Models/ServerGroup.php delete mode 100644 Xboard/app/Models/ServerLog.php delete mode 100644 Xboard/app/Models/ServerRoute.php delete mode 100644 Xboard/app/Models/ServerStat.php delete mode 100644 Xboard/app/Models/Setting.php delete mode 100644 Xboard/app/Models/Stat.php delete mode 100644 Xboard/app/Models/StatServer.php delete mode 100644 Xboard/app/Models/StatUser.php delete mode 100644 Xboard/app/Models/SubscribeTemplate.php delete mode 100644 Xboard/app/Models/Ticket.php delete mode 100644 Xboard/app/Models/TicketMessage.php delete mode 100644 Xboard/app/Models/TrafficResetLog.php delete mode 100644 Xboard/app/Models/User.php delete mode 100644 Xboard/app/Observers/PlanObserver.php delete mode 100644 Xboard/app/Observers/ServerObserver.php delete mode 100644 Xboard/app/Observers/ServerRouteObserver.php delete mode 100644 Xboard/app/Observers/UserObserver.php delete mode 100644 Xboard/app/Protocols/Clash.php delete mode 100644 Xboard/app/Protocols/ClashMeta.php delete mode 100644 Xboard/app/Protocols/General.php delete mode 100644 Xboard/app/Protocols/Loon.php delete mode 100644 Xboard/app/Protocols/QuantumultX.php delete mode 100644 Xboard/app/Protocols/Shadowrocket.php delete mode 100644 Xboard/app/Protocols/Shadowsocks.php delete mode 100644 Xboard/app/Protocols/SingBox.php delete mode 100644 Xboard/app/Protocols/Stash.php delete mode 100644 Xboard/app/Protocols/Surfboard.php delete mode 100644 Xboard/app/Protocols/Surge.php delete mode 100644 Xboard/app/Providers/AuthServiceProvider.php delete mode 100644 Xboard/app/Providers/BroadcastServiceProvider.php delete mode 100644 Xboard/app/Providers/EventServiceProvider.php delete mode 100644 Xboard/app/Providers/HorizonServiceProvider.php delete mode 100644 Xboard/app/Providers/OctaneServiceProvider.php delete mode 100644 Xboard/app/Providers/PluginServiceProvider.php delete mode 100644 Xboard/app/Providers/ProtocolServiceProvider.php delete mode 100644 Xboard/app/Providers/RouteServiceProvider.php delete mode 100644 Xboard/app/Providers/SettingServiceProvider.php delete mode 100644 Xboard/app/Scope/FilterScope.php delete mode 100644 Xboard/app/Services/Auth/LoginService.php delete mode 100644 Xboard/app/Services/Auth/MailLinkService.php delete mode 100644 Xboard/app/Services/Auth/RegisterService.php delete mode 100644 Xboard/app/Services/AuthService.php delete mode 100644 Xboard/app/Services/CaptchaService.php delete mode 100644 Xboard/app/Services/CouponService.php delete mode 100644 Xboard/app/Services/DeviceStateService.php delete mode 100644 Xboard/app/Services/GiftCardService.php delete mode 100644 Xboard/app/Services/MailService.php delete mode 100644 Xboard/app/Services/NodeRegistry.php delete mode 100644 Xboard/app/Services/NodeSyncService.php delete mode 100644 Xboard/app/Services/OrderService.php delete mode 100644 Xboard/app/Services/PaymentService.php delete mode 100644 Xboard/app/Services/PlanService.php delete mode 100644 Xboard/app/Services/Plugin/AbstractPlugin.php delete mode 100644 Xboard/app/Services/Plugin/HookManager.php delete mode 100644 Xboard/app/Services/Plugin/InterceptResponseException.php delete mode 100644 Xboard/app/Services/Plugin/PluginConfigService.php delete mode 100644 Xboard/app/Services/Plugin/PluginManager.php delete mode 100644 Xboard/app/Services/ServerService.php delete mode 100644 Xboard/app/Services/SettingService.php delete mode 100644 Xboard/app/Services/StatisticalService.php delete mode 100644 Xboard/app/Services/TelegramService.php delete mode 100644 Xboard/app/Services/ThemeService.php delete mode 100644 Xboard/app/Services/TicketService.php delete mode 100644 Xboard/app/Services/TrafficResetService.php delete mode 100644 Xboard/app/Services/UpdateService.php delete mode 100644 Xboard/app/Services/UserService.php delete mode 100644 Xboard/app/Support/AbstractProtocol.php delete mode 100644 Xboard/app/Support/ProtocolManager.php delete mode 100644 Xboard/app/Support/Setting.php delete mode 100644 Xboard/app/Traits/HasPluginConfig.php delete mode 100644 Xboard/app/Traits/QueryOperators.php delete mode 100644 Xboard/app/Utils/CacheKey.php delete mode 100644 Xboard/app/Utils/Dict.php delete mode 100644 Xboard/app/Utils/Helper.php delete mode 100644 Xboard/app/WebSocket/NodeEventHandlers.php delete mode 100644 Xboard/app/WebSocket/NodeWorker.php delete mode 100644 Xboard/artisan delete mode 100644 Xboard/bootstrap/app.php delete mode 100644 Xboard/bootstrap/cache/.gitignore delete mode 100644 Xboard/compose.sample.yaml delete mode 100644 Xboard/composer.json delete mode 100644 Xboard/config/app.php delete mode 100644 Xboard/config/auth.php delete mode 100644 Xboard/config/broadcasting.php delete mode 100644 Xboard/config/cache.php delete mode 100644 Xboard/config/cloud_storage.php delete mode 100644 Xboard/config/cors.php delete mode 100644 Xboard/config/database.php delete mode 100644 Xboard/config/debugbar.php delete mode 100644 Xboard/config/filesystems.php delete mode 100644 Xboard/config/hashing.php delete mode 100644 Xboard/config/hidden_features.php delete mode 100644 Xboard/config/horizon.php delete mode 100644 Xboard/config/logging.php delete mode 100644 Xboard/config/mail.php delete mode 100644 Xboard/config/octane.php delete mode 100644 Xboard/config/queue.php delete mode 100644 Xboard/config/sanctum.php delete mode 100644 Xboard/config/scribe.php delete mode 100644 Xboard/config/services.php delete mode 100644 Xboard/config/session.php delete mode 100644 Xboard/config/theme/.gitignore delete mode 100644 Xboard/config/view.php delete mode 100644 Xboard/database/.gitignore delete mode 100644 Xboard/database/factories/UserFactory.php delete mode 100644 Xboard/database/migrations/2019_08_19_000000_create_failed_jobs_table.php delete mode 100644 Xboard/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php delete mode 100644 Xboard/database/migrations/2023_03_19_000000_create_v2_tables.php delete mode 100644 Xboard/database/migrations/2023_08_14_221234_create_v2_settings_table.php delete mode 100644 Xboard/database/migrations/2023_09_04_190923_add_column_excludes_to_server_table.php delete mode 100644 Xboard/database/migrations/2023_09_06_195956_add_column_ips_to_server_table.php delete mode 100644 Xboard/database/migrations/2023_09_14_013244_add_column_alpn_to_server_hysteria_table.php delete mode 100644 Xboard/database/migrations/2023_09_24_040317_add_column_network_and_network_settings_to_v2_server_trojan.php delete mode 100644 Xboard/database/migrations/2023_09_29_044957_add_column_version_and_is_obfs_to_server_hysteria_table.php delete mode 100644 Xboard/database/migrations/2023_11_19_205026_change_column_value_to_v2_settings_table.php delete mode 100644 Xboard/database/migrations/2023_12_12_212239_add_index_to_v2_user_table.php delete mode 100644 Xboard/database/migrations/2024_03_19_103149_modify_icon_column_to_v2_payment_table.php delete mode 100644 Xboard/database/migrations/2025_01_01_130644_modify_commission_status_in_v2_order_table.php delete mode 100644 Xboard/database/migrations/2025_01_04_optimize_plan_table.php delete mode 100644 Xboard/database/migrations/2025_01_05_131425_create_v2_server_table.php delete mode 100644 Xboard/database/migrations/2025_01_10_152139_add_device_limit_column.php delete mode 100644 Xboard/database/migrations/2025_01_12_190315_add_sort_to_v2_notice_table.php delete mode 100644 Xboard/database/migrations/2025_01_12_200936_modify_commission_status_in_v2_order_table.php delete mode 100644 Xboard/database/migrations/2025_01_13_000000_convert_order_period_fields.php delete mode 100644 Xboard/database/migrations/2025_01_15_000002_add_stat_performance_indexes.php delete mode 100644 Xboard/database/migrations/2025_01_16_142320_add_updated_at_index_to_v2_order_table.php delete mode 100644 Xboard/database/migrations/2025_01_18_140511_create_plugins_table.php delete mode 100644 Xboard/database/migrations/2025_06_21_000001_optimize_v2_settings_table.php delete mode 100644 Xboard/database/migrations/2025_06_21_000002_create_traffic_reset_logs_table.php delete mode 100644 Xboard/database/migrations/2025_06_21_000003_add_traffic_reset_fields_to_users.php delete mode 100644 Xboard/database/migrations/2025_07_01_081556_add_tags_to_v2_plan_table.php delete mode 100644 Xboard/database/migrations/2025_07_01_122908_create_gift_card_tables.php delete mode 100644 Xboard/database/migrations/2025_07_13_224539_add_column_rate_time_ranges_to_v2_server_table.php delete mode 100644 Xboard/database/migrations/2025_07_26_000001_add_type_to_plugins_table.php delete mode 100644 Xboard/database/migrations/2025_07_27_000001_create_v2_subscribe_templates_table.php delete mode 100644 Xboard/database/migrations/2026_03_11_000001_replace_v2_log_with_admin_audit_log.php delete mode 100644 Xboard/database/migrations/2026_03_11_000002_add_stat_user_record_at_index.php delete mode 100644 Xboard/database/migrations/2026_03_15_060035_add_custom_config_and_cert_to_v2_server_table.php delete mode 100644 Xboard/database/migrations/2026_03_28_161536_add_traffic_fields_to_servers.php delete mode 100644 Xboard/database/seeders/DatabaseSeeder.php delete mode 100644 Xboard/database/seeders/OriginV2bMigrationsTableSeeder.php delete mode 100644 Xboard/docs/en/development/device-limit.md delete mode 100644 Xboard/docs/en/development/performance.md delete mode 100644 Xboard/docs/en/development/plugin-development-guide.md delete mode 100644 Xboard/docs/en/installation/1panel.md delete mode 100644 Xboard/docs/en/installation/aapanel-docker.md delete mode 100644 Xboard/docs/en/installation/aapanel.md delete mode 100644 Xboard/docs/en/installation/docker-compose.md delete mode 100644 Xboard/docs/en/migration/config.md delete mode 100644 Xboard/docs/en/migration/v2board-1.7.3.md delete mode 100644 Xboard/docs/en/migration/v2board-1.7.4.md delete mode 100644 Xboard/docs/en/migration/v2board-dev.md delete mode 100644 Xboard/docs/images/admin.png delete mode 100644 Xboard/docs/images/user.png delete mode 100644 Xboard/init.sh delete mode 100644 Xboard/package.json delete mode 100644 Xboard/phpstan.neon delete mode 100644 Xboard/public/index.php delete mode 100644 Xboard/public/robots.txt delete mode 100644 Xboard/public/theme/.gitignore delete mode 100644 Xboard/public/web.config delete mode 100644 Xboard/resources/js/app.js delete mode 100644 Xboard/resources/js/bootstrap.js delete mode 100644 Xboard/resources/lang/en-US.json delete mode 100644 Xboard/resources/lang/ru-RU.json delete mode 100644 Xboard/resources/lang/zh-CN.json delete mode 100644 Xboard/resources/lang/zh-TW.json delete mode 100644 Xboard/resources/rules/.gitignore delete mode 100644 Xboard/resources/rules/app.clash.yaml delete mode 100644 Xboard/resources/rules/default.clash.yaml delete mode 100644 Xboard/resources/rules/default.sing-box.json delete mode 100644 Xboard/resources/rules/default.surfboard.conf delete mode 100644 Xboard/resources/rules/default.surge.conf delete mode 100644 Xboard/resources/sass/app.scss delete mode 100644 Xboard/resources/views/admin.blade.php delete mode 100644 Xboard/resources/views/client/subscribe.blade.php delete mode 100644 Xboard/resources/views/errors/500.blade.php delete mode 100644 Xboard/resources/views/mail/classic/mailLogin.blade.php delete mode 100644 Xboard/resources/views/mail/classic/notify.blade.php delete mode 100644 Xboard/resources/views/mail/classic/remindExpire.blade.php delete mode 100644 Xboard/resources/views/mail/classic/remindTraffic.blade.php delete mode 100644 Xboard/resources/views/mail/classic/verify.blade.php delete mode 100644 Xboard/resources/views/mail/default/mailLogin.blade.php delete mode 100644 Xboard/resources/views/mail/default/notify.blade.php delete mode 100644 Xboard/resources/views/mail/default/remindExpire.blade.php delete mode 100644 Xboard/resources/views/mail/default/remindTraffic.blade.php delete mode 100644 Xboard/resources/views/mail/default/verify.blade.php delete mode 100644 Xboard/routes/channels.php delete mode 100644 Xboard/routes/console.php delete mode 100644 Xboard/routes/web.php delete mode 100644 Xboard/storage/backup/.gitignore delete mode 100644 Xboard/storage/debugbar/.gitignore delete mode 100644 Xboard/storage/framework/cache/.gitignore delete mode 100644 Xboard/storage/framework/sessions/.gitignore delete mode 100644 Xboard/storage/framework/views/.gitignore delete mode 100644 Xboard/storage/logs/.gitignore delete mode 100644 Xboard/storage/theme/.gitignore delete mode 100644 Xboard/storage/tmp/.gitignore delete mode 100644 Xboard/storage/views/.gitignore delete mode 100644 Xboard/theme/.gitignore delete mode 100644 Xboard/theme/Xboard/assets/images/background.svg delete mode 100644 Xboard/theme/Xboard/assets/umi.js delete mode 100644 Xboard/theme/Xboard/config.json delete mode 100644 Xboard/theme/Xboard/dashboard.blade.php delete mode 100644 Xboard/theme/Xboard/env.example.js delete mode 100644 Xboard/theme/Xboard/env.js delete mode 100644 Xboard/theme/Xboard/index.html delete mode 100644 Xboard/update.sh rename {Xboard/plugins => plugins}/.gitignore (100%) rename {Xboard/plugins => plugins}/UserOnlineDevices/Controllers/UserOnlineDevicesController.php (100%) rename {Xboard/plugins => plugins}/UserOnlineDevices/Plugin.php (100%) rename {Xboard/plugins => plugins}/UserOnlineDevices/README.md (100%) rename {Xboard/plugins => plugins}/UserOnlineDevices/config.json (100%) rename {Xboard/plugins => plugins}/UserOnlineDevices/resources/views/admin-users.blade.php (90%) rename {Xboard/plugins => plugins}/UserOnlineDevices/resources/views/panel.blade.php (100%) rename {Xboard/plugins => plugins}/UserOnlineDevices/resources/views/userstatus.blade.php (100%) rename {Xboard/plugins => plugins}/UserOnlineDevices/routes/api.php (100%) rename {Xboard/plugins => plugins}/UserOnlineDevices/routes/web.php (100%) diff --git a/Xboard/.docker/.data/.gitignore b/Xboard/.docker/.data/.gitignore deleted file mode 100644 index 0377233..0000000 --- a/Xboard/.docker/.data/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -* -!.gitignore -!redis \ No newline at end of file diff --git a/Xboard/.docker/.data/redis/.gitignore b/Xboard/.docker/.data/redis/.gitignore deleted file mode 100644 index c96a04f..0000000 --- a/Xboard/.docker/.data/redis/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/Xboard/.docker/supervisor/supervisord.conf b/Xboard/.docker/supervisor/supervisord.conf deleted file mode 100644 index c516e4e..0000000 --- a/Xboard/.docker/supervisor/supervisord.conf +++ /dev/null @@ -1,81 +0,0 @@ -[supervisord] -nodaemon=true -user=root -logfile=/dev/stdout -logfile_maxbytes=0 -pidfil=/www/storage/logs/supervisor/supervisord.pid -loglevel=info - -[program:octane] -process_name=%(program_name)s_%(process_num)02d -command=php /www/artisan octane:start --host=0.0.0.0 --port=7001 -autostart=%(ENV_ENABLE_WEB)s -autorestart=true -user=www -redirect_stderr=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stdout_logfile_backups=0 -numprocs=1 -stopwaitsecs=10 -stopsignal=QUIT -stopasgroup=true -killasgroup=true -priority=100 - -[program:horizon] -process_name=%(program_name)s_%(process_num)02d -command=php /www/artisan horizon -autostart=%(ENV_ENABLE_HORIZON)s -autorestart=true -user=www -redirect_stderr=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stdout_logfile_backups=0 -numprocs=1 -stopwaitsecs=3 -stopsignal=SIGINT -stopasgroup=true -killasgroup=true -priority=200 - -[program:redis] -process_name=%(program_name)s_%(process_num)02d -command=redis-server --dir /data - --dbfilename dump.rdb - --save 900 1 - --save 300 10 - --save 60 10000 - --unixsocket /data/redis.sock - --unixsocketperm 777 -autostart=%(ENV_ENABLE_REDIS)s -autorestart=true -user=redis -redirect_stderr=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stdout_logfile_backups=0 -numprocs=1 -stopwaitsecs=3 -stopsignal=TERM -stopasgroup=true -killasgroup=true -priority=300 - -[program:ws-server] -process_name=%(program_name)s_%(process_num)02d -command=php /www/artisan ws-server start -autostart=%(ENV_ENABLE_WS_SERVER)s -autorestart=true -user=www -redirect_stderr=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stdout_logfile_backups=0 -numprocs=1 -stopwaitsecs=5 -stopsignal=SIGINT -stopasgroup=true -killasgroup=true -priority=400 \ No newline at end of file diff --git a/Xboard/.dockerignore b/Xboard/.dockerignore deleted file mode 100644 index 3b0ee11..0000000 --- a/Xboard/.dockerignore +++ /dev/null @@ -1,26 +0,0 @@ -/node_modules -/config/v2board.php -/public/hot -/public/storage -/public/env.example.js -/storage/*.key -/vendor -.env -.env.backup -.phpunit.result.cache -.idea -.lock -Homestead.json -Homestead.yaml -npm-debug.log -yarn-error.log -composer.phar -composer.lock -yarn.lock -docker-compose.yml -.DS_Store -/docker -storage/laravels.conf -storage/laravels.pid -storage/laravels-timer-process.pid -/frontend diff --git a/Xboard/.editorconfig b/Xboard/.editorconfig deleted file mode 100644 index 6537ca4..0000000 --- a/Xboard/.editorconfig +++ /dev/null @@ -1,15 +0,0 @@ -root = true - -[*] -charset = utf-8 -end_of_line = lf -insert_final_newline = true -indent_style = space -indent_size = 4 -trim_trailing_whitespace = true - -[*.md] -trim_trailing_whitespace = false - -[*.{yml,yaml}] -indent_size = 2 diff --git a/Xboard/.env.example b/Xboard/.env.example deleted file mode 100644 index 251858b..0000000 --- a/Xboard/.env.example +++ /dev/null @@ -1,41 +0,0 @@ -APP_NAME=XBoard -APP_ENV=production -APP_KEY=base64:PZXk5vTuTinfeEVG5FpYv2l6WEhLsyvGpiWK7IgJJ60= -APP_DEBUG=false -APP_URL=http://localhost - -LOG_CHANNEL=stack - -DB_CONNECTION=mysql -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_DATABASE=xboard -DB_USERNAME=root -DB_PASSWORD= - -REDIS_HOST=127.0.0.1 -REDIS_PASSWORD=null -REDIS_PORT=6379 - -BROADCAST_DRIVER=log -CACHE_DRIVER=redis -QUEUE_CONNECTION=redis - -MAIL_DRIVER=smtp -MAIL_HOST=smtp.mailtrap.io -MAIL_PORT=2525 -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_ENCRYPTION=null -MAIL_FROM_ADDRESS=null -MAIL_FROM_NAME=null -MAILGUN_DOMAIN= -MAILGUN_SECRET= - -# google cloud storage -ENABLE_AUTO_BACKUP_AND_UPDATE=false -GOOGLE_CLOUD_KEY_FILE=config/googleCloudStorageKey.json -GOOGLE_CLOUD_STORAGE_BUCKET= - -# Prevent reinstallation -INSTALLED=false \ No newline at end of file diff --git a/Xboard/.gitattributes b/Xboard/.gitattributes deleted file mode 100644 index 967315d..0000000 --- a/Xboard/.gitattributes +++ /dev/null @@ -1,5 +0,0 @@ -* text=auto -*.css linguist-vendored -*.scss linguist-vendored -*.js linguist-vendored -CHANGELOG.md export-ignore diff --git a/Xboard/.github/ISSUE_TEMPLATE/bug-report.md b/Xboard/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index 271b8df..0000000 --- a/Xboard/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -name: 🐛 问题反馈 | Bug Report -about: 提交使用过程中遇到的问题 | Report an issue -title: "Bug Report:" -labels: '🐛 bug' -assignees: '' ---- - - - - -> ⚠️ 请务必按照模板填写完整信息,没有详细描述的issue可能会被忽略或关闭 -> ⚠️ Please follow the template to provide complete information, issues without detailed description may be ignored or closed - -**基本信息 | Basic Info** -```yaml -XBoard版本 | Version: -部署方式 | Deployment: [Docker/手动部署] -PHP版本 | Version: -数据库 | Database: -``` - -**问题描述 | Description** - - - -**复现步骤 | Steps** - -1. -2. - -**相关截图 | Screenshots** - - -**日志信息 | Logs** - -```log -// 粘贴日志内容到这里 -``` \ No newline at end of file diff --git a/Xboard/.github/ISSUE_TEMPLATE/feature-request.md b/Xboard/.github/ISSUE_TEMPLATE/feature-request.md deleted file mode 100644 index 5428f95..0000000 --- a/Xboard/.github/ISSUE_TEMPLATE/feature-request.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: ✨ 功能请求 | Feature Request -about: 提交新功能建议或改进意见 | Suggest an idea -title: "Feature Request:" -labels: '✨ enhancement' -assignees: '' ---- - -> ⚠️ 请务必按照模板详细描述你的需求,没有详细描述的issue可能会被忽略或关闭 -> ⚠️ Please follow the template to describe your request in detail, issues without detailed description may be ignored or closed - -**需求描述 | Description** - - - -**使用场景 | Use Case** - - - -**功能建议 | Suggestion** - -```yaml -功能形式 | Type: [新功能/功能优化/界面改进] -预期效果 | Expected: -``` - -**补充说明 | Additional** - \ No newline at end of file diff --git a/Xboard/.github/workflows/docker-publish.yml b/Xboard/.github/workflows/docker-publish.yml deleted file mode 100644 index 9898924..0000000 --- a/Xboard/.github/workflows/docker-publish.yml +++ /dev/null @@ -1,100 +0,0 @@ -name: Docker Build and Publish - -on: - push: - branches: ["master", "new-dev"] - workflow_dispatch: - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - id-token: write - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: 'arm64,amd64' - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - with: - platforms: linux/amd64,linux/arm64 - driver-opts: | - image=moby/buildkit:v0.20.0 - network=host - - - name: Free Disk Space - run: | - sudo rm -rf /usr/share/dotnet - sudo rm -rf /usr/local/lib/android - sudo rm -rf /opt/ghc - sudo rm -rf /opt/hostedtoolcache/CodeQL - sudo docker image prune -af - - - name: Login to registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=sha,format=short,prefix=,enable=true - type=raw,value=new,enable=${{ github.ref == 'refs/heads/master' }} - type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }} - type=raw,value=${{ steps.get_version.outputs.version }} - labels: | - org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} - org.opencontainers.image.revision=${{ github.sha }} - - - name: Get version - id: get_version - run: echo "version=$(git describe --tags --always)" >> $GITHUB_OUTPUT - - - name: Update version in app.php - run: | - VERSION=$(date '+%Y%m%d')-$(git rev-parse --short HEAD) - sed -i "s/'version' => '.*'/'version' => '$VERSION'/g" config/app.php - echo "Updated version to: $VERSION" - - - name: Build and push - id: build-and-push - uses: docker/build-push-action@v5 - with: - context: . - push: true - platforms: linux/amd64,linux/arm64 - cache-from: type=gha - cache-to: type=gha,mode=max - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - build-args: | - BUILDKIT_INLINE_CACHE=1 - BUILDKIT_MULTI_PLATFORM=1 - CACHEBUST=${{ github.sha }} - REPO_URL=https://github.com/${{ github.repository }} - BRANCH_NAME=${{ github.ref_name }} - provenance: false - outputs: type=registry,push=true - allow: | - network.host - diff --git a/Xboard/.gitignore b/Xboard/.gitignore deleted file mode 100644 index 2715539..0000000 --- a/Xboard/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -/node_modules -/config/v2board.php -/config/googleCloudStorageKey.json -/public/hot -/public/storage -/public/env.example.js -*.user.ini -/storage/*.key -/vendor -.env -.env.backup -.phpunit.result.cache -.idea -.lock -Homestead.json -Homestead.yaml -npm-debug.log -yarn-error.log -composer.phar -composer.lock -yarn.lock -docker-compose.yml -.DS_Store -/docker -storage/laravels.conf -storage/laravels.pid -storage/update_pending -storage/laravels-timer-process.pid -cli-php.ini -frontend -docker-compose.yaml -bun.lockb -compose.yaml -.scribe \ No newline at end of file diff --git a/Xboard/.gitmodules b/Xboard/.gitmodules deleted file mode 100644 index 060ba10..0000000 --- a/Xboard/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "public/assets/admin"] - path = public/assets/admin - url = https://github.com/cedar2025/xboard-admin-dist.git diff --git a/Xboard/Dockerfile b/Xboard/Dockerfile deleted file mode 100644 index ba41fe1..0000000 --- a/Xboard/Dockerfile +++ /dev/null @@ -1,47 +0,0 @@ -FROM phpswoole/swoole:php8.2-alpine - -COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ - -# Install PHP extensions one by one with lower optimization level for ARM64 compatibility -RUN CFLAGS="-O0" install-php-extensions pcntl && \ - CFLAGS="-O0 -g0" install-php-extensions bcmath && \ - install-php-extensions zip && \ - install-php-extensions redis && \ - apk --no-cache add shadow sqlite mysql-client mysql-dev mariadb-connector-c git patch supervisor redis && \ - addgroup -S -g 1000 www && adduser -S -G www -u 1000 www && \ - (getent group redis || addgroup -S redis) && \ - (getent passwd redis || adduser -S -G redis -H -h /data redis) - -WORKDIR /www - -COPY .docker / - -# Add build arguments -ARG CACHEBUST -ARG REPO_URL -ARG BRANCH_NAME - -RUN echo "Attempting to clone branch: ${BRANCH_NAME} from ${REPO_URL} with CACHEBUST: ${CACHEBUST}" && \ - rm -rf ./* && \ - rm -rf .git && \ - git config --global --add safe.directory /www && \ - git clone --depth 1 --branch ${BRANCH_NAME} ${REPO_URL} . && \ - git submodule update --init --recursive --force - -COPY .docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf - -RUN composer install --no-cache --no-dev \ - && php artisan storage:link \ - && cp -r plugins/ /opt/default-plugins/ \ - && chown -R www:www /www \ - && chmod -R 775 /www \ - && mkdir -p /data \ - && chown redis:redis /data - -ENV ENABLE_WEB=true \ - ENABLE_HORIZON=true \ - ENABLE_REDIS=false \ - ENABLE_WS_SERVER=false - -EXPOSE 7001 -CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] diff --git a/Xboard/LICENSE b/Xboard/LICENSE deleted file mode 100644 index 8d23873..0000000 --- a/Xboard/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019 Tokumeikoi - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/Xboard/README.md b/Xboard/README.md deleted file mode 100644 index 4d6a618..0000000 --- a/Xboard/README.md +++ /dev/null @@ -1,100 +0,0 @@ -# Xboard - -
- -[![Telegram](https://img.shields.io/badge/Telegram-Channel-blue)](https://t.me/XboardOfficial) -![PHP](https://img.shields.io/badge/PHP-8.2+-green.svg) -![MySQL](https://img.shields.io/badge/MySQL-5.7+-blue.svg) -[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) - -
- -## 📖 Introduction - -Xboard is a modern panel system built on Laravel 11, focusing on providing a clean and efficient user experience. - -## ✨ Features - -- 🚀 Built with Laravel 12 + Octane for significant performance gains -- 🎨 Redesigned admin interface (React + Shadcn UI) -- 📱 Modern user frontend (Vue3 + TypeScript) -- 🐳 Ready-to-use Docker deployment solution -- 🎯 Optimized system architecture for better maintainability - -## 🚀 Quick Start - -```bash -git clone -b compose --depth 1 https://github.com/cedar2025/Xboard && \ -cd Xboard && \ -docker compose run -it --rm \ - -e ENABLE_SQLITE=true \ - -e ENABLE_REDIS=true \ - -e ADMIN_ACCOUNT=admin@demo.com \ - web php artisan xboard:install && \ -docker compose up -d -``` - -> After installation, visit: http://SERVER_IP:7001 -> ⚠️ Make sure to save the admin credentials shown during installation - -## 📖 Documentation - -### 🔄 Upgrade Notice -> 🚨 **Important:** This version involves significant changes. Please strictly follow the upgrade documentation and backup your database before upgrading. Note that upgrading and migration are different processes, do not confuse them. - -### Development Guides -- [Plugin Development Guide](./docs/en/development/plugin-development-guide.md) - Complete guide for developing XBoard plugins - -### Deployment Guides -- [Deploy with 1Panel](./docs/en/installation/1panel.md) -- [Deploy with Docker Compose](./docs/en/installation/docker-compose.md) -- [Deploy with aaPanel](./docs/en/installation/aapanel.md) -- [Deploy with aaPanel + Docker](./docs/en/installation/aapanel-docker.md) (Recommended) - -### Migration Guides -- [Migrate from v2board dev](./docs/en/migration/v2board-dev.md) -- [Migrate from v2board 1.7.4](./docs/en/migration/v2board-1.7.4.md) -- [Migrate from v2board 1.7.3](./docs/en/migration/v2board-1.7.3.md) - -## 🛠️ Tech Stack - -- Backend: Laravel 11 + Octane -- Admin Panel: React + Shadcn UI + TailwindCSS -- User Frontend: Vue3 + TypeScript + NaiveUI -- Deployment: Docker + Docker Compose -- Caching: Redis + Octane Cache - -## 📷 Preview -![Admin Preview](./docs/images/admin.png) - -![User Preview](./docs/images/user.png) - -## ⚠️ Disclaimer - -This project is for learning and communication purposes only. Users are responsible for any consequences of using this project. - -## 🌟 Maintenance Notice - -This project is currently under light maintenance. We will: -- Fix critical bugs and security issues -- Review and merge important pull requests -- Provide necessary updates for compatibility - -However, new feature development may be limited. - -## 🔔 Important Notes - -1. Restart required after modifying admin path: -```bash -docker compose restart -``` - -2. For aaPanel installations, restart the Octane daemon process - -## 🤝 Contributing - -Issues and Pull Requests are welcome to help improve the project. - -## 📈 Star History - -[![Stargazers over time](https://starchart.cc/cedar2025/Xboard.svg)](https://starchart.cc/cedar2025/Xboard) diff --git a/Xboard/app/Console/Commands/BackupDatabase.php b/Xboard/app/Console/Commands/BackupDatabase.php deleted file mode 100644 index 14c82ee..0000000 --- a/Xboard/app/Console/Commands/BackupDatabase.php +++ /dev/null @@ -1,100 +0,0 @@ -argument('upload'); - // 如果是上传到云端则判断是否存在必要配置 - if($isUpload){ - $requiredConfigs = ['database.connections.mysql', 'cloud_storage.google_cloud.key_file', 'cloud_storage.google_cloud.storage_bucket']; - foreach ($requiredConfigs as $config) { - if (blank(config($config))) { - $this->error("❌:缺少必要配置项: $config , 取消备份"); - return; - } - } - } - - // 数据库备份逻辑 - try{ - if (config('database.default') === 'mysql'){ - $databaseBackupPath = storage_path('backup/' . now()->format('Y-m-d_H-i-s') . '_' . config('database.connections.mysql.database') . '_database_backup.sql'); - $this->info("1️⃣:开始备份Mysql"); - \Spatie\DbDumper\Databases\MySql::create() - ->setHost(config('database.connections.mysql.host')) - ->setPort(config('database.connections.mysql.port')) - ->setDbName(config('database.connections.mysql.database')) - ->setUserName(config('database.connections.mysql.username')) - ->setPassword(config('database.connections.mysql.password')) - ->dumpToFile($databaseBackupPath); - $this->info("2️⃣:Mysql备份完成"); - }elseif(config('database.default') === 'sqlite'){ - $databaseBackupPath = storage_path('backup/' . now()->format('Y-m-d_H-i-s') . '_sqlite' . '_database_backup.sql'); - $this->info("1️⃣:开始备份Sqlite"); - \Spatie\DbDumper\Databases\Sqlite::create() - ->setDbName(config('database.connections.sqlite.database')) - ->dumpToFile($databaseBackupPath); - $this->info("2️⃣:Sqlite备份完成"); - }else{ - $this->error('备份失败,你的数据库不是sqlite或者mysql'); - return; - } - $this->info('3️⃣:开始压缩备份文件'); - // 使用 gzip 压缩备份文件 - $compressedBackupPath = $databaseBackupPath . '.gz'; - $gzipCommand = new Process(["gzip", "-c", $databaseBackupPath]); - $gzipCommand->run(); - - // 检查压缩是否成功 - if ($gzipCommand->isSuccessful()) { - // 压缩成功,你可以删除原始备份文件 - file_put_contents($compressedBackupPath, $gzipCommand->getOutput()); - $this->info('4️⃣:文件压缩成功'); - unlink($databaseBackupPath); - } else { - // 压缩失败,处理错误 - echo $gzipCommand->getErrorOutput(); - $this->error('😔:文件压缩失败'); - unlink($databaseBackupPath); - return; - } - if (!$isUpload){ - $this->info("🎉:数据库成功备份到:$compressedBackupPath"); - }else{ - // 传到云盘 - $this->info("5️⃣:开始将备份上传到Google Cloud"); - // Google Cloud Storage 配置 - $storage = new StorageClient([ - 'keyFilePath' => config('cloud_storage.google_cloud.key_file'), - ]); - $bucket = $storage->bucket(config('cloud_storage.google_cloud.storage_bucket')); - $objectName = 'backup/' . now()->format('Y-m-d_H-i-s') . '_database_backup.sql.gz'; - // 上传文件 - $bucket->upload(fopen($compressedBackupPath, 'r'), [ - 'name' => $objectName, - ]); - - // 输出文件链接 - Log::channel('backup')->info("🎉:数据库备份已上传到 Google Cloud Storage: $objectName"); - $this->info("🎉:数据库备份已上传到 Google Cloud Storage: $objectName"); - File::delete($compressedBackupPath); - } - }catch(\Exception $e){ - Log::channel('backup')->error("😔:数据库备份失败 \n" . $e); - $this->error("😔:数据库备份失败\n" . $e); - File::delete($compressedBackupPath); - } - } -} diff --git a/Xboard/app/Console/Commands/CheckCommission.php b/Xboard/app/Console/Commands/CheckCommission.php deleted file mode 100644 index 0eabc74..0000000 --- a/Xboard/app/Console/Commands/CheckCommission.php +++ /dev/null @@ -1,129 +0,0 @@ -autoCheck(); - $this->autoPayCommission(); - } - - public function autoCheck() - { - if ((int)admin_setting('commission_auto_check_enable', 1)) { - Order::where('commission_status', 0) - ->where('invite_user_id', '!=', NULL) - ->where('status', 3) - ->where('updated_at', '<=', strtotime('-3 day', time())) - ->update([ - 'commission_status' => 1 - ]); - } - } - - public function autoPayCommission() - { - $orders = Order::where('commission_status', 1) - ->where('invite_user_id', '!=', NULL) - ->get(); - foreach ($orders as $order) { - try{ - DB::beginTransaction(); - if (!$this->payHandle($order->invite_user_id, $order)) { - DB::rollBack(); - continue; - } - $order->commission_status = 2; - if (!$order->save()) { - DB::rollBack(); - continue; - } - DB::commit(); - } catch (\Exception $e){ - DB::rollBack(); - throw $e; - } - } - } - - public function payHandle($inviteUserId, Order $order) - { - $level = 3; - if ((int)admin_setting('commission_distribution_enable', 0)) { - $commissionShareLevels = [ - 0 => (int)admin_setting('commission_distribution_l1'), - 1 => (int)admin_setting('commission_distribution_l2'), - 2 => (int)admin_setting('commission_distribution_l3') - ]; - } else { - $commissionShareLevels = [ - 0 => 100 - ]; - } - for ($l = 0; $l < $level; $l++) { - $inviter = User::find($inviteUserId); - if (!$inviter) continue; - if (!isset($commissionShareLevels[$l])) continue; - $commissionBalance = $order->commission_balance * ($commissionShareLevels[$l] / 100); - if (!$commissionBalance) continue; - if ((int)admin_setting('withdraw_close_enable', 0)) { - $inviter->increment('balance', $commissionBalance); - } else { - $inviter->increment('commission_balance', $commissionBalance); - } - if (!$inviter->save()) { - DB::rollBack(); - return false; - } - CommissionLog::create([ - 'invite_user_id' => $inviteUserId, - 'user_id' => $order->user_id, - 'trade_no' => $order->trade_no, - 'order_amount' => $order->total_amount, - 'get_amount' => $commissionBalance - ]); - $inviteUserId = $inviter->invite_user_id; - // update order actual commission balance - $order->actual_commission_balance = $order->actual_commission_balance + $commissionBalance; - } - return true; - } - -} diff --git a/Xboard/app/Console/Commands/CheckOrder.php b/Xboard/app/Console/Commands/CheckOrder.php deleted file mode 100644 index 7d03f58..0000000 --- a/Xboard/app/Console/Commands/CheckOrder.php +++ /dev/null @@ -1,53 +0,0 @@ -orderBy('created_at', 'ASC') - ->lazyById(200) - ->each(function ($order) { - OrderHandleJob::dispatch($order->trade_no); - }); - } -} diff --git a/Xboard/app/Console/Commands/CheckServer.php b/Xboard/app/Console/Commands/CheckServer.php deleted file mode 100644 index 2d3dae9..0000000 --- a/Xboard/app/Console/Commands/CheckServer.php +++ /dev/null @@ -1,64 +0,0 @@ -checkOffline(); - } - - private function checkOffline() - { - $servers = ServerService::getAllServers(); - foreach ($servers as $server) { - if ($server['parent_id']) continue; - if ($server['last_check_at'] && (time() - $server['last_check_at']) > 1800) { - $telegramService = new TelegramService(); - $message = sprintf( - "节点掉线通知\r\n----\r\n节点名称:%s\r\n节点地址:%s\r\n", - $server['name'], - $server['host'] - ); - $telegramService->sendMessageWithAdmin($message); - Cache::forget(CacheKey::get(sprintf("SERVER_%s_LAST_CHECK_AT", strtoupper($server['type'])), $server->id)); - } - } - } -} diff --git a/Xboard/app/Console/Commands/CheckTicket.php b/Xboard/app/Console/Commands/CheckTicket.php deleted file mode 100644 index 51d4539..0000000 --- a/Xboard/app/Console/Commands/CheckTicket.php +++ /dev/null @@ -1,51 +0,0 @@ -where('updated_at', '<=', time() - 24 * 3600) - ->where('reply_status', 0) - ->lazyById(200) - ->each(function ($ticket) { - if ($ticket->user_id === $ticket->last_reply_user_id) return; - $ticket->status = Ticket::STATUS_CLOSED; - $ticket->save(); - }); - } -} diff --git a/Xboard/app/Console/Commands/CheckTrafficExceeded.php b/Xboard/app/Console/Commands/CheckTrafficExceeded.php deleted file mode 100644 index 35a1db6..0000000 --- a/Xboard/app/Console/Commands/CheckTrafficExceeded.php +++ /dev/null @@ -1,63 +0,0 @@ -whereIn('id', $pendingUserIds) - ->whereRaw('u + d >= transfer_enable') - ->where('transfer_enable', '>', 0) - ->where('banned', 0) - ->select(['id', 'group_id']) - ->get(); - - if ($exceededUsers->isEmpty()) { - return; - } - - $groupedUsers = $exceededUsers->groupBy('group_id'); - $notifiedCount = 0; - - foreach ($groupedUsers as $groupId => $users) { - if (!$groupId) { - continue; - } - - $userIdsInGroup = $users->pluck('id')->toArray(); - $servers = Server::whereJsonContains('group_ids', (string) $groupId)->get(); - - foreach ($servers as $server) { - if (!NodeSyncService::isNodeOnline($server->id)) { - continue; - } - - NodeSyncService::push($server->id, 'sync.user.delta', [ - 'action' => 'remove', - 'users' => array_map(fn($id) => ['id' => $id], $userIdsInGroup), - ]); - $notifiedCount++; - } - } - - $this->info("Checked " . count($pendingUserIds) . " users, notified {$notifiedCount} nodes for " . $exceededUsers->count() . " exceeded users."); - } -} diff --git a/Xboard/app/Console/Commands/ClearUser.php b/Xboard/app/Console/Commands/ClearUser.php deleted file mode 100644 index cd64abe..0000000 --- a/Xboard/app/Console/Commands/ClearUser.php +++ /dev/null @@ -1,51 +0,0 @@ -where('transfer_enable', 0) - ->where('expired_at', 0) - ->where('last_login_at', NULL); - $count = $builder->count(); - if ($builder->delete()) { - $this->info("已删除{$count}位没有任何数据的用户"); - } - } -} diff --git a/Xboard/app/Console/Commands/HookList.php b/Xboard/app/Console/Commands/HookList.php deleted file mode 100644 index cec2bfd..0000000 --- a/Xboard/app/Console/Commands/HookList.php +++ /dev/null @@ -1,42 +0,0 @@ -filter(fn($f) => Str::endsWith($f, '.php')); - foreach ($files as $file) { - $content = @file_get_contents($file); - if ($content && preg_match_all($pattern, $content, $matches)) { - foreach ($matches[2] as $hook) { - $hooks->push($hook); - } - } - } - } - $hooks = $hooks->unique()->sort()->values(); - if ($hooks->isEmpty()) { - $this->info('未扫描到任何 hook'); - } else { - $this->info('All Supported Hooks:'); - foreach ($hooks as $hook) { - $this->line(' ' . $hook); - } - } - } -} \ No newline at end of file diff --git a/Xboard/app/Console/Commands/MigrateFromV2b.php b/Xboard/app/Console/Commands/MigrateFromV2b.php deleted file mode 100644 index ace9c85..0000000 --- a/Xboard/app/Console/Commands/MigrateFromV2b.php +++ /dev/null @@ -1,186 +0,0 @@ -argument('version'); - if($version === 'config'){ - $this->MigrateV2ConfigToV2Settings(); - return; - } - - // Define your SQL commands based on versions - $sqlCommands = [ - 'dev231027' => [ - // SQL commands for version Dev 2023/10/27 - 'ALTER TABLE v2_order ADD COLUMN surplus_order_ids TEXT NULL;', - 'ALTER TABLE v2_plan DROP COLUMN daily_unit_price, DROP COLUMN transfer_unit_price;', - 'ALTER TABLE v2_server_hysteria DROP COLUMN ignore_client_bandwidth, DROP COLUMN obfs_type;' - ], - '1.7.4' => [ - 'CREATE TABLE `v2_server_vless` ( - `id` INT AUTO_INCREMENT PRIMARY KEY, - `group_id` TEXT NOT NULL, - `route_id` TEXT NULL, - `name` VARCHAR(255) NOT NULL, - `parent_id` INT NULL, - `host` VARCHAR(255) NOT NULL, - `port` INT NOT NULL, - `server_port` INT NOT NULL, - `tls` BOOLEAN NOT NULL, - `tls_settings` TEXT NULL, - `flow` VARCHAR(64) NULL, - `network` VARCHAR(11) NOT NULL, - `network_settings` TEXT NULL, - `tags` TEXT NULL, - `rate` VARCHAR(11) NOT NULL, - `show` BOOLEAN DEFAULT 0, - `sort` INT NULL, - `created_at` INT NOT NULL, - `updated_at` INT NOT NULL - );' - ], - '1.7.3' => [ - 'ALTER TABLE `v2_stat_order` RENAME TO `v2_stat`;', - "ALTER TABLE `v2_stat` CHANGE COLUMN order_amount paid_total INT COMMENT '订单合计';", - "ALTER TABLE `v2_stat` CHANGE COLUMN order_count paid_count INT COMMENT '邀请佣金';", - "ALTER TABLE `v2_stat` CHANGE COLUMN commission_amount commission_total INT COMMENT '佣金合计';", - "ALTER TABLE `v2_stat` - ADD COLUMN order_count INT NULL, - ADD COLUMN order_total INT NULL, - ADD COLUMN register_count INT NULL, - ADD COLUMN invite_count INT NULL, - ADD COLUMN transfer_used_total VARCHAR(32) NULL; - ", - "CREATE TABLE `v2_log` ( - `id` INT AUTO_INCREMENT PRIMARY KEY, - `title` TEXT NOT NULL, - `level` VARCHAR(11) NULL, - `host` VARCHAR(255) NULL, - `uri` VARCHAR(255) NOT NULL, - `method` VARCHAR(11) NOT NULL, - `data` TEXT NULL, - `ip` VARCHAR(128) NULL, - `context` TEXT NULL, - `created_at` INT NOT NULL, - `updated_at` INT NOT NULL - );", - 'CREATE TABLE `v2_server_hysteria` ( - `id` INT AUTO_INCREMENT PRIMARY KEY, - `group_id` VARCHAR(255) NOT NULL, - `route_id` VARCHAR(255) NULL, - `name` VARCHAR(255) NOT NULL, - `parent_id` INT NULL, - `host` VARCHAR(255) NOT NULL, - `port` VARCHAR(11) NOT NULL, - `server_port` INT NOT NULL, - `tags` VARCHAR(255) NULL, - `rate` VARCHAR(11) NOT NULL, - `show` BOOLEAN DEFAULT FALSE, - `sort` INT NULL, - `up_mbps` INT NOT NULL, - `down_mbps` INT NOT NULL, - `server_name` VARCHAR(64) NULL, - `insecure` BOOLEAN DEFAULT FALSE, - `created_at` INT NOT NULL, - `updated_at` INT NOT NULL - );', - "CREATE TABLE `v2_server_vless` ( - `id` INT AUTO_INCREMENT PRIMARY KEY, - `group_id` TEXT NOT NULL, - `route_id` TEXT NULL, - `name` VARCHAR(255) NOT NULL, - `parent_id` INT NULL, - `host` VARCHAR(255) NOT NULL, - `port` INT NOT NULL, - `server_port` INT NOT NULL, - `tls` BOOLEAN NOT NULL, - `tls_settings` TEXT NULL, - `flow` VARCHAR(64) NULL, - `network` VARCHAR(11) NOT NULL, - `network_settings` TEXT NULL, - `tags` TEXT NULL, - `rate` VARCHAR(11) NOT NULL, - `show` BOOLEAN DEFAULT FALSE, - `sort` INT NULL, - `created_at` INT NOT NULL, - `updated_at` INT NOT NULL - );", - ], - 'wyx2685' => [ - "ALTER TABLE `v2_plan` DROP COLUMN `device_limit`;", - "ALTER TABLE `v2_server_hysteria` DROP COLUMN `version`, DROP COLUMN `obfs`, DROP COLUMN `obfs_password`;", - "ALTER TABLE `v2_server_trojan` DROP COLUMN `network`, DROP COLUMN `network_settings`;", - "ALTER TABLE `v2_user` DROP COLUMN `device_limit`;" - ] - ]; - - if (!$version) { - $version = $this->choice('请选择你迁移前的V2board版本:', array_keys($sqlCommands)); - } - - if (array_key_exists($version, $sqlCommands)) { - - try { - foreach ($sqlCommands[$version] as $sqlCommand) { - // Execute SQL command - DB::statement($sqlCommand); - } - - $this->info('1️⃣、数据库差异矫正成功'); - - // 初始化数据库迁移 - $this->call('db:seed', ['--class' => 'OriginV2bMigrationsTableSeeder']); - $this->info('2️⃣、数据库迁移记录初始化成功'); - - $this->call('xboard:update'); - $this->info('3️⃣、更新成功'); - - $this->info("🎉:成功从 $version 迁移到Xboard"); - } catch (\Exception $e) { - // An error occurred, rollback the transaction - $this->error('迁移失败'. $e->getMessage() ); - } - - - } else { - $this->error("你所输入的版本未找到"); - } - } - - public function MigrateV2ConfigToV2Settings() - { - Artisan::call('config:clear'); - $configValue = config('v2board') ?? []; - - foreach ($configValue as $k => $v) { - // 检查记录是否已存在 - $existingSetting = Setting::where('name', $k)->first(); - - // 如果记录不存在,则插入 - if ($existingSetting) { - $this->warn("配置 {$k} 在数据库已经存在, 忽略"); - continue; - } - Setting::create([ - 'name' => $k, - 'value' => is_array($v)? json_encode($v) : $v, - ]); - $this->info("配置 {$k} 迁移成功"); - } - Artisan::call('config:cache'); - - $this->info('所有配置迁移完成'); - } -} diff --git a/Xboard/app/Console/Commands/NodeWebSocketServer.php b/Xboard/app/Console/Commands/NodeWebSocketServer.php deleted file mode 100644 index cbad533..0000000 --- a/Xboard/app/Console/Commands/NodeWebSocketServer.php +++ /dev/null @@ -1,34 +0,0 @@ -argument('action'); - - $argv[1] = $action; - if ($this->option('d')) { - $argv[2] = '-d'; - } - - $host = $this->option('host'); - $port = $this->option('port'); - - $worker = new NodeWorker($host, $port); - $worker->run(); - } -} diff --git a/Xboard/app/Console/Commands/ResetLog.php b/Xboard/app/Console/Commands/ResetLog.php deleted file mode 100644 index 89ddfb3..0000000 --- a/Xboard/app/Console/Commands/ResetLog.php +++ /dev/null @@ -1,48 +0,0 @@ -delete(); - StatServer::where('record_at', '<', strtotime('-2 month', time()))->delete(); - AdminAuditLog::where('created_at', '<', strtotime('-3 month', time()))->delete(); - } -} diff --git a/Xboard/app/Console/Commands/ResetPassword.php b/Xboard/app/Console/Commands/ResetPassword.php deleted file mode 100644 index 3e131f2..0000000 --- a/Xboard/app/Console/Commands/ResetPassword.php +++ /dev/null @@ -1,55 +0,0 @@ -argument('password') ; - $user = User::byEmail($this->argument('email'))->first(); - if (!$user) abort(500, '邮箱不存在'); - $password = $password ?? Helper::guid(false); - $user->password = password_hash($password, PASSWORD_DEFAULT); - $user->password_algo = null; - if (!$user->save()) abort(500, '重置失败'); - $this->info("!!!重置成功!!!"); - $this->info("新密码为:{$password},请尽快修改密码。"); - } -} diff --git a/Xboard/app/Console/Commands/ResetTraffic.php b/Xboard/app/Console/Commands/ResetTraffic.php deleted file mode 100644 index 5152077..0000000 --- a/Xboard/app/Console/Commands/ResetTraffic.php +++ /dev/null @@ -1,289 +0,0 @@ -option('fix-null'); - $force = $this->option('force'); - - $this->info('🚀 开始执行流量重置任务...'); - - if ($fixNull) { - $this->warn('🔧 修正模式 - 将重新计算next_reset_at为null的用户'); - } elseif ($force) { - $this->warn('⚡ 强制模式 - 将重新计算所有用户的重置时间'); - } - - try { - $result = $fixNull ? $this->performFix() : ($force ? $this->performForce() : $this->performReset()); - $this->displayResults($result, $fixNull || $force); - return self::SUCCESS; - - } catch (\Exception $e) { - $this->error("❌ 任务执行失败: {$e->getMessage()}"); - - Log::error('流量重置命令执行失败', [ - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - ]); - - return self::FAILURE; - } - } - - private function displayResults(array $result, bool $isSpecialMode): void - { - $this->info("✅ 任务完成!\n"); - - if ($isSpecialMode) { - $this->displayFixResults($result); - } else { - $this->displayExecutionResults($result); - } - } - - private function displayFixResults(array $result): void - { - $this->info("📊 修正结果统计:"); - $this->info("🔍 发现用户总数: {$result['total_found']}"); - $this->info("✅ 成功修正数量: {$result['total_fixed']}"); - $this->info("⏱️ 总执行时间: {$result['duration']} 秒"); - - if ($result['error_count'] > 0) { - $this->warn("⚠️ 错误数量: {$result['error_count']}"); - $this->warn("详细错误信息请查看日志"); - } else { - $this->info("✨ 无错误发生"); - } - - if ($result['total_found'] > 0) { - $avgTime = round($result['duration'] / $result['total_found'], 4); - $this->info("⚡ 平均处理速度: {$avgTime} 秒/用户"); - } - } - - - - private function displayExecutionResults(array $result): void - { - $this->info("📊 执行结果统计:"); - $this->info("👥 处理用户总数: {$result['total_processed']}"); - $this->info("🔄 重置用户数量: {$result['total_reset']}"); - $this->info("⏱️ 总执行时间: {$result['duration']} 秒"); - - if ($result['error_count'] > 0) { - $this->warn("⚠️ 错误数量: {$result['error_count']}"); - $this->warn("详细错误信息请查看日志"); - } else { - $this->info("✨ 无错误发生"); - } - - if ($result['total_processed'] > 0) { - $avgTime = round($result['duration'] / $result['total_processed'], 4); - $this->info("⚡ 平均处理速度: {$avgTime} 秒/用户"); - } - } - - private function performReset(): array - { - $startTime = microtime(true); - $totalResetCount = 0; - $errors = []; - - $users = $this->getResetQuery()->get(); - - if ($users->isEmpty()) { - $this->info("😴 当前没有需要重置的用户"); - return [ - 'total_processed' => 0, - 'total_reset' => 0, - 'error_count' => 0, - 'duration' => round(microtime(true) - $startTime, 2), - ]; - } - - $this->info("找到 {$users->count()} 个需要重置的用户"); - - foreach ($users as $user) { - try { - $totalResetCount += (int) $this->trafficResetService->checkAndReset($user, TrafficResetLog::SOURCE_CRON); - } catch (\Exception $e) { - $errors[] = [ - 'user_id' => $user->id, - 'email' => $user->email, - 'error' => $e->getMessage(), - ]; - Log::error('用户流量重置失败', [ - 'user_id' => $user->id, - 'error' => $e->getMessage(), - ]); - } - } - - return [ - 'total_processed' => $users->count(), - 'total_reset' => $totalResetCount, - 'error_count' => count($errors), - 'duration' => round(microtime(true) - $startTime, 2), - ]; - } - - private function performFix(): array - { - $startTime = microtime(true); - $nullUsers = $this->getNullResetTimeUsers(); - - if ($nullUsers->isEmpty()) { - $this->info("✅ 没有发现next_reset_at为null的用户"); - return [ - 'total_found' => 0, - 'total_fixed' => 0, - 'error_count' => 0, - 'duration' => round(microtime(true) - $startTime, 2), - ]; - } - - $this->info("🔧 发现 {$nullUsers->count()} 个next_reset_at为null的用户,开始修正..."); - - $fixedCount = 0; - $errors = []; - - foreach ($nullUsers as $user) { - try { - $nextResetTime = $this->trafficResetService->calculateNextResetTime($user); - if ($nextResetTime) { - $user->next_reset_at = $nextResetTime->timestamp; - $user->save(); - $fixedCount++; - } - } catch (\Exception $e) { - $errors[] = [ - 'user_id' => $user->id, - 'email' => $user->email, - 'error' => $e->getMessage(), - ]; - Log::error('修正用户next_reset_at失败', [ - 'user_id' => $user->id, - 'error' => $e->getMessage(), - ]); - } - } - - return [ - 'total_found' => $nullUsers->count(), - 'total_fixed' => $fixedCount, - 'error_count' => count($errors), - 'duration' => round(microtime(true) - $startTime, 2), - ]; - } - - private function performForce(): array - { - $startTime = microtime(true); - $allUsers = $this->getAllUsers(); - - if ($allUsers->isEmpty()) { - $this->info("✅ 没有发现需要处理的用户"); - return [ - 'total_found' => 0, - 'total_fixed' => 0, - 'error_count' => 0, - 'duration' => round(microtime(true) - $startTime, 2), - ]; - } - - $this->info("⚡ 发现 {$allUsers->count()} 个用户,开始重新计算重置时间..."); - - $fixedCount = 0; - $errors = []; - - foreach ($allUsers as $user) { - try { - $nextResetTime = $this->trafficResetService->calculateNextResetTime($user); - if ($nextResetTime) { - $user->next_reset_at = $nextResetTime->timestamp; - $user->save(); - $fixedCount++; - } - } catch (\Exception $e) { - $errors[] = [ - 'user_id' => $user->id, - 'email' => $user->email, - 'error' => $e->getMessage(), - ]; - Log::error('强制重新计算用户next_reset_at失败', [ - 'user_id' => $user->id, - 'error' => $e->getMessage(), - ]); - } - } - - return [ - 'total_found' => $allUsers->count(), - 'total_fixed' => $fixedCount, - 'error_count' => count($errors), - 'duration' => round(microtime(true) - $startTime, 2), - ]; - } - - - - private function getResetQuery() - { - return User::where('next_reset_at', '<=', time()) - ->whereNotNull('next_reset_at') - ->where(function ($query) { - $query->where('expired_at', '>', time()) - ->orWhereNull('expired_at'); - }) - ->where('banned', 0) - ->whereNotNull('plan_id'); - } - - - - private function getNullResetTimeUsers() - { - return User::whereNull('next_reset_at') - ->whereNotNull('plan_id') - ->where(function ($query) { - $query->where('expired_at', '>', time()) - ->orWhereNull('expired_at'); - }) - ->where('banned', 0) - ->with('plan:id,name,reset_traffic_method') - ->get(); - } - - private function getAllUsers() - { - return User::whereNotNull('plan_id') - ->where(function ($query) { - $query->where('expired_at', '>', time()) - ->orWhereNull('expired_at'); - }) - ->where('banned', 0) - ->with('plan:id,name,reset_traffic_method') - ->get(); - } - -} \ No newline at end of file diff --git a/Xboard/app/Console/Commands/ResetUser.php b/Xboard/app/Console/Commands/ResetUser.php deleted file mode 100644 index 51197ae..0000000 --- a/Xboard/app/Console/Commands/ResetUser.php +++ /dev/null @@ -1,58 +0,0 @@ -confirm("确定要重置所有用户安全信息吗?")) { - return; - } - ini_set('memory_limit', -1); - $users = User::all(); - foreach ($users as $user) - { - $user->token = Helper::guid(); - $user->uuid = Helper::guid(true); - $user->save(); - $this->info("已重置用户{$user->email}的安全信息"); - } - } -} diff --git a/Xboard/app/Console/Commands/SendRemindMail.php b/Xboard/app/Console/Commands/SendRemindMail.php deleted file mode 100644 index 5fdb2d1..0000000 --- a/Xboard/app/Console/Commands/SendRemindMail.php +++ /dev/null @@ -1,103 +0,0 @@ -warn('邮件提醒功能未启用'); - return 0; - } - - $chunkSize = max(100, min(2000, (int) $this->option('chunk-size'))); - $mailService = new MailService(); - - $totalUsers = $mailService->getTotalUsersNeedRemind(); - if ($totalUsers === 0) { - $this->info('没有需要发送提醒邮件的用户'); - return 0; - } - - $this->displayInfo($totalUsers, $chunkSize); - - if (!$this->option('force') && !$this->confirm("确定要发送提醒邮件给 {$totalUsers} 个用户吗?")) { - return 0; - } - - $startTime = microtime(true); - $progressBar = $this->output->createProgressBar((int) ceil($totalUsers / $chunkSize)); - $progressBar->start(); - - $statistics = $mailService->processUsersInChunks($chunkSize, function () use ($progressBar) { - $progressBar->advance(); - }); - - $progressBar->finish(); - $this->newLine(); - - $this->displayResults($statistics, microtime(true) - $startTime); - $this->logResults($statistics); - - return 0; - } - - private function displayInfo(int $totalUsers, int $chunkSize): void - { - $this->table(['项目', '值'], [ - ['需要处理的用户', number_format($totalUsers)], - ['批次大小', $chunkSize], - ['预计批次', ceil($totalUsers / $chunkSize)], - ]); - } - - private function displayResults(array $stats, float $duration): void - { - $this->info('✅ 提醒邮件发送完成!'); - - $this->table(['统计项', '数量'], [ - ['总处理用户', number_format($stats['processed_users'])], - ['过期提醒邮件', number_format($stats['expire_emails'])], - ['流量提醒邮件', number_format($stats['traffic_emails'])], - ['跳过用户', number_format($stats['skipped'])], - ['错误数量', number_format($stats['errors'])], - ['总耗时', round($duration, 2) . ' 秒'], - ['平均速度', round($stats['processed_users'] / max($duration, 0.1), 1) . ' 用户/秒'], - ]); - - if ($stats['errors'] > 0) { - $this->warn("⚠️ 有 {$stats['errors']} 个用户的邮件发送失败,请检查日志"); - } - } - - private function logResults(array $statistics): void - { - Log::info('SendRemindMail命令执行完成', ['statistics' => $statistics]); - } -} diff --git a/Xboard/app/Console/Commands/Test.php b/Xboard/app/Console/Commands/Test.php deleted file mode 100644 index 667e616..0000000 --- a/Xboard/app/Console/Commands/Test.php +++ /dev/null @@ -1,41 +0,0 @@ -info("__ __ ____ _ "); - $this->info("\ \ / /| __ ) ___ __ _ _ __ __| | "); - $this->info(" \ \/ / | __ \ / _ \ / _` | '__/ _` | "); - $this->info(" / /\ \ | |_) | (_) | (_| | | | (_| | "); - $this->info("/_/ \_\|____/ \___/ \__,_|_| \__,_| "); - if ( - (File::exists(base_path() . '/.env') && $this->getEnvValue('INSTALLED')) - || (getenv('INSTALLED', false) && $isDocker) - ) { - $securePath = admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key')))); - $this->info("访问 http(s)://你的站点/{$securePath} 进入管理面板,你可以在用户中心修改你的密码。"); - $this->warn("如需重新安装请清空目录下 .env 文件的内容(Docker安装方式不可以删除此文件)"); - $this->warn("快捷清空.env命令:"); - note('rm .env && touch .env'); - return; - } - if (is_dir(base_path() . '/.env')) { - $this->error('😔:安装失败,Docker环境下安装请保留空的 .env 文件'); - return; - } - // 选择数据库类型 - $dbType = $enableSqlite ? 'sqlite' : select( - label: '请选择数据库类型', - options: [ - 'sqlite' => 'SQLite (无需额外安装)', - 'mysql' => 'MySQL', - 'postgresql' => 'PostgreSQL' - ], - default: 'sqlite' - ); - - // 使用 match 表达式配置数据库 - $envConfig = match ($dbType) { - 'sqlite' => $this->configureSqlite(), - 'mysql' => $this->configureMysql(), - 'postgresql' => $this->configurePostgresql(), - default => throw new \InvalidArgumentException("不支持的数据库类型: {$dbType}") - }; - - if (is_null($envConfig)) { - return; // 用户选择退出安装 - } - $envConfig['APP_KEY'] = 'base64:' . base64_encode(Encrypter::generateKey('AES-256-CBC')); - $isReidsValid = false; - while (!$isReidsValid) { - // 判断是否为Docker环境 - if ($isDocker == 'true' && ($enableRedis || confirm(label: '是否启用Docker内置的Redis', default: true, yes: '启用', no: '不启用'))) { - $envConfig['REDIS_HOST'] = '/data/redis.sock'; - $envConfig['REDIS_PORT'] = 0; - $envConfig['REDIS_PASSWORD'] = null; - } else { - $envConfig['REDIS_HOST'] = text(label: '请输入Redis地址', default: '127.0.0.1', required: true); - $envConfig['REDIS_PORT'] = text(label: '请输入Redis端口', default: '6379', required: true); - $envConfig['REDIS_PASSWORD'] = text(label: '请输入redis密码(默认: null)', default: ''); - } - $redisConfig = [ - 'client' => 'phpredis', - 'default' => [ - 'host' => $envConfig['REDIS_HOST'], - 'password' => $envConfig['REDIS_PASSWORD'], - 'port' => $envConfig['REDIS_PORT'], - 'database' => 0, - ], - ]; - try { - $redis = new \Illuminate\Redis\RedisManager(app(), 'phpredis', $redisConfig); - $redis->ping(); - $isReidsValid = true; - } catch (\Exception $e) { - // 连接失败,输出错误消息 - $this->error("redis连接失败:" . $e->getMessage()); - $this->info("请重新输入REDIS配置"); - $enableRedis = false; - sleep(1); - } - } - - if (!copy(base_path() . '/.env.example', base_path() . '/.env')) { - abort(500, '复制环境文件失败,请检查目录权限'); - } - ; - $email = !empty($adminAccount) ? $adminAccount : text( - label: '请输入管理员账号', - default: 'admin@demo.com', - required: true, - validate: fn(string $email): ?string => match (true) { - !filter_var($email, FILTER_VALIDATE_EMAIL) => '请输入有效的邮箱地址.', - default => null, - } - ); - $password = Helper::guid(false); - $this->saveToEnv($envConfig); - - $this->call('config:cache'); - Artisan::call('cache:clear'); - $this->info('正在导入数据库请稍等...'); - Artisan::call("migrate", ['--force' => true]); - $this->info(Artisan::output()); - $this->info('数据库导入完成'); - $this->info('开始注册管理员账号'); - if (!self::registerAdmin($email, $password)) { - abort(500, '管理员账号注册失败,请重试'); - } - self::restoreProtectedPlugins($this); - $this->info('正在安装默认插件...'); - PluginManager::installDefaultPlugins(); - $this->info('默认插件安装完成'); - - $this->info('🎉:一切就绪'); - $this->info("管理员邮箱:{$email}"); - $this->info("管理员密码:{$password}"); - - $defaultSecurePath = hash('crc32b', config('app.key')); - $this->info("访问 http(s)://你的站点/{$defaultSecurePath} 进入管理面板,你可以在用户中心修改你的密码。"); - $envConfig['INSTALLED'] = true; - $this->saveToEnv($envConfig); - } catch (\Exception $e) { - $this->error($e); - } - } - - public static function registerAdmin($email, $password) - { - $user = new User(); - $user->email = $email; - if (strlen($password) < 8) { - abort(500, '管理员密码长度最小为8位字符'); - } - $user->password = password_hash($password, PASSWORD_DEFAULT); - $user->uuid = Helper::guid(true); - $user->token = Helper::guid(); - $user->is_admin = 1; - return $user->save(); - } - - private function set_env_var($key, $value) - { - $value = !strpos($value, ' ') ? $value : '"' . $value . '"'; - $key = strtoupper($key); - - $envPath = app()->environmentFilePath(); - $contents = file_get_contents($envPath); - - if (preg_match("/^{$key}=[^\r\n]*/m", $contents, $matches)) { - $contents = str_replace($matches[0], "{$key}={$value}", $contents); - } else { - $contents .= "\n{$key}={$value}\n"; - } - - return file_put_contents($envPath, $contents) !== false; - } - - private function saveToEnv($data = []) - { - foreach ($data as $key => $value) { - self::set_env_var($key, $value); - } - return true; - } - - function getEnvValue($key, $default = null) - { - $dotenv = \Dotenv\Dotenv::createImmutable(base_path()); - $dotenv->load(); - - return Env::get($key, $default); - } - - /** - * 配置 SQLite 数据库 - * - * @return array|null - */ - private function configureSqlite(): ?array - { - $sqliteFile = '.docker/.data/database.sqlite'; - if (!file_exists(base_path($sqliteFile))) { - // 创建空文件 - if (!touch(base_path($sqliteFile))) { - $this->info("sqlite创建成功: $sqliteFile"); - } - } - - $envConfig = [ - 'DB_CONNECTION' => 'sqlite', - 'DB_DATABASE' => $sqliteFile, - 'DB_HOST' => '', - 'DB_USERNAME' => '', - 'DB_PASSWORD' => '', - ]; - - try { - Config::set("database.default", 'sqlite'); - Config::set("database.connections.sqlite.database", base_path($envConfig['DB_DATABASE'])); - DB::purge('sqlite'); - DB::connection('sqlite')->getPdo(); - - if (!blank(DB::connection('sqlite')->getPdo()->query("SELECT name FROM sqlite_master WHERE type='table'")->fetchAll(\PDO::FETCH_COLUMN))) { - if (confirm(label: '检测到数据库中已经存在数据,是否要清空数据库以便安装新的数据?', default: false, yes: '清空', no: '退出安装')) { - $this->info('正在清空数据库请稍等'); - $this->call('db:wipe', ['--force' => true]); - $this->info('数据库清空完成'); - } else { - return null; - } - } - } catch (\Exception $e) { - $this->error("SQLite数据库连接失败:" . $e->getMessage()); - return null; - } - - return $envConfig; - } - - /** - * 配置 MySQL 数据库 - * - * @return array - */ - private function configureMysql(): array - { - while (true) { - $envConfig = [ - 'DB_CONNECTION' => 'mysql', - 'DB_HOST' => text(label: "请输入MySQL数据库地址", default: '127.0.0.1', required: true), - 'DB_PORT' => text(label: '请输入MySQL数据库端口', default: '3306', required: true), - 'DB_DATABASE' => text(label: '请输入MySQL数据库名', default: 'xboard', required: true), - 'DB_USERNAME' => text(label: '请输入MySQL数据库用户名', default: 'root', required: true), - 'DB_PASSWORD' => text(label: '请输入MySQL数据库密码', required: false), - ]; - - try { - Config::set("database.default", 'mysql'); - Config::set("database.connections.mysql.host", $envConfig['DB_HOST']); - Config::set("database.connections.mysql.port", $envConfig['DB_PORT']); - Config::set("database.connections.mysql.database", $envConfig['DB_DATABASE']); - Config::set("database.connections.mysql.username", $envConfig['DB_USERNAME']); - Config::set("database.connections.mysql.password", $envConfig['DB_PASSWORD']); - DB::purge('mysql'); - DB::connection('mysql')->getPdo(); - - if (!blank(DB::connection('mysql')->select('SHOW TABLES'))) { - if (confirm(label: '检测到数据库中已经存在数据,是否要清空数据库以便安装新的数据?', default: false, yes: '清空', no: '不清空')) { - $this->info('正在清空数据库请稍等'); - $this->call('db:wipe', ['--force' => true]); - $this->info('数据库清空完成'); - return $envConfig; - } else { - continue; // 重新输入配置 - } - } - - return $envConfig; - } catch (\Exception $e) { - $this->error("MySQL数据库连接失败:" . $e->getMessage()); - $this->info("请重新输入MySQL数据库配置"); - } - } - } - - /** - * 配置 PostgreSQL 数据库 - * - * @return array - */ - private function configurePostgresql(): array - { - while (true) { - $envConfig = [ - 'DB_CONNECTION' => 'pgsql', - 'DB_HOST' => text(label: "请输入PostgreSQL数据库地址", default: '127.0.0.1', required: true), - 'DB_PORT' => text(label: '请输入PostgreSQL数据库端口', default: '5432', required: true), - 'DB_DATABASE' => text(label: '请输入PostgreSQL数据库名', default: 'xboard', required: true), - 'DB_USERNAME' => text(label: '请输入PostgreSQL数据库用户名', default: 'postgres', required: true), - 'DB_PASSWORD' => text(label: '请输入PostgreSQL数据库密码', required: false), - ]; - - try { - Config::set("database.default", 'pgsql'); - Config::set("database.connections.pgsql.host", $envConfig['DB_HOST']); - Config::set("database.connections.pgsql.port", $envConfig['DB_PORT']); - Config::set("database.connections.pgsql.database", $envConfig['DB_DATABASE']); - Config::set("database.connections.pgsql.username", $envConfig['DB_USERNAME']); - Config::set("database.connections.pgsql.password", $envConfig['DB_PASSWORD']); - DB::purge('pgsql'); - DB::connection('pgsql')->getPdo(); - - // 检查PostgreSQL数据库是否有表 - $tables = DB::connection('pgsql')->select("SELECT tablename FROM pg_tables WHERE schemaname = 'public'"); - if (!blank($tables)) { - if (confirm(label: '检测到数据库中已经存在数据,是否要清空数据库以便安装新的数据?', default: false, yes: '清空', no: '不清空')) { - $this->info('正在清空数据库请稍等'); - $this->call('db:wipe', ['--force' => true]); - $this->info('数据库清空完成'); - return $envConfig; - } else { - continue; // 重新输入配置 - } - } - - return $envConfig; - } catch (\Exception $e) { - $this->error("PostgreSQL数据库连接失败:" . $e->getMessage()); - $this->info("请重新输入PostgreSQL数据库配置"); - } - } - } - - /** - * 还原内置受保护插件(可在安装和更新时调用) - * Docker 部署时 plugins/ 目录被外部挂载覆盖,需要从镜像备份中还原默认插件 - */ - public static function restoreProtectedPlugins(Command $console = null) - { - $backupBase = '/opt/default-plugins'; - $pluginsBase = base_path('plugins'); - - if (!File::isDirectory($backupBase)) { - $console?->info('非 Docker 环境或备份目录不存在,跳过插件还原。'); - return; - } - - foreach (Plugin::PROTECTED_PLUGINS as $pluginCode) { - $dirName = Str::studly($pluginCode); - $source = "{$backupBase}/{$dirName}"; - $target = "{$pluginsBase}/{$dirName}"; - - if (!File::isDirectory($source)) { - continue; - } - - // 先清除旧文件再复制,避免重命名后残留旧文件 - File::deleteDirectory($target); - File::copyDirectory($source, $target); - $console?->info("已同步默认插件 [{$dirName}]"); - } - } -} diff --git a/Xboard/app/Console/Commands/XboardRollback.php b/Xboard/app/Console/Commands/XboardRollback.php deleted file mode 100644 index f9e5f46..0000000 --- a/Xboard/app/Console/Commands/XboardRollback.php +++ /dev/null @@ -1,45 +0,0 @@ -info('正在回滚数据库请稍等...'); - \Artisan::call("migrate:rollback"); - $this->info(\Artisan::output()); - } -} diff --git a/Xboard/app/Console/Commands/XboardStatistics.php b/Xboard/app/Console/Commands/XboardStatistics.php deleted file mode 100644 index 0bd4736..0000000 --- a/Xboard/app/Console/Commands/XboardStatistics.php +++ /dev/null @@ -1,75 +0,0 @@ -statUser(); - // $this->statServer(); - $this->stat(); - info('统计任务执行完毕。耗时:' . (microtime(true) - $startAt) / 1000); - } - - - private function stat() - { - try { - $endAt = strtotime(date('Y-m-d')); - $startAt = strtotime('-1 day', $endAt); - $statisticalService = new StatisticalService(); - $statisticalService->setStartAt($startAt); - $statisticalService->setEndAt($endAt); - $data = $statisticalService->generateStatData(); - $data['record_at'] = $startAt; - $data['record_type'] = 'd'; - $statistic = Stat::where('record_at', $startAt) - ->where('record_type', 'd') - ->first(); - if ($statistic) { - $statistic->update($data); - return; - } - Stat::create($data); - } catch (\Exception $e) { - Log::error($e->getMessage(), ['exception' => $e]); - } - } -} diff --git a/Xboard/app/Console/Commands/XboardUpdate.php b/Xboard/app/Console/Commands/XboardUpdate.php deleted file mode 100644 index 65c95da..0000000 --- a/Xboard/app/Console/Commands/XboardUpdate.php +++ /dev/null @@ -1,65 +0,0 @@ -info('正在导入数据库请稍等...'); - Artisan::call("migrate", ['--force' => true]); - $this->info(Artisan::output()); - $this->info('正在检查内置插件文件...'); - XboardInstall::restoreProtectedPlugins($this); - $this->info('正在检查并安装默认插件...'); - PluginManager::installDefaultPlugins(); - $this->info('默认插件检查完成'); - // Artisan::call('reset:traffic', ['--fix-null' => true]); - $this->info('正在重新计算所有用户的重置时间...'); - Artisan::call('reset:traffic', ['--force' => true]); - $updateService = new UpdateService(); - $updateService->updateVersionCache(); - $themeService = app(ThemeService::class); - $themeService->refreshCurrentTheme(); - Artisan::call('horizon:terminate'); - $this->info('更新完毕,队列服务已重启,你无需进行任何操作。'); - } -} diff --git a/Xboard/app/Console/Kernel.php b/Xboard/app/Console/Kernel.php deleted file mode 100644 index 77b1810..0000000 --- a/Xboard/app/Console/Kernel.php +++ /dev/null @@ -1,68 +0,0 @@ -command('xboard:statistics')->dailyAt('0:10')->onOneServer(); - // check - $schedule->command('check:order')->everyMinute()->onOneServer()->withoutOverlapping(5); - $schedule->command('check:commission')->everyMinute()->onOneServer()->withoutOverlapping(5); - $schedule->command('check:ticket')->everyMinute()->onOneServer()->withoutOverlapping(5); - $schedule->command('check:traffic-exceeded')->everyMinute()->onOneServer()->withoutOverlapping(10)->runInBackground(); - // reset - $schedule->command('reset:traffic')->everyMinute()->onOneServer()->withoutOverlapping(10); - $schedule->command('reset:log')->daily()->onOneServer(); - // send - $schedule->command('send:remindMail', ['--force'])->dailyAt('11:30')->onOneServer(); - // horizon metrics - $schedule->command('horizon:snapshot')->everyFiveMinutes()->onOneServer(); - // backup Timing - // if (env('ENABLE_AUTO_BACKUP_AND_UPDATE', false)) { - // $schedule->command('backup:database', ['true'])->daily()->onOneServer(); - // } - app(PluginManager::class)->registerPluginSchedules($schedule); - - } - - /** - * Register the commands for the application. - * - * @return void - */ - protected function commands() - { - $this->load(__DIR__ . '/Commands'); - - try { - app(PluginManager::class)->initializeEnabledPlugins(); - } catch (\Exception $e) { - } - require base_path('routes/console.php'); - } -} diff --git a/Xboard/app/Contracts/PaymentInterface.php b/Xboard/app/Contracts/PaymentInterface.php deleted file mode 100644 index 2661362..0000000 --- a/Xboard/app/Contracts/PaymentInterface.php +++ /dev/null @@ -1,10 +0,0 @@ -message = $message; - $this->code = $code; - $this->errors = $errors; - } - public function errors(){ - return $this->errors; - } - -} diff --git a/Xboard/app/Exceptions/BusinessException.php b/Xboard/app/Exceptions/BusinessException.php deleted file mode 100644 index 2079495..0000000 --- a/Xboard/app/Exceptions/BusinessException.php +++ /dev/null @@ -1,19 +0,0 @@ -> - */ - protected $dontReport = [ - ApiException::class, - InterceptResponseException::class - ]; - - /** - * A list of the inputs that are never flashed for validation exceptions. - * - * @var array - */ - protected $dontFlash = [ - 'password', - 'password_confirmation', - ]; - - /** - * Report or log an exception. - * - * @param \Throwable $exception - * @return void - * - * @throws \Throwable - */ - public function report(Throwable $exception) - { - parent::report($exception); - } - - /** - * Render an exception into an HTTP response. - * - * @param \Illuminate\Http\Request $request - * @param \Throwable $exception - * @return \Symfony\Component\HttpFoundation\Response - * - * @throws \Throwable - */ - public function render($request, Throwable $exception) - { - if ($exception instanceof ViewException) { - return $this->fail([500, '主题渲染失败。如更新主题,参数可能发生变化请重新配置主题后再试。']); - } - // ApiException主动抛出错误 - if ($exception instanceof ApiException) { - $code = $exception->getCode(); - $message = $exception->getMessage(); - $errors = $exception->errors(); - return $this->fail([$code, $message],null,$errors); - } - return parent::render($request, $exception); - } - - /** - * Register the exception handling callbacks for the application. - */ - public function register(): void - { - $this->reportable(function (Throwable $e) { - // - }); - - $this->renderable(function (InterceptResponseException $e) { - return $e->getResponse(); - }); - } - - protected function convertExceptionToArray(Throwable $e) - { - return config('app.debug') ? [ - 'message' => $e->getMessage(), - 'exception' => get_class($e), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - 'trace' => collect($e->getTrace())->map(function ($trace) { - return Arr::except($trace, ['args']); - })->all(), - ] : [ - 'message' => $this->isHttpException($e) ? $e->getMessage() : __("Uh-oh, we've had some problems, we're working on it."), - ]; - } -} diff --git a/Xboard/app/Helpers/ApiResponse.php b/Xboard/app/Helpers/ApiResponse.php deleted file mode 100644 index 4820eb4..0000000 --- a/Xboard/app/Helpers/ApiResponse.php +++ /dev/null @@ -1,79 +0,0 @@ -jsonResponse('success', $codeResponse, $data, null); - } - - /** - * 失败 - * @param array $codeResponse - * @param mixed $data - * @param mixed $error - * @return JsonResponse - */ - public function fail($codeResponse = ResponseEnum::HTTP_ERROR, $data = null, $error = null): JsonResponse - { - return $this->jsonResponse('fail', $codeResponse, $data, $error); - } - - /** - * json响应 - * @param $status - * @param $codeResponse - * @param $data - * @param $error - * @return JsonResponse - */ - private function jsonResponse($status, $codeResponse, $data, $error): JsonResponse - { - list($code, $message) = $codeResponse; - return response() - ->json([ - 'status' => $status, - // 'code' => $code, - 'message' => $message, - 'data' => $data ?? null, - 'error' => $error, - ], (int) substr(((string) $code), 0, 3)); - } - - - public function paginate(LengthAwarePaginator $page) - { - return response()->json([ - 'total' => $page->total(), - 'current_page' => $page->currentPage(), - 'per_page' => $page->perPage(), - 'last_page' => $page->lastPage(), - 'data' => $page->items() - ]); - } - - /** - * 业务异常返回 - * @param array $codeResponse - * @param string $info - * @throws BusinessException - */ - public function throwBusinessException(array $codeResponse = ResponseEnum::HTTP_ERROR, string $info = '') - { - throw new BusinessException($codeResponse, $info); - } -} \ No newline at end of file diff --git a/Xboard/app/Helpers/Functions.php b/Xboard/app/Helpers/Functions.php deleted file mode 100644 index 282d62e..0000000 --- a/Xboard/app/Helpers/Functions.php +++ /dev/null @@ -1,82 +0,0 @@ -toArray(); - } - - if (is_array($key)) { - $setting->save($key); - return ''; - } - - $default = config('v2board.' . $key) ?? $default; - return $setting->get($key) ?? $default; - } -} - -if (!function_exists('subscribe_template')) { - /** - * Get subscribe template content by protocol name. - */ - function subscribe_template(string $name): ?string - { - return \App\Models\SubscribeTemplate::getContent($name); - } -} - -if (!function_exists('admin_settings_batch')) { - /** - * 批量获取配置参数,性能优化版本 - * - * @param array $keys 配置键名数组 - * @return array 返回键值对数组 - */ - function admin_settings_batch(array $keys): array - { - return app(Setting::class)->getBatch($keys); - } -} - -if (!function_exists('source_base_url')) { - /** - * 获取来源基础URL,优先Referer,其次Host - * @param string $path - * @return string - */ - function source_base_url(string $path = ''): string - { - $baseUrl = ''; - $referer = request()->header('Referer'); - - if ($referer) { - $parsedUrl = parse_url($referer); - if (isset($parsedUrl['scheme']) && isset($parsedUrl['host'])) { - $baseUrl = $parsedUrl['scheme'] . '://' . $parsedUrl['host']; - if (isset($parsedUrl['port'])) { - $baseUrl .= ':' . $parsedUrl['port']; - } - } - } - - if (!$baseUrl) { - $baseUrl = request()->getSchemeAndHttpHost(); - } - - $baseUrl = rtrim($baseUrl, '/'); - $path = ltrim($path, '/'); - return $baseUrl . '/' . $path; - } -} diff --git a/Xboard/app/Helpers/ResponseEnum.php b/Xboard/app/Helpers/ResponseEnum.php deleted file mode 100644 index f749ad3..0000000 --- a/Xboard/app/Helpers/ResponseEnum.php +++ /dev/null @@ -1,81 +0,0 @@ -isPluginEnabled()) { - return [400, '插件未启用']; - } - return null; - } -} \ No newline at end of file diff --git a/Xboard/app/Http/Controllers/V1/Client/AppController.php b/Xboard/app/Http/Controllers/V1/Client/AppController.php deleted file mode 100644 index 4637b08..0000000 --- a/Xboard/app/Http/Controllers/V1/Client/AppController.php +++ /dev/null @@ -1,90 +0,0 @@ -user(); - $userService = new UserService(); - if ($userService->isAvailable($user)) { - $servers = ServerService::getAvailableServers($user); - } - $defaultConfig = base_path() . '/resources/rules/app.clash.yaml'; - $customConfig = base_path() . '/resources/rules/custom.app.clash.yaml'; - if (File::exists($customConfig)) { - $config = Yaml::parseFile($customConfig); - } else { - $config = Yaml::parseFile($defaultConfig); - } - $proxy = []; - $proxies = []; - - foreach ($servers as $item) { - $protocol_settings = $item['protocol_settings']; - if ($item['type'] === 'shadowsocks' - && in_array(data_get($protocol_settings, 'cipher'), [ - 'aes-128-gcm', - 'aes-192-gcm', - 'aes-256-gcm', - 'chacha20-ietf-poly1305' - ]) - ) { - array_push($proxy, \App\Protocols\Clash::buildShadowsocks($user['uuid'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === 'vmess') { - array_push($proxy, \App\Protocols\Clash::buildVmess($user['uuid'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === 'trojan') { - array_push($proxy, \App\Protocols\Clash::buildTrojan($user['uuid'], $item)); - array_push($proxies, $item['name']); - } - } - - $config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy); - foreach ($config['proxy-groups'] as $k => $v) { - $config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies); - } - return(Yaml::dump($config)); - } - - public function getVersion(Request $request) - { - if (strpos($request->header('user-agent'), 'tidalab/4.0.0') !== false - || strpos($request->header('user-agent'), 'tunnelab/4.0.0') !== false - ) { - if (strpos($request->header('user-agent'), 'Win64') !== false) { - $data = [ - 'version' => admin_setting('windows_version'), - 'download_url' => admin_setting('windows_download_url') - ]; - } else { - $data = [ - 'version' => admin_setting('macos_version'), - 'download_url' => admin_setting('macos_download_url') - ]; - } - }else{ - $data = [ - 'windows_version' => admin_setting('windows_version'), - 'windows_download_url' => admin_setting('windows_download_url'), - 'macos_version' => admin_setting('macos_version'), - 'macos_download_url' => admin_setting('macos_download_url'), - 'android_version' => admin_setting('android_version'), - 'android_download_url' => admin_setting('android_download_url') - ]; - } - return $this->success($data); - } -} diff --git a/Xboard/app/Http/Controllers/V1/Client/ClientController.php b/Xboard/app/Http/Controllers/V1/Client/ClientController.php deleted file mode 100644 index e4437bc..0000000 --- a/Xboard/app/Http/Controllers/V1/Client/ClientController.php +++ /dev/null @@ -1,247 +0,0 @@ - [ - 1 => '[Hy]', - 2 => '[Hy2]' - ], - 'vless' => '[vless]', - 'shadowsocks' => '[ss]', - 'vmess' => '[vmess]', - 'trojan' => '[trojan]', - 'tuic' => '[tuic]', - 'socks' => '[socks]', - 'anytls' => '[anytls]' - ]; - - - public function subscribe(Request $request) - { - HookManager::call('client.subscribe.before'); - $request->validate([ - 'types' => ['nullable', 'string'], - 'filter' => ['nullable', 'string'], - 'flag' => ['nullable', 'string'], - ]); - - $user = $request->user(); - $userService = new UserService(); - - if (!$userService->isAvailable($user)) { - HookManager::call('client.subscribe.unavailable'); - return response('', 403, ['Content-Type' => 'text/plain']); - } - - return $this->doSubscribe($request, $user); - } - - public function doSubscribe(Request $request, $user, $servers = null) - { - if ($servers === null) { - $servers = ServerService::getAvailableServers($user); - $servers = HookManager::filter('client.subscribe.servers', $servers, $user, $request); - } - - $clientInfo = $this->getClientInfo($request); - - $requestedTypes = $this->parseRequestedTypes($request->input('types')); - $filterKeywords = $this->parseFilterKeywords($request->input('filter')); - - $protocolClassName = app('protocols.manager')->matchProtocolClassName($clientInfo['flag']) - ?? General::class; - - $serversFiltered = $this->filterServers( - servers: $servers, - allowedTypes: $requestedTypes, - filterKeywords: $filterKeywords - ); - - $this->setSubscribeInfoToServers($serversFiltered, $user, count($servers) - count($serversFiltered)); - $serversFiltered = $this->addPrefixToServerName($serversFiltered); - - // Instantiate the protocol class with filtered servers and client info - $protocolInstance = app()->make($protocolClassName, [ - 'user' => $user, - 'servers' => $serversFiltered, - 'clientName' => $clientInfo['name'] ?? null, - 'clientVersion' => $clientInfo['version'] ?? null, - 'userAgent' => $clientInfo['flag'] ?? null - ]); - - return $protocolInstance->handle(); - } - - /** - * Parses the input string for requested server types. - */ - private function parseRequestedTypes(?string $typeInputString): array - { - if (blank($typeInputString) || $typeInputString === 'all') { - return Server::VALID_TYPES; - } - - $requested = collect(preg_split('/[|,|]+/', $typeInputString)) - ->map(fn($type) => trim($type)) - ->filter() // Remove empty strings that might result from multiple delimiters - ->all(); - - return array_values(array_intersect($requested, Server::VALID_TYPES)); - } - - /** - * Parses the input string for filter keywords. - */ - private function parseFilterKeywords(?string $filterInputString): ?array - { - if (blank($filterInputString) || mb_strlen($filterInputString) > 20) { - return null; - } - - return collect(preg_split('/[|,|]+/', $filterInputString)) - ->map(fn($keyword) => trim($keyword)) - ->filter() // Remove empty strings - ->all(); - } - - /** - * Filters servers based on allowed types and keywords. - */ - private function filterServers(array $servers, array $allowedTypes, ?array $filterKeywords): array - { - return collect($servers)->filter(function ($server) use ($allowedTypes, $filterKeywords) { - // Condition 1: Server type must be in the list of allowed types - if ($allowedTypes && !in_array($server['type'], $allowedTypes)) { - return false; // Filter out (don't keep) - } - - // Condition 2: If filterKeywords are provided, at least one keyword must match - if (!empty($filterKeywords)) { // Check if $filterKeywords is not empty - $keywordMatch = collect($filterKeywords)->contains(function ($keyword) use ($server) { - return stripos($server['name'], $keyword) !== false - || in_array($keyword, $server['tags'] ?? []); - }); - if (!$keywordMatch) { - return false; // Filter out if no keywords match - } - } - // Keep the server if its type is allowed AND (no filter keywords OR at least one keyword matched) - return true; - })->values()->all(); - } - - private function getClientInfo(Request $request): array - { - $flag = strtolower($request->input('flag') ?? $request->header('User-Agent', '')); - - $clientName = null; - $clientVersion = null; - - if (preg_match('/([a-zA-Z0-9\-_]+)[\/\s]+(v?[0-9]+(?:\.[0-9]+){0,2})/', $flag, $matches)) { - $potentialName = strtolower($matches[1]); - $clientVersion = preg_replace('/^v/', '', $matches[2]); - - if (in_array($potentialName, app('protocols.flags'))) { - $clientName = $potentialName; - } - } - - if (!$clientName) { - $flags = collect(app('protocols.flags'))->sortByDesc(fn($f) => strlen($f))->values()->all(); - foreach ($flags as $name) { - if (stripos($flag, $name) !== false) { - $clientName = $name; - if (!$clientVersion) { - $pattern = '/' . preg_quote($name, '/') . '[\/\s]+(v?[0-9]+(?:\.[0-9]+){0,2})/i'; - if (preg_match($pattern, $flag, $vMatches)) { - $clientVersion = preg_replace('/^v/', '', $vMatches[1]); - } - } - break; - } - } - } - - if (!$clientVersion) { - if (preg_match('/\/v?(\d+(?:\.\d+){0,2})/', $flag, $matches)) { - $clientVersion = $matches[1]; - } - } - - return [ - 'flag' => $flag, - 'name' => $clientName, - 'version' => $clientVersion - ]; - } - - private function setSubscribeInfoToServers(&$servers, $user, $rejectServerCount = 0) - { - if (!isset($servers[0])) - return; - if ($rejectServerCount > 0) { - array_unshift($servers, array_merge($servers[0], [ - 'name' => "过滤掉{$rejectServerCount}条线路", - ])); - } - if (!(int) admin_setting('show_info_to_server_enable', 0)) - return; - $useTraffic = $user['u'] + $user['d']; - $totalTraffic = $user['transfer_enable']; - $remainingTraffic = Helper::trafficConvert($totalTraffic - $useTraffic); - $expiredDate = $user['expired_at'] ? date('Y-m-d', $user['expired_at']) : __('长期有效'); - $userService = new UserService(); - $resetDay = $userService->getResetDay($user); - array_unshift($servers, array_merge($servers[0], [ - 'name' => "套餐到期:{$expiredDate}", - ])); - if ($resetDay) { - array_unshift($servers, array_merge($servers[0], [ - 'name' => "距离下次重置剩余:{$resetDay} 天", - ])); - } - array_unshift($servers, array_merge($servers[0], [ - 'name' => "剩余流量:{$remainingTraffic}", - ])); - } - - private function addPrefixToServerName(array $servers): array - { - if (!admin_setting('show_protocol_to_server_enable', false)) { - return $servers; - } - return collect($servers) - ->map(function (array $server): array { - $server['name'] = $this->getPrefixedServerName($server); - return $server; - }) - ->all(); - } - - private function getPrefixedServerName(array $server): string - { - $type = $server['type'] ?? ''; - if (!isset(self::PROTOCOL_PREFIXES[$type])) { - return $server['name'] ?? ''; - } - $prefix = is_array(self::PROTOCOL_PREFIXES[$type]) - ? self::PROTOCOL_PREFIXES[$type][$server['protocol_settings']['version'] ?? 1] ?? '' - : self::PROTOCOL_PREFIXES[$type]; - return $prefix . ($server['name'] ?? ''); - } -} diff --git a/Xboard/app/Http/Controllers/V1/Guest/CommController.php b/Xboard/app/Http/Controllers/V1/Guest/CommController.php deleted file mode 100644 index 00c05c7..0000000 --- a/Xboard/app/Http/Controllers/V1/Guest/CommController.php +++ /dev/null @@ -1,39 +0,0 @@ - admin_setting('tos_url'), - 'is_email_verify' => (int) admin_setting('email_verify', 0) ? 1 : 0, - 'is_invite_force' => (int) admin_setting('invite_force', 0) ? 1 : 0, - 'email_whitelist_suffix' => (int) admin_setting('email_whitelist_enable', 0) - ? Helper::getEmailSuffix() - : 0, - 'is_captcha' => (int) admin_setting('captcha_enable', 0) ? 1 : 0, - 'captcha_type' => admin_setting('captcha_type', 'recaptcha'), - 'recaptcha_site_key' => admin_setting('recaptcha_site_key'), - 'recaptcha_v3_site_key' => admin_setting('recaptcha_v3_site_key'), - 'recaptcha_v3_score_threshold' => admin_setting('recaptcha_v3_score_threshold', 0.5), - 'turnstile_site_key' => admin_setting('turnstile_site_key'), - 'app_description' => admin_setting('app_description'), - 'app_url' => admin_setting('app_url'), - 'logo' => admin_setting('logo'), - // 保持向后兼容 - 'is_recaptcha' => (int) admin_setting('captcha_enable', 0) ? 1 : 0, - ]; - - $data = HookManager::filter('guest_comm_config', $data); - - return $this->success($data); - } -} diff --git a/Xboard/app/Http/Controllers/V1/Guest/PaymentController.php b/Xboard/app/Http/Controllers/V1/Guest/PaymentController.php deleted file mode 100644 index 31f444e..0000000 --- a/Xboard/app/Http/Controllers/V1/Guest/PaymentController.php +++ /dev/null @@ -1,52 +0,0 @@ -notify($request->input()); - if (!$verify) { - HookManager::call('payment.notify.failed', [$method, $uuid, $request]); - return $this->fail([422, 'verify error']); - } - HookManager::call('payment.notify.verified', $verify); - if (!$this->handle($verify['trade_no'], $verify['callback_no'])) { - return $this->fail([400, 'handle error']); - } - return (isset($verify['custom_result']) ? $verify['custom_result'] : 'success'); - } catch (\Exception $e) { - Log::error($e); - return $this->fail([500, 'fail']); - } - } - - private function handle($tradeNo, $callbackNo) - { - $order = Order::where('trade_no', $tradeNo)->first(); - if (!$order) { - return $this->fail([400202, 'order is not found']); - } - if ($order->status !== Order::STATUS_PENDING) - return true; - $orderService = new OrderService($order); - if (!$orderService->paid($callbackNo)) { - return false; - } - - HookManager::call('payment.notify.success', $order); - return true; - } -} diff --git a/Xboard/app/Http/Controllers/V1/Guest/PlanController.php b/Xboard/app/Http/Controllers/V1/Guest/PlanController.php deleted file mode 100644 index 4348a25..0000000 --- a/Xboard/app/Http/Controllers/V1/Guest/PlanController.php +++ /dev/null @@ -1,25 +0,0 @@ -planService = $planService; - } - public function fetch(Request $request) - { - $plan = $this->planService->getAvailablePlans(); - return $this->success(PlanResource::collection($plan)); - } -} diff --git a/Xboard/app/Http/Controllers/V1/Guest/TelegramController.php b/Xboard/app/Http/Controllers/V1/Guest/TelegramController.php deleted file mode 100644 index 01369fa..0000000 --- a/Xboard/app/Http/Controllers/V1/Guest/TelegramController.php +++ /dev/null @@ -1,126 +0,0 @@ -telegramService = $telegramService; - $this->userService = $userService; - } - - public function webhook(Request $request): void - { - $expectedToken = md5(admin_setting('telegram_bot_token')); - if ($request->input('access_token') !== $expectedToken) { - throw new ApiException('access_token is error', 401); - } - - $data = $request->json()->all(); - - $this->formatMessage($data); - $this->formatChatJoinRequest($data); - $this->handle(); - } - - private function handle(): void - { - if (!$this->msg) - return; - $msg = $this->msg; - $this->processBotName($msg); - try { - HookManager::call('telegram.message.before', [$msg]); - $handled = HookManager::filter('telegram.message.handle', false, [$msg]); - if (!$handled) { - HookManager::call('telegram.message.unhandled', [$msg]); - } - HookManager::call('telegram.message.after', [$msg]); - } catch (\Exception $e) { - HookManager::call('telegram.message.error', [$msg, $e]); - $this->telegramService->sendMessage($msg->chat_id, $e->getMessage()); - } - } - - private function processBotName(object $msg): void - { - $commandParts = explode('@', $msg->command); - - if (count($commandParts) === 2) { - $botName = $this->getBotName(); - if ($commandParts[1] === $botName) { - $msg->command = $commandParts[0]; - } - } - } - - private function getBotName(): string - { - $response = $this->telegramService->getMe(); - return $response->result->username; - } - - private function formatMessage(array $data): void - { - if (!isset($data['message']['text'])) - return; - - $message = $data['message']; - $text = explode(' ', $message['text']); - - $this->msg = (object) [ - 'command' => $text[0], - 'args' => array_slice($text, 1), - 'chat_id' => $message['chat']['id'], - 'message_id' => $message['message_id'], - 'message_type' => 'message', - 'text' => $message['text'], - 'is_private' => $message['chat']['type'] === 'private', - ]; - - if (isset($message['reply_to_message']['text'])) { - $this->msg->message_type = 'reply_message'; - $this->msg->reply_text = $message['reply_to_message']['text']; - } - } - - private function formatChatJoinRequest(array $data): void - { - $joinRequest = $data['chat_join_request'] ?? null; - if (!$joinRequest) - return; - - $chatId = $joinRequest['chat']['id'] ?? null; - $userId = $joinRequest['from']['id'] ?? null; - - if (!$chatId || !$userId) - return; - - $user = User::where('telegram_id', $userId)->first(); - - if (!$user) { - $this->telegramService->declineChatJoinRequest($chatId, $userId); - return; - } - - if (!$this->userService->isAvailable($user)) { - $this->telegramService->declineChatJoinRequest($chatId, $userId); - return; - } - - $this->telegramService->approveChatJoinRequest($chatId, $userId); - } -} diff --git a/Xboard/app/Http/Controllers/V1/Passport/AuthController.php b/Xboard/app/Http/Controllers/V1/Passport/AuthController.php deleted file mode 100644 index 5464426..0000000 --- a/Xboard/app/Http/Controllers/V1/Passport/AuthController.php +++ /dev/null @@ -1,175 +0,0 @@ -mailLinkService = $mailLinkService; - $this->registerService = $registerService; - $this->loginService = $loginService; - } - - /** - * 通过邮件链接登录 - */ - public function loginWithMailLink(Request $request) - { - $params = $request->validate([ - 'email' => 'required|email:strict', - 'redirect' => 'nullable' - ]); - - [$success, $result] = $this->mailLinkService->handleMailLink( - $params['email'], - $request->input('redirect') - ); - - if (!$success) { - return $this->fail($result); - } - - return $this->success($result); - } - - /** - * 用户注册 - */ - public function register(AuthRegister $request) - { - [$success, $result] = $this->registerService->register($request); - - if (!$success) { - return $this->fail($result); - } - - $authService = new AuthService($result); - return $this->success($authService->generateAuthData()); - } - - /** - * 用户登录 - */ - public function login(AuthLogin $request) - { - $email = $request->input('email'); - $password = $request->input('password'); - - [$success, $result] = $this->loginService->login($email, $password); - - if (!$success) { - return $this->fail($result); - } - - $authService = new AuthService($result); - return $this->success($authService->generateAuthData()); - } - - /** - * 通过token登录 - */ - public function token2Login(Request $request) - { - // 处理直接通过token重定向 - if ($token = $request->input('token')) { - $redirect = '/#/login?verify=' . $token . '&redirect=' . ($request->input('redirect', 'dashboard')); - - return redirect()->to( - admin_setting('app_url') - ? admin_setting('app_url') . $redirect - : url($redirect) - ); - } - - // 处理通过验证码登录 - if ($verify = $request->input('verify')) { - $userId = $this->mailLinkService->handleTokenLogin($verify); - - if (!$userId) { - return response()->json([ - 'message' => __('Token error') - ], 400); - } - - $user = \App\Models\User::find($userId); - - if (!$user) { - return response()->json([ - 'message' => __('User not found') - ], 400); - } - - $authService = new AuthService($user); - - return response()->json([ - 'data' => $authService->generateAuthData() - ]); - } - - return response()->json([ - 'message' => __('Invalid request') - ], 400); - } - - /** - * 获取快速登录URL - */ - public function getQuickLoginUrl(Request $request) - { - $authorization = $request->input('auth_data') ?? $request->header('authorization'); - - if (!$authorization) { - return response()->json([ - 'message' => ResponseEnum::CLIENT_HTTP_UNAUTHORIZED - ], 401); - } - - $user = AuthService::findUserByBearerToken($authorization); - - if (!$user) { - return response()->json([ - 'message' => ResponseEnum::CLIENT_HTTP_UNAUTHORIZED_EXPIRED - ], 401); - } - - $url = $this->loginService->generateQuickLoginUrl($user, $request->input('redirect')); - return $this->success($url); - } - - /** - * 忘记密码处理 - */ - public function forget(AuthForget $request) - { - [$success, $result] = $this->loginService->resetPassword( - $request->input('email'), - $request->input('email_code'), - $request->input('password') - ); - - if (!$success) { - return $this->fail($result); - } - - return $this->success(true); - } -} diff --git a/Xboard/app/Http/Controllers/V1/Passport/CommController.php b/Xboard/app/Http/Controllers/V1/Passport/CommController.php deleted file mode 100644 index 663badf..0000000 --- a/Xboard/app/Http/Controllers/V1/Passport/CommController.php +++ /dev/null @@ -1,76 +0,0 @@ -verify($request); - if (!$captchaValid) { - return $this->fail($captchaError); - } - - $email = $request->input('email'); - - // 检查白名单后缀限制 - if ((int) admin_setting('email_whitelist_enable', 0)) { - $isRegisteredEmail = User::byEmail($email)->exists(); - if (!$isRegisteredEmail) { - $allowedSuffixes = Helper::getEmailSuffix(); - $emailSuffix = substr(strrchr($email, '@'), 1); - - if (!in_array($emailSuffix, $allowedSuffixes)) { - return $this->fail([400, __('Email suffix is not in whitelist')]); - } - } - } - - if (Cache::get(CacheKey::get('LAST_SEND_EMAIL_VERIFY_TIMESTAMP', $email))) { - return $this->fail([400, __('Email verification code has been sent, please request again later')]); - } - $code = rand(100000, 999999); - $subject = admin_setting('app_name', 'XBoard') . __('Email verification code'); - - SendEmailJob::dispatch([ - 'email' => $email, - 'subject' => $subject, - 'template_name' => 'verify', - 'template_value' => [ - 'name' => admin_setting('app_name', 'XBoard'), - 'code' => $code, - 'url' => admin_setting('app_url') - ] - ]); - - Cache::put(CacheKey::get('EMAIL_VERIFY_CODE', $email), $code, 300); - Cache::put(CacheKey::get('LAST_SEND_EMAIL_VERIFY_TIMESTAMP', $email), time(), 60); - return $this->success(true); - } - - public function pv(Request $request) - { - $inviteCode = InviteCode::where('code', $request->input('invite_code'))->first(); - if ($inviteCode) { - $inviteCode->pv = $inviteCode->pv + 1; - $inviteCode->save(); - } - - return $this->success(true); - } - -} diff --git a/Xboard/app/Http/Controllers/V1/Server/ShadowsocksTidalabController.php b/Xboard/app/Http/Controllers/V1/Server/ShadowsocksTidalabController.php deleted file mode 100644 index 62e5af9..0000000 --- a/Xboard/app/Http/Controllers/V1/Server/ShadowsocksTidalabController.php +++ /dev/null @@ -1,65 +0,0 @@ -attributes->get('node_info'); - Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_CHECK_AT', $server->id), time(), 3600); - $users = ServerService::getAvailableUsers($server); - $result = []; - foreach ($users as $user) { - array_push($result, [ - 'id' => $user->id, - 'port' => $server->server_port, - 'cipher' => $server->cipher, - 'secret' => $user->uuid - ]); - } - $eTag = sha1(json_encode($result)); - if (strpos($request->header('If-None-Match'), $eTag) !== false ) { - return response(null,304); - } - return response([ - 'data' => $result - ])->header('ETag', "\"{$eTag}\""); - } - - // 后端提交数据 - public function submit(Request $request) - { - $server = $request->attributes->get('node_info'); - $data = json_decode(request()->getContent(), true); - Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_ONLINE_USER', $server->id), count($data), 3600); - Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_PUSH_AT', $server->id), time(), 3600); - $userService = new UserService(); - $formatData = []; - - foreach ($data as $item) { - $formatData[$item['user_id']] = [$item['u'], $item['d']]; - } - $userService->trafficFetch($server, 'shadowsocks', $formatData); - - return response([ - 'ret' => 1, - 'msg' => 'ok' - ]); - } -} \ No newline at end of file diff --git a/Xboard/app/Http/Controllers/V1/Server/TrojanTidalabController.php b/Xboard/app/Http/Controllers/V1/Server/TrojanTidalabController.php deleted file mode 100644 index ceff48f..0000000 --- a/Xboard/app/Http/Controllers/V1/Server/TrojanTidalabController.php +++ /dev/null @@ -1,108 +0,0 @@ -attributes->get('node_info'); - if ($server->type !== 'trojan') { - return $this->fail([400, '节点不存在']); - } - Cache::put(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $server->id), time(), 3600); - $users = ServerService::getAvailableUsers($server); - $result = []; - foreach ($users as $user) { - $user->trojan_user = [ - "password" => $user->uuid, - ]; - unset($user->uuid); - array_push($result, $user); - } - $eTag = sha1(json_encode($result)); - if (strpos($request->header('If-None-Match'), $eTag) !== false) { - return response(null, 304); - } - return response([ - 'msg' => 'ok', - 'data' => $result, - ])->header('ETag', "\"{$eTag}\""); - } - - // 后端提交数据 - public function submit(Request $request) - { - $server = $request->attributes->get('node_info'); - if ($server->type !== 'trojan') { - return $this->fail([400, '节点不存在']); - } - $data = json_decode(request()->getContent(), true); - Cache::put(CacheKey::get('SERVER_TROJAN_ONLINE_USER', $server->id), count($data), 3600); - Cache::put(CacheKey::get('SERVER_TROJAN_LAST_PUSH_AT', $server->id), time(), 3600); - $userService = new UserService(); - $formatData = []; - foreach ($data as $item) { - $formatData[$item['user_id']] = [$item['u'], $item['d']]; - } - $userService->trafficFetch($server, 'trojan', $formatData); - - return response([ - 'ret' => 1, - 'msg' => 'ok' - ]); - } - - // 后端获取配置 - public function config(Request $request) - { - $server = $request->attributes->get('node_info'); - if ($server->type !== 'trojan') { - return $this->fail([400, '节点不存在']); - } - $request->validate([ - 'node_id' => 'required', - 'local_port' => 'required' - ], [ - 'node_id.required' => '节点ID不能为空', - 'local_port.required' => '本地端口不能为空' - ]); - try { - $json = $this->getTrojanConfig($server, $request->input('local_port')); - } catch (\Exception $e) { - \Log::error($e); - return $this->fail([500, '配置获取失败']); - } - - return (json_encode($json, JSON_UNESCAPED_UNICODE)); - } - - private function getTrojanConfig($server, int $localPort) - { - $protocolSettings = $server->protocol_settings; - $json = json_decode(self::TROJAN_CONFIG); - $json->local_port = $server->server_port; - $json->ssl->sni = data_get($protocolSettings, 'server_name', $server->host); - $json->ssl->cert = "/root/.cert/server.crt"; - $json->ssl->key = "/root/.cert/server.key"; - $json->api->api_port = $localPort; - return $json; - } -} \ No newline at end of file diff --git a/Xboard/app/Http/Controllers/V1/Server/UniProxyController.php b/Xboard/app/Http/Controllers/V1/Server/UniProxyController.php deleted file mode 100644 index 5901c4b..0000000 --- a/Xboard/app/Http/Controllers/V1/Server/UniProxyController.php +++ /dev/null @@ -1,178 +0,0 @@ -attributes->get('node_info'); - } - - // 后端获取用户 - public function user(Request $request) - { - ini_set('memory_limit', -1); - $node = $this->getNodeInfo($request); - $nodeType = $node->type; - $nodeId = $node->id; - Cache::put(CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_CHECK_AT', $nodeId), time(), 3600); - $users = ServerService::getAvailableUsers($node); - - $response['users'] = $users; - - $eTag = sha1(json_encode($response)); - if (strpos($request->header('If-None-Match', ''), $eTag) !== false) { - return response(null, 304); - } - - return response($response)->header('ETag', "\"{$eTag}\""); - } - - // 后端提交数据 - public function push(Request $request) - { - $res = json_decode(request()->getContent(), true); - if (!is_array($res)) { - return $this->fail([422, 'Invalid data format']); - } - $data = array_filter($res, function ($item) { - return is_array($item) - && count($item) === 2 - && is_numeric($item[0]) - && is_numeric($item[1]); - }); - if (empty($data)) { - return $this->success(true); - } - $node = $this->getNodeInfo($request); - $nodeType = $node->type; - $nodeId = $node->id; - - Cache::put( - CacheKey::get('SERVER_' . strtoupper($nodeType) . '_ONLINE_USER', $nodeId), - count($data), - 3600 - ); - Cache::put( - CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_PUSH_AT', $nodeId), - time(), - 3600 - ); - - $userService = new UserService(); - $userService->trafficFetch($node, $nodeType, $data); - return $this->success(true); - } - - // 后端获取配置 - public function config(Request $request) - { - $node = $this->getNodeInfo($request); - $response = ServerService::buildNodeConfig($node); - - $response['base_config'] = [ - 'push_interval' => (int) admin_setting('server_push_interval', 60), - 'pull_interval' => (int) admin_setting('server_pull_interval', 60) - ]; - - $eTag = sha1(json_encode($response)); - if (strpos($request->header('If-None-Match', ''), $eTag) !== false) { - return response(null, 304); - } - return response($response)->header('ETag', "\"{$eTag}\""); - } - - // 获取在线用户数据 - public function alivelist(Request $request): JsonResponse - { - $node = $this->getNodeInfo($request); - $deviceLimitUsers = ServerService::getAvailableUsers($node) - ->where('device_limit', '>', 0); - - $alive = $this->deviceStateService->getAliveList(collect($deviceLimitUsers)); - - return response()->json(['alive' => (object) $alive]); - } - - // 后端提交在线数据 - public function alive(Request $request): JsonResponse - { - $node = $this->getNodeInfo($request); - $data = json_decode(request()->getContent(), true); - if ($data === null) { - return response()->json([ - 'error' => 'Invalid online data' - ], 400); - } - - foreach ($data as $uid => $ips) { - $this->deviceStateService->setDevices((int) $uid, $node->id, $ips); - } - - return response()->json(['data' => true]); - } - - // 提交节点负载状态 - public function status(Request $request): JsonResponse - { - $node = $this->getNodeInfo($request); - - $data = $request->validate([ - 'cpu' => 'required|numeric|min:0|max:100', - 'mem.total' => 'required|integer|min:0', - 'mem.used' => 'required|integer|min:0', - 'swap.total' => 'required|integer|min:0', - 'swap.used' => 'required|integer|min:0', - 'disk.total' => 'required|integer|min:0', - 'disk.used' => 'required|integer|min:0', - ]); - - $nodeType = $node->type; - $nodeId = $node->id; - - $statusData = [ - 'cpu' => (float) $data['cpu'], - 'mem' => [ - 'total' => (int) $data['mem']['total'], - 'used' => (int) $data['mem']['used'], - ], - 'swap' => [ - 'total' => (int) $data['swap']['total'], - 'used' => (int) $data['swap']['used'], - ], - 'disk' => [ - 'total' => (int) $data['disk']['total'], - 'used' => (int) $data['disk']['used'], - ], - 'updated_at' => now()->timestamp, - ]; - - $cacheTime = max(300, (int) admin_setting('server_push_interval', 60) * 3); - cache([ - CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LOAD_STATUS', $nodeId) => $statusData, - CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_LOAD_AT', $nodeId) => now()->timestamp, - ], $cacheTime); - - return response()->json(['data' => true, "code" => 0, "message" => "success"]); - } -} diff --git a/Xboard/app/Http/Controllers/V1/User/CommController.php b/Xboard/app/Http/Controllers/V1/User/CommController.php deleted file mode 100644 index fa0ae27..0000000 --- a/Xboard/app/Http/Controllers/V1/User/CommController.php +++ /dev/null @@ -1,39 +0,0 @@ - (int)admin_setting('telegram_bot_enable', 0), - 'telegram_discuss_link' => admin_setting('telegram_discuss_link'), - 'stripe_pk' => admin_setting('stripe_pk_live'), - 'withdraw_methods' => admin_setting('commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT), - 'withdraw_close' => (int)admin_setting('withdraw_close_enable', 0), - 'currency' => admin_setting('currency', 'CNY'), - 'currency_symbol' => admin_setting('currency_symbol', '¥'), - 'commission_distribution_enable' => (int)admin_setting('commission_distribution_enable', 0), - 'commission_distribution_l1' => admin_setting('commission_distribution_l1'), - 'commission_distribution_l2' => admin_setting('commission_distribution_l2'), - 'commission_distribution_l3' => admin_setting('commission_distribution_l3') - ]; - return $this->success($data); - } - - public function getStripePublicKey(Request $request) - { - $payment = Payment::where('id', $request->input('id')) - ->where('payment', 'StripeCredit') - ->first(); - if (!$payment) throw new ApiException('payment is not found'); - return $this->success($payment->config['stripe_pk_live']); - } -} diff --git a/Xboard/app/Http/Controllers/V1/User/CouponController.php b/Xboard/app/Http/Controllers/V1/User/CouponController.php deleted file mode 100644 index b7c091f..0000000 --- a/Xboard/app/Http/Controllers/V1/User/CouponController.php +++ /dev/null @@ -1,25 +0,0 @@ -input('code'))) { - return $this->fail([422, __('Coupon cannot be empty')]); - } - $couponService = new CouponService($request->input('code')); - $couponService->setPlanId($request->input('plan_id')); - $couponService->setUserId($request->user()->id); - $couponService->setPeriod($request->input('period')); - $couponService->check(); - return $this->success(CouponResource::make($couponService->getCoupon())); - } -} diff --git a/Xboard/app/Http/Controllers/V1/User/GiftCardController.php b/Xboard/app/Http/Controllers/V1/User/GiftCardController.php deleted file mode 100644 index 507c164..0000000 --- a/Xboard/app/Http/Controllers/V1/User/GiftCardController.php +++ /dev/null @@ -1,193 +0,0 @@ -input('code')); - $giftCardService->setUser($request->user()); - - // 1. 验证礼品卡本身是否有效 (如不存在、已过期、已禁用) - $giftCardService->validateIsActive(); - - // 2. 检查用户是否满足使用条件,但不在此处抛出异常 - $eligibility = $giftCardService->checkUserEligibility(); - - // 3. 获取卡片信息和奖励预览 - $codeInfo = $giftCardService->getCodeInfo(); - $rewardPreview = $giftCardService->previewRewards(); - - return $this->success([ - 'code_info' => $codeInfo, // 这里面已经包含 plan_info - 'reward_preview' => $rewardPreview, - 'can_redeem' => $eligibility['can_redeem'], - 'reason' => $eligibility['reason'], - ]); - - } catch (ApiException $e) { - // 这里只捕获 validateIsActive 抛出的异常 - return $this->fail([400, $e->getMessage()]); - } catch (\Exception $e) { - Log::error('礼品卡查询失败', [ - 'code' => $request->input('code'), - 'user_id' => $request->user()->id, - 'error' => $e->getMessage(), - ]); - return $this->fail([500, '查询失败,请稍后重试']); - } - } - - /** - * 使用兑换码 - */ - public function redeem(GiftCardRedeemRequest $request) - { - try { - $giftCardService = new GiftCardService($request->input('code')); - $giftCardService->setUser($request->user()); - $giftCardService->validate(); - - // 使用礼品卡 - $result = $giftCardService->redeem([ - // 'ip_address' => $request->ip(), - 'user_agent' => $request->userAgent(), - ]); - - Log::info('礼品卡使用成功', [ - 'code' => $request->input('code'), - 'user_id' => $request->user()->id, - 'rewards' => $result['rewards'], - ]); - - return $this->success([ - 'message' => '兑换成功!', - 'rewards' => $result['rewards'], - 'invite_rewards' => $result['invite_rewards'], - 'template_name' => $result['template_name'], - ]); - - } catch (ApiException $e) { - return $this->fail([400, $e->getMessage()]); - } catch (\Exception $e) { - Log::error('礼品卡使用失败', [ - 'code' => $request->input('code'), - 'user_id' => $request->user()->id, - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - ]); - return $this->fail([500, '兑换失败,请稍后重试']); - } - } - - /** - * 获取用户兑换记录 - */ - public function history(Request $request) - { - $request->validate([ - 'page' => 'integer|min:1', - 'per_page' => 'integer|min:1|max:100', - ]); - - $perPage = $request->input('per_page', 15); - - $usages = GiftCardUsage::with(['template', 'code']) - ->where('user_id', $request->user()->id) - ->orderBy('created_at', 'desc') - ->paginate($perPage); - - $data = $usages->getCollection()->map(function (GiftCardUsage $usage) { - return [ - 'id' => $usage->id, - 'code' => ($usage->code instanceof \App\Models\GiftCardCode && $usage->code->code) - ? (substr($usage->code->code, 0, 8) . '****') - : '', - 'template_name' => $usage->template->name ?? '', - 'template_type' => $usage->template->type ?? '', - 'template_type_name' => $usage->template->type_name ?? '', - 'rewards_given' => $usage->rewards_given, - 'invite_rewards' => $usage->invite_rewards, - 'multiplier_applied' => $usage->multiplier_applied, - 'created_at' => $usage->created_at, - ]; - })->values(); - return response()->json([ - 'data' => $data, - 'pagination' => [ - 'current_page' => $usages->currentPage(), - 'last_page' => $usages->lastPage(), - 'per_page' => $usages->perPage(), - 'total' => $usages->total(), - ], - ]); - } - - /** - * 获取兑换记录详情 - */ - public function detail(Request $request) - { - $request->validate([ - 'id' => 'required|integer|exists:v2_gift_card_usage,id', - ]); - - $usage = GiftCardUsage::with(['template', 'code', 'inviteUser']) - ->where('user_id', $request->user()->id) - ->where('id', $request->input('id')) - ->first(); - - if (!$usage) { - return $this->fail([404, '记录不存在']); - } - - return $this->success([ - 'id' => $usage->id, - 'code' => $usage->code->code ?? '', - 'template' => [ - 'name' => $usage->template->name ?? '', - 'description' => $usage->template->description ?? '', - 'type' => $usage->template->type ?? '', - 'type_name' => $usage->template->type_name ?? '', - 'icon' => $usage->template->icon ?? '', - 'theme_color' => $usage->template->theme_color ?? '', - ], - 'rewards_given' => $usage->rewards_given, - 'invite_rewards' => $usage->invite_rewards, - 'invite_user' => $usage->inviteUser ? [ - 'id' => $usage->inviteUser->id ?? '', - 'email' => isset($usage->inviteUser->email) ? (substr($usage->inviteUser->email, 0, 3) . '***@***') : '', - ] : null, - 'user_level_at_use' => $usage->user_level_at_use, - 'plan_id_at_use' => $usage->plan_id_at_use, - 'multiplier_applied' => $usage->multiplier_applied, - // 'ip_address' => $usage->ip_address, - 'notes' => $usage->notes, - 'created_at' => $usage->created_at, - ]); - } - - /** - * 获取可用的礼品卡类型 - */ - public function types(Request $request) - { - return $this->success([ - 'types' => \App\Models\GiftCardTemplate::getTypeMap(), - ]); - } -} diff --git a/Xboard/app/Http/Controllers/V1/User/InviteController.php b/Xboard/app/Http/Controllers/V1/User/InviteController.php deleted file mode 100644 index cbde315..0000000 --- a/Xboard/app/Http/Controllers/V1/User/InviteController.php +++ /dev/null @@ -1,79 +0,0 @@ -user()->id)->where('status', 0)->count() >= admin_setting('invite_gen_limit', 5)) { - return $this->fail([400,__('The maximum number of creations has been reached')]); - } - $inviteCode = new InviteCode(); - $inviteCode->user_id = $request->user()->id; - $inviteCode->code = Helper::randomChar(8); - return $this->success($inviteCode->save()); - } - - public function details(Request $request) - { - $current = $request->input('current') ? $request->input('current') : 1; - $pageSize = $request->input('page_size') >= 10 ? $request->input('page_size') : 10; - $builder = CommissionLog::where('invite_user_id', $request->user()->id) - ->where('get_amount', '>', 0) - ->orderBy('created_at', 'DESC'); - $total = $builder->count(); - $details = $builder->forPage($current, $pageSize) - ->get(); - return response([ - 'data' => ComissionLogResource::collection($details), - 'total' => $total - ]); - } - - public function fetch(Request $request) - { - $commission_rate = admin_setting('invite_commission', 10); - $user = User::find($request->user()->id) - ->load(['codes' => fn($query) => $query->where('status', 0)]); - if ($user->commission_rate) { - $commission_rate = $user->commission_rate; - } - $uncheck_commission_balance = (int)Order::where('status', 3) - ->where('commission_status', 0) - ->where('invite_user_id', $user->id) - ->sum('commission_balance'); - if (admin_setting('commission_distribution_enable', 0)) { - $uncheck_commission_balance = $uncheck_commission_balance * (admin_setting('commission_distribution_l1') / 100); - } - $stat = [ - //已注册用户数 - (int)User::where('invite_user_id', $user->id)->count(), - //有效的佣金 - (int)CommissionLog::where('invite_user_id', $user->id) - ->sum('get_amount'), - //确认中的佣金 - $uncheck_commission_balance, - //佣金比例 - (int)$commission_rate, - //可用佣金 - (int)$user->commission_balance - ]; - $data = [ - 'codes' => InviteCodeResource::collection($user->codes), - 'stat' => $stat - ]; - return $this->success($data); - } -} diff --git a/Xboard/app/Http/Controllers/V1/User/KnowledgeController.php b/Xboard/app/Http/Controllers/V1/User/KnowledgeController.php deleted file mode 100644 index e3646c9..0000000 --- a/Xboard/app/Http/Controllers/V1/User/KnowledgeController.php +++ /dev/null @@ -1,150 +0,0 @@ -userService = $userService; - } - - public function fetch(Request $request) - { - $request->validate([ - 'id' => 'nullable|sometimes|integer|min:1', - 'language' => 'nullable|sometimes|string|max:10', - 'keyword' => 'nullable|sometimes|string|max:255', - ]); - - return $request->input('id') - ? $this->fetchSingle($request) - : $this->fetchList($request); - } - - private function fetchSingle(Request $request) - { - $knowledge = $this->buildKnowledgeQuery() - ->where('id', $request->input('id')) - ->first(); - - if (!$knowledge) { - return $this->fail([500, __('Article does not exist')]); - } - - $knowledge = $knowledge->toArray(); - $knowledge = $this->processKnowledgeContent($knowledge, $request->user()); - - return $this->success(KnowledgeResource::make($knowledge)); - } - - private function fetchList(Request $request) - { - $builder = $this->buildKnowledgeQuery(['id', 'category', 'title', 'updated_at', 'body']) - ->where('language', $request->input('language')) - ->orderBy('sort', 'ASC'); - - $keyword = $request->input('keyword'); - if ($keyword) { - $builder = $builder->where(function ($query) use ($keyword) { - $query->where('title', 'LIKE', "%{$keyword}%") - ->orWhere('body', 'LIKE', "%{$keyword}%"); - }); - } - - $knowledges = $builder->get() - ->map(function ($knowledge) use ($request) { - $knowledge = $knowledge->toArray(); - $knowledge = $this->processKnowledgeContent($knowledge, $request->user()); - return KnowledgeResource::make($knowledge); - }) - ->groupBy('category'); - - return $this->success($knowledges); - } - - private function buildKnowledgeQuery(array $select = ['*']) - { - return Knowledge::select($select)->where('show', 1); - } - - private function processKnowledgeContent(array $knowledge, User $user): array - { - if (!isset($knowledge['body'])) { - return $knowledge; - } - - if (!$this->userService->isAvailable($user)) { - $this->formatAccessData($knowledge['body']); - } - $subscribeUrl = Helper::getSubscribeUrl($user['token']); - $knowledge['body'] = $this->replacePlaceholders($knowledge['body'], $subscribeUrl); - - return $knowledge; - } - - private function formatAccessData(&$body): void - { - $rules = [ - [ - 'type' => 'regex', - 'pattern' => '/(.*?)/s', - 'replacement' => '
' . __('You must have a valid subscription to view content in this area') . '
' - ] - ]; - - $this->applyReplacementRules($body, $rules); - } - - private function replacePlaceholders(string $body, string $subscribeUrl): string - { - $rules = [ - [ - 'type' => 'string', - 'search' => '{{siteName}}', - 'replacement' => admin_setting('app_name', 'XBoard') - ], - [ - 'type' => 'string', - 'search' => '{{subscribeUrl}}', - 'replacement' => $subscribeUrl - ], - [ - 'type' => 'string', - 'search' => '{{urlEncodeSubscribeUrl}}', - 'replacement' => urlencode($subscribeUrl) - ], - [ - 'type' => 'string', - 'search' => '{{safeBase64SubscribeUrl}}', - 'replacement' => str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($subscribeUrl)) - ] - ]; - - $this->applyReplacementRules($body, $rules); - return $body; - } - - private function applyReplacementRules(string &$body, array $rules): void - { - foreach ($rules as $rule) { - if ($rule['type'] === 'regex') { - $body = preg_replace($rule['pattern'], $rule['replacement'], $body); - } else { - $body = str_replace($rule['search'], $rule['replacement'], $body); - } - } - } -} diff --git a/Xboard/app/Http/Controllers/V1/User/NoticeController.php b/Xboard/app/Http/Controllers/V1/User/NoticeController.php deleted file mode 100644 index 9382e06..0000000 --- a/Xboard/app/Http/Controllers/V1/User/NoticeController.php +++ /dev/null @@ -1,26 +0,0 @@ -input('current') ? $request->input('current') : 1; - $pageSize = 5; - $model = Notice::orderBy('sort', 'ASC') - ->orderBy('id', 'DESC') - ->where('show', true); - $total = $model->count(); - $res = $model->forPage($current, $pageSize) - ->get(); - return response([ - 'data' => $res, - 'total' => $total - ]); - } -} diff --git a/Xboard/app/Http/Controllers/V1/User/OrderController.php b/Xboard/app/Http/Controllers/V1/User/OrderController.php deleted file mode 100644 index 7b28128..0000000 --- a/Xboard/app/Http/Controllers/V1/User/OrderController.php +++ /dev/null @@ -1,212 +0,0 @@ -validate([ - 'status' => 'nullable|integer|in:0,1,2,3', - ]); - $orders = Order::with('plan') - ->where('user_id', $request->user()->id) - ->when($request->input('status') !== null, function ($query) use ($request) { - $query->where('status', $request->input('status')); - }) - ->orderBy('created_at', 'DESC') - ->get(); - - return $this->success(OrderResource::collection($orders)); - } - - public function detail(Request $request) - { - $request->validate([ - 'trade_no' => 'required|string', - ]); - $order = Order::with(['payment', 'plan']) - ->where('user_id', $request->user()->id) - ->where('trade_no', $request->input('trade_no')) - ->first(); - if (!$order) { - return $this->fail([400, __('Order does not exist or has been paid')]); - } - $order['try_out_plan_id'] = (int) admin_setting('try_out_plan_id'); - if (!$order->plan) { - return $this->fail([400, __('Subscription plan does not exist')]); - } - if ($order->surplus_order_ids) { - $order['surplus_orders'] = Order::whereIn('id', $order->surplus_order_ids)->get(); - } - return $this->success(OrderResource::make($order)); - } - - public function save(OrderSave $request) - { - $request->validate([ - 'plan_id' => 'required|exists:App\Models\Plan,id', - 'period' => 'required|string' - ]); - - $user = User::findOrFail($request->user()->id); - $userService = app(UserService::class); - - if ($userService->isNotCompleteOrderByUserId($user->id)) { - throw new ApiException(__('You have an unpaid or pending order, please try again later or cancel it')); - } - - $plan = Plan::findOrFail($request->input('plan_id')); - $planService = new PlanService($plan); - - $planService->validatePurchase($user, $request->input('period')); - - $order = OrderService::createFromRequest( - $user, - $plan, - $request->input('period'), - $request->input('coupon_code') - ); - - return $this->success($order->trade_no); - } - - protected function applyCoupon(Order $order, string $couponCode): void - { - $couponService = new CouponService($couponCode); - if (!$couponService->use($order)) { - throw new ApiException(__('Coupon failed')); - } - $order->coupon_id = $couponService->getId(); - } - - protected function handleUserBalance(Order $order, User $user, UserService $userService): void - { - $remainingBalance = $user->balance - $order->total_amount; - - if ($remainingBalance > 0) { - if (!$userService->addBalance($order->user_id, -$order->total_amount)) { - throw new ApiException(__('Insufficient balance')); - } - $order->balance_amount = $order->total_amount; - $order->total_amount = 0; - } else { - if (!$userService->addBalance($order->user_id, -$user->balance)) { - throw new ApiException(__('Insufficient balance')); - } - $order->balance_amount = $user->balance; - $order->total_amount = $order->total_amount - $user->balance; - } - } - - public function checkout(Request $request) - { - $tradeNo = $request->input('trade_no'); - $method = $request->input('method'); - $order = Order::where('trade_no', $tradeNo) - ->where('user_id', $request->user()->id) - ->where('status', 0) - ->first(); - if (!$order) { - return $this->fail([400, __('Order does not exist or has been paid')]); - } - // free process - if ($order->total_amount <= 0) { - $orderService = new OrderService($order); - if (!$orderService->paid($order->trade_no)) - return $this->fail([400, '支付失败']); - return response([ - 'type' => -1, - 'data' => true - ]); - } - $payment = Payment::find($method); - if (!$payment || !$payment->enable) { - return $this->fail([400, __('Payment method is not available')]); - } - $paymentService = new PaymentService($payment->payment, $payment->id); - $order->handling_amount = NULL; - if ($payment->handling_fee_fixed || $payment->handling_fee_percent) { - $order->handling_amount = (int) round(($order->total_amount * ($payment->handling_fee_percent / 100)) + $payment->handling_fee_fixed); - } - $order->payment_id = $method; - if (!$order->save()) - return $this->fail([400, __('Request failed, please try again later')]); - $result = $paymentService->pay([ - 'trade_no' => $tradeNo, - 'total_amount' => isset($order->handling_amount) ? ($order->total_amount + $order->handling_amount) : $order->total_amount, - 'user_id' => $order->user_id, - 'stripe_token' => $request->input('token') - ]); - return response([ - 'type' => $result['type'], - 'data' => $result['data'] - ]); - } - - public function check(Request $request) - { - $tradeNo = $request->input('trade_no'); - $order = Order::where('trade_no', $tradeNo) - ->where('user_id', $request->user()->id) - ->first(); - if (!$order) { - return $this->fail([400, __('Order does not exist')]); - } - return $this->success($order->status); - } - - public function getPaymentMethod() - { - $methods = Payment::select([ - 'id', - 'name', - 'payment', - 'icon', - 'handling_fee_fixed', - 'handling_fee_percent' - ]) - ->where('enable', 1) - ->orderBy('sort', 'ASC') - ->get(); - - return $this->success($methods); - } - - public function cancel(Request $request) - { - if (empty($request->input('trade_no'))) { - return $this->fail([422, __('Invalid parameter')]); - } - $order = Order::where('trade_no', $request->input('trade_no')) - ->where('user_id', $request->user()->id) - ->first(); - if (!$order) { - return $this->fail([400, __('Order does not exist')]); - } - if ($order->status !== 0) { - return $this->fail([400, __('You can only cancel pending orders')]); - } - $orderService = new OrderService($order); - if (!$orderService->cancel()) { - return $this->fail([400, __('Cancel failed')]); - } - return $this->success(true); - } -} diff --git a/Xboard/app/Http/Controllers/V1/User/PlanController.php b/Xboard/app/Http/Controllers/V1/User/PlanController.php deleted file mode 100644 index 7db7aad..0000000 --- a/Xboard/app/Http/Controllers/V1/User/PlanController.php +++ /dev/null @@ -1,38 +0,0 @@ -planService = $planService; - } - public function fetch(Request $request) - { - $user = User::find($request->user()->id); - if ($request->input('id')) { - $plan = Plan::where('id', $request->input('id'))->first(); - if (!$plan) { - return $this->fail([400, __('Subscription plan does not exist')]); - } - if (!$this->planService->isPlanAvailableForUser($plan, $user)) { - return $this->fail([400, __('Subscription plan does not exist')]); - } - return $this->success(PlanResource::make($plan)); - } - - $plans = $this->planService->getAvailablePlans(); - return $this->success(PlanResource::collection($plans)); - } -} diff --git a/Xboard/app/Http/Controllers/V1/User/ServerController.php b/Xboard/app/Http/Controllers/V1/User/ServerController.php deleted file mode 100644 index f12b005..0000000 --- a/Xboard/app/Http/Controllers/V1/User/ServerController.php +++ /dev/null @@ -1,31 +0,0 @@ -user()->id); - $servers = []; - $userService = new UserService(); - if ($userService->isAvailable($user)) { - $servers = ServerService::getAvailableServers($user); - } - $eTag = sha1(json_encode(array_column($servers, 'cache_key'))); - if (strpos($request->header('If-None-Match', ''), $eTag) !== false ) { - return response(null,304); - } - $data = NodeResource::collection($servers); - return response([ - 'data' => $data - ])->header('ETag', "\"{$eTag}\""); - } -} diff --git a/Xboard/app/Http/Controllers/V1/User/StatController.php b/Xboard/app/Http/Controllers/V1/User/StatController.php deleted file mode 100644 index 11bb9c0..0000000 --- a/Xboard/app/Http/Controllers/V1/User/StatController.php +++ /dev/null @@ -1,26 +0,0 @@ -startOfMonth()->timestamp; - $records = StatUser::query() - ->where('user_id', $request->user()->id) - ->where('record_at', '>=', $startDate) - ->orderBy('record_at', 'DESC') - ->get(); - - $data = TrafficLogResource::collection(collect($records)); - return $this->success($data); - } -} diff --git a/Xboard/app/Http/Controllers/V1/User/TelegramController.php b/Xboard/app/Http/Controllers/V1/User/TelegramController.php deleted file mode 100644 index 2cf65c7..0000000 --- a/Xboard/app/Http/Controllers/V1/User/TelegramController.php +++ /dev/null @@ -1,26 +0,0 @@ -getMe(); - $data = [ - 'username' => $response->result->username - ]; - return $this->success($data); - } - - public function unbind(Request $request) - { - $user = User::where('user_id', $request->user()->id)->first(); - } -} diff --git a/Xboard/app/Http/Controllers/V1/User/TicketController.php b/Xboard/app/Http/Controllers/V1/User/TicketController.php deleted file mode 100644 index 05ca915..0000000 --- a/Xboard/app/Http/Controllers/V1/User/TicketController.php +++ /dev/null @@ -1,154 +0,0 @@ -input('id')) { - $ticket = Ticket::where('id', $request->input('id')) - ->where('user_id', $request->user()->id) - ->first() - ->load('message'); - if (!$ticket) { - return $this->fail([400, __('Ticket does not exist')]); - } - $ticket['message'] = TicketMessage::where('ticket_id', $ticket->id)->get(); - $ticket['message']->each(function ($message) use ($ticket) { - $message['is_me'] = ($message['user_id'] == $ticket->user_id); - }); - return $this->success(TicketResource::make($ticket)->additional(['message' => true])); - } - $ticket = Ticket::where('user_id', $request->user()->id) - ->orderBy('created_at', 'DESC') - ->get(); - return $this->success(TicketResource::collection($ticket)); - } - - public function save(TicketSave $request) - { - $ticketService = new TicketService(); - $ticket = $ticketService->createTicket( - $request->user()->id, - $request->input('subject'), - $request->input('level'), - $request->input('message') - ); - HookManager::call('ticket.create.after', $ticket); - return $this->success(true); - - } - - public function reply(Request $request) - { - if (empty($request->input('id'))) { - return $this->fail([400, __('Invalid parameter')]); - } - if (empty($request->input('message'))) { - return $this->fail([400, __('Message cannot be empty')]); - } - $ticket = Ticket::where('id', $request->input('id')) - ->where('user_id', $request->user()->id) - ->first(); - if (!$ticket) { - return $this->fail([400, __('Ticket does not exist')]); - } - if ($ticket->status) { - return $this->fail([400, __('The ticket is closed and cannot be replied')]); - } - if ((int) admin_setting('ticket_must_wait_reply', 0) && $request->user()->id == $this->getLastMessage($ticket->id)->user_id) { - return $this->fail(codeResponse: [400, __('Please wait for the technical enginneer to reply')]); - } - $ticketService = new TicketService(); - if ( - !$ticketService->reply( - $ticket, - $request->input('message'), - $request->user()->id - ) - ) { - return $this->fail([400, __('Ticket reply failed')]); - } - HookManager::call('ticket.reply.user.after', $ticket); - return $this->success(true); - } - - - public function close(Request $request) - { - if (empty($request->input('id'))) { - return $this->fail([422, __('Invalid parameter')]); - } - $ticket = Ticket::where('id', $request->input('id')) - ->where('user_id', $request->user()->id) - ->first(); - if (!$ticket) { - return $this->fail([400, __('Ticket does not exist')]); - } - $ticket->status = Ticket::STATUS_CLOSED; - if (!$ticket->save()) { - return $this->fail([500, __('Close failed')]); - } - return $this->success(true); - } - - private function getLastMessage($ticketId) - { - return TicketMessage::where('ticket_id', $ticketId) - ->orderBy('id', 'DESC') - ->first(); - } - - public function withdraw(TicketWithdraw $request) - { - if ((int) admin_setting('withdraw_close_enable', 0)) { - return $this->fail([400, 'Unsupported withdraw']); - } - if ( - !in_array( - $request->input('withdraw_method'), - admin_setting('commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT) - ) - ) { - return $this->fail([422, __('Unsupported withdrawal method')]); - } - $user = User::find($request->user()->id); - $limit = admin_setting('commission_withdraw_limit', 100); - if ($limit > ($user->commission_balance / 100)) { - return $this->fail([422, __('The current required minimum withdrawal commission is :limit', ['limit' => $limit])]); - } - try { - $ticketService = new TicketService(); - $subject = __('[Commission Withdrawal Request] This ticket is opened by the system'); - $message = sprintf( - "%s\r\n%s", - __('Withdrawal method') . ":" . $request->input('withdraw_method'), - __('Withdrawal account') . ":" . $request->input('withdraw_account') - ); - $ticket = $ticketService->createTicket( - $request->user()->id, - $subject, - 2, - $message - ); - } catch (\Exception $e) { - throw $e; - } - HookManager::call('ticket.create.after', $ticket); - return $this->success(true); - } -} diff --git a/Xboard/app/Http/Controllers/V1/User/UserController.php b/Xboard/app/Http/Controllers/V1/User/UserController.php deleted file mode 100644 index c561988..0000000 --- a/Xboard/app/Http/Controllers/V1/User/UserController.php +++ /dev/null @@ -1,223 +0,0 @@ -loginService = $loginService; - } - - public function getActiveSession(Request $request) - { - $user = $request->user(); - $authService = new AuthService($user); - return $this->success($authService->getSessions()); - } - - public function removeActiveSession(Request $request) - { - $user = $request->user(); - $authService = new AuthService($user); - return $this->success($authService->removeSession($request->input('session_id'))); - } - - public function checkLogin(Request $request) - { - $data = [ - 'is_login' => $request->user()?->id ? true : false - ]; - if ($request->user()?->is_admin) { - $data['is_admin'] = true; - } - return $this->success($data); - } - - public function changePassword(UserChangePassword $request) - { - $user = $request->user(); - if ( - !Helper::multiPasswordVerify( - $user->password_algo, - $user->password_salt, - $request->input('old_password'), - $user->password - ) - ) { - return $this->fail([400, __('The old password is wrong')]); - } - $user->password = password_hash($request->input('new_password'), PASSWORD_DEFAULT); - $user->password_algo = NULL; - $user->password_salt = NULL; - if (!$user->save()) { - return $this->fail([400, __('Save failed')]); - } - - $currentToken = $user->currentAccessToken(); - if ($currentToken) { - $user->tokens()->where('id', '!=', $currentToken->id)->delete(); - } else { - $user->tokens()->delete(); - } - - return $this->success(true); - } - - public function info(Request $request) - { - $user = User::where('id', $request->user()->id) - ->select([ - 'email', - 'transfer_enable', - 'last_login_at', - 'created_at', - 'banned', - 'remind_expire', - 'remind_traffic', - 'expired_at', - 'balance', - 'commission_balance', - 'plan_id', - 'discount', - 'commission_rate', - 'telegram_id', - 'uuid' - ]) - ->first(); - if (!$user) { - return $this->fail([400, __('The user does not exist')]); - } - $user['avatar_url'] = 'https://cdn.v2ex.com/gravatar/' . md5($user->email) . '?s=64&d=identicon'; - return $this->success($user); - } - - public function getStat(Request $request) - { - $stat = [ - Order::where('status', 0) - ->where('user_id', $request->user()->id) - ->count(), - Ticket::where('status', 0) - ->where('user_id', $request->user()->id) - ->count(), - User::where('invite_user_id', $request->user()->id) - ->count() - ]; - return $this->success($stat); - } - - public function getSubscribe(Request $request) - { - $user = User::where('id', $request->user()->id) - ->select([ - 'plan_id', - 'token', - 'expired_at', - 'u', - 'd', - 'transfer_enable', - 'email', - 'uuid', - 'device_limit', - 'speed_limit', - 'next_reset_at' - ]) - ->first(); - if (!$user) { - return $this->fail([400, __('The user does not exist')]); - } - if ($user->plan_id) { - $user['plan'] = Plan::find($user->plan_id); - if (!$user['plan']) { - return $this->fail([400, __('Subscription plan does not exist')]); - } - } - $user['subscribe_url'] = Helper::getSubscribeUrl($user['token']); - $userService = new UserService(); - $user['reset_day'] = $userService->getResetDay($user); - $user = HookManager::filter('user.subscribe.response', $user); - return $this->success($user); - } - - public function resetSecurity(Request $request) - { - $user = $request->user(); - $user->uuid = Helper::guid(true); - $user->token = Helper::guid(); - if (!$user->save()) { - return $this->fail([400, __('Reset failed')]); - } - return $this->success(Helper::getSubscribeUrl($user->token)); - } - - public function update(UserUpdate $request) - { - $updateData = $request->only([ - 'remind_expire', - 'remind_traffic' - ]); - - $user = $request->user(); - try { - $user->update($updateData); - } catch (\Exception $e) { - return $this->fail([400, __('Save failed')]); - } - - return $this->success(true); - } - - public function transfer(UserTransfer $request) - { - $amount = $request->input('transfer_amount'); - try { - DB::transaction(function () use ($request, $amount) { - $user = User::lockForUpdate()->find($request->user()->id); - if (!$user) { - throw new \Exception(__('The user does not exist')); - } - if ($amount > $user->commission_balance) { - throw new \Exception(__('Insufficient commission balance')); - } - $user->commission_balance -= $amount; - $user->balance += $amount; - if (!$user->save()) { - throw new \Exception(__('Transfer failed')); - } - }); - } catch (\Exception $e) { - return $this->fail([400, $e->getMessage()]); - } - return $this->success(true); - } - - public function getQuickLoginUrl(Request $request) - { - $user = $request->user(); - - $url = $this->loginService->generateQuickLoginUrl($user, $request->input('redirect')); - return $this->success($url); - } -} diff --git a/Xboard/app/Http/Controllers/V2/Admin/ConfigController.php b/Xboard/app/Http/Controllers/V2/Admin/ConfigController.php deleted file mode 100644 index 81c5570..0000000 --- a/Xboard/app/Http/Controllers/V2/Admin/ConfigController.php +++ /dev/null @@ -1,300 +0,0 @@ -success($files); - } - - public function getThemeTemplate() - { - $path = public_path('theme/'); - $files = array_map(function ($item) use ($path) { - return str_replace($path, '', $item); - }, glob($path . '*')); - return $this->success($files); - } - - public function testSendMail(Request $request) - { - $mailLog = MailService::sendEmail([ - 'email' => $request->user()->email, - 'subject' => 'This is xboard test email', - 'template_name' => 'notify', - 'template_value' => [ - 'name' => admin_setting('app_name', 'XBoard'), - 'content' => 'This is xboard test email', - 'url' => admin_setting('app_url') - ] - ]); - return response([ - 'data' => $mailLog, - ]); - } - public function setTelegramWebhook(Request $request) - { - $hookUrl = $this->resolveTelegramWebhookUrl(); - if (blank($hookUrl)) { - return $this->fail([422, 'Telegram Webhook地址未配置']); - } - $hookUrl .= '?' . http_build_query([ - 'access_token' => md5(admin_setting('telegram_bot_token', $request->input('telegram_bot_token'))) - ]); - $telegramService = new TelegramService($request->input('telegram_bot_token')); - $telegramService->getMe(); - $telegramService->setWebhook(url: $hookUrl); - $telegramService->registerBotCommands(); - return $this->success([ - 'success' => true, - 'webhook_url' => $hookUrl, - 'webhook_base_url' => $this->getTelegramWebhookBaseUrl(), - ]); - } - - public function fetch(Request $request) - { - $key = $request->input('key'); - $configMappings = $this->getConfigMappings(); - if ($key && isset($configMappings[$key])) { - return $this->success([$key => $configMappings[$key]]); - } - - return $this->success($configMappings); - } - - /** - * 获取配置映射数据 - * - * @return array 配置映射数组 - */ - private function getConfigMappings(): array - { - return [ - 'invite' => [ - 'invite_force' => (bool) admin_setting('invite_force', 0), - 'invite_commission' => admin_setting('invite_commission', 10), - 'invite_gen_limit' => admin_setting('invite_gen_limit', 5), - 'invite_never_expire' => (bool) admin_setting('invite_never_expire', 0), - 'commission_first_time_enable' => (bool) admin_setting('commission_first_time_enable', 1), - 'commission_auto_check_enable' => (bool) admin_setting('commission_auto_check_enable', 1), - 'commission_withdraw_limit' => admin_setting('commission_withdraw_limit', 100), - 'commission_withdraw_method' => admin_setting('commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT), - 'withdraw_close_enable' => (bool) admin_setting('withdraw_close_enable', 0), - 'commission_distribution_enable' => (bool) admin_setting('commission_distribution_enable', 0), - 'commission_distribution_l1' => admin_setting('commission_distribution_l1'), - 'commission_distribution_l2' => admin_setting('commission_distribution_l2'), - 'commission_distribution_l3' => admin_setting('commission_distribution_l3') - ], - 'site' => [ - 'logo' => admin_setting('logo'), - 'force_https' => (int) admin_setting('force_https', 0), - 'stop_register' => (int) admin_setting('stop_register', 0), - 'app_name' => admin_setting('app_name', 'XBoard'), - 'app_description' => admin_setting('app_description', 'XBoard is best!'), - 'app_url' => admin_setting('app_url'), - 'subscribe_url' => admin_setting('subscribe_url'), - 'try_out_plan_id' => (int) admin_setting('try_out_plan_id', 0), - 'try_out_hour' => (int) admin_setting('try_out_hour', 1), - 'tos_url' => admin_setting('tos_url'), - 'currency' => admin_setting('currency', 'CNY'), - 'currency_symbol' => admin_setting('currency_symbol', '¥'), - 'ticket_must_wait_reply' => (bool) admin_setting('ticket_must_wait_reply', 0), - ], - 'subscribe' => [ - 'plan_change_enable' => (bool) admin_setting('plan_change_enable', 1), - 'reset_traffic_method' => (int) admin_setting('reset_traffic_method', 0), - 'surplus_enable' => (bool) admin_setting('surplus_enable', 1), - 'new_order_event_id' => (int) admin_setting('new_order_event_id', 0), - 'renew_order_event_id' => (int) admin_setting('renew_order_event_id', 0), - 'change_order_event_id' => (int) admin_setting('change_order_event_id', 0), - 'show_info_to_server_enable' => (bool) admin_setting('show_info_to_server_enable', 0), - 'show_protocol_to_server_enable' => (bool) admin_setting('show_protocol_to_server_enable', 0), - 'default_remind_expire' => (bool) admin_setting('default_remind_expire', 1), - 'default_remind_traffic' => (bool) admin_setting('default_remind_traffic', 1), - 'subscribe_path' => admin_setting('subscribe_path', 's'), - ], - 'frontend' => [ - 'frontend_theme' => admin_setting('frontend_theme', 'Xboard'), - 'frontend_theme_sidebar' => admin_setting('frontend_theme_sidebar', 'light'), - 'frontend_theme_header' => admin_setting('frontend_theme_header', 'dark'), - 'frontend_theme_color' => admin_setting('frontend_theme_color', 'default'), - 'frontend_background_url' => admin_setting('frontend_background_url'), - ], - 'server' => [ - 'server_token' => admin_setting('server_token'), - 'server_pull_interval' => admin_setting('server_pull_interval', 60), - 'server_push_interval' => admin_setting('server_push_interval', 60), - 'device_limit_mode' => (int) admin_setting('device_limit_mode', 0), - 'server_ws_enable' => (bool) admin_setting('server_ws_enable', 1), - 'server_ws_url' => admin_setting('server_ws_url', ''), - ], - 'email' => [ - 'email_template' => admin_setting('email_template', 'default'), - 'email_host' => admin_setting('email_host'), - 'email_port' => admin_setting('email_port'), - 'email_username' => admin_setting('email_username'), - 'email_password' => admin_setting('email_password'), - 'email_encryption' => admin_setting('email_encryption'), - 'email_from_address' => admin_setting('email_from_address'), - 'remind_mail_enable' => (bool) admin_setting('remind_mail_enable', false), - ], - 'telegram' => [ - 'telegram_bot_enable' => (bool) admin_setting('telegram_bot_enable', 0), - 'telegram_bot_token' => admin_setting('telegram_bot_token'), - 'telegram_webhook_url' => admin_setting('telegram_webhook_url'), - 'telegram_discuss_link' => admin_setting('telegram_discuss_link') - ], - 'app' => [ - 'windows_version' => admin_setting('windows_version', ''), - 'windows_download_url' => admin_setting('windows_download_url', ''), - 'macos_version' => admin_setting('macos_version', ''), - 'macos_download_url' => admin_setting('macos_download_url', ''), - 'android_version' => admin_setting('android_version', ''), - 'android_download_url' => admin_setting('android_download_url', '') - ], - 'safe' => [ - 'email_verify' => (bool) admin_setting('email_verify', 0), - 'safe_mode_enable' => (bool) admin_setting('safe_mode_enable', 0), - 'secure_path' => admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key')))), - 'email_whitelist_enable' => (bool) admin_setting('email_whitelist_enable', 0), - 'email_whitelist_suffix' => admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT), - 'email_gmail_limit_enable' => (bool) admin_setting('email_gmail_limit_enable', 0), - 'captcha_enable' => (bool) admin_setting('captcha_enable', 0), - 'captcha_type' => admin_setting('captcha_type', 'recaptcha'), - 'recaptcha_key' => admin_setting('recaptcha_key', ''), - 'recaptcha_site_key' => admin_setting('recaptcha_site_key', ''), - 'recaptcha_v3_secret_key' => admin_setting('recaptcha_v3_secret_key', ''), - 'recaptcha_v3_site_key' => admin_setting('recaptcha_v3_site_key', ''), - 'recaptcha_v3_score_threshold' => admin_setting('recaptcha_v3_score_threshold', 0.5), - 'turnstile_secret_key' => admin_setting('turnstile_secret_key', ''), - 'turnstile_site_key' => admin_setting('turnstile_site_key', ''), - 'register_limit_by_ip_enable' => (bool) admin_setting('register_limit_by_ip_enable', 0), - 'register_limit_count' => admin_setting('register_limit_count', 3), - 'register_limit_expire' => admin_setting('register_limit_expire', 60), - 'password_limit_enable' => (bool) admin_setting('password_limit_enable', 1), - 'password_limit_count' => admin_setting('password_limit_count', 5), - 'password_limit_expire' => admin_setting('password_limit_expire', 60), - // 保持向后兼容 - 'recaptcha_enable' => (bool) admin_setting('captcha_enable', 0) - ], - 'subscribe_template' => [ - 'subscribe_template_singbox' => $this->formatTemplateContent( - subscribe_template('singbox') ?? '', - 'json' - ), - 'subscribe_template_clash' => subscribe_template('clash') ?? '', - 'subscribe_template_clashmeta' => subscribe_template('clashmeta') ?? '', - 'subscribe_template_stash' => subscribe_template('stash') ?? '', - 'subscribe_template_surge' => subscribe_template('surge') ?? '', - 'subscribe_template_surfboard' => subscribe_template('surfboard') ?? '' - ] - ]; - } - - public function save(ConfigSave $request) - { - $data = $request->validated(); - - $templateKeys = [ - 'subscribe_template_singbox' => 'singbox', - 'subscribe_template_clash' => 'clash', - 'subscribe_template_clashmeta' => 'clashmeta', - 'subscribe_template_stash' => 'stash', - 'subscribe_template_surge' => 'surge', - 'subscribe_template_surfboard' => 'surfboard', - ]; - - foreach ($data as $k => $v) { - if (isset($templateKeys[$k])) { - SubscribeTemplate::setContent($templateKeys[$k], $v); - continue; - } - if ($k == 'frontend_theme') { - $themeService = app(ThemeService::class); - $themeService->switch($v); - } - admin_setting([$k => $v]); - } - - return $this->success(true); - } - - /** - * 格式化模板内容 - * - * @param mixed $content 模板内容 - * @param string $format 输出格式 (json|string) - * @return string 格式化后的内容 - */ - private function formatTemplateContent(mixed $content, string $format = 'string'): string - { - return match ($format) { - 'json' => match (true) { - is_array($content) => json_encode( - value: $content, - flags: JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES - ), - - is_string($content) && str($content)->isJson() => rescue( - callback: fn() => json_encode( - value: json_decode($content, associative: true, flags: JSON_THROW_ON_ERROR), - flags: JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES - ), - rescue: $content, - report: false - ), - - default => str($content)->toString() - }, - - default => str($content)->toString() - }; - } - - private function getTelegramWebhookBaseUrl(): ?string - { - $customUrl = trim((string) admin_setting('telegram_webhook_url', '')); - if ($customUrl !== '') { - return rtrim($customUrl, '/'); - } - - $appUrl = trim((string) admin_setting('app_url', '')); - if ($appUrl !== '') { - return rtrim($appUrl, '/'); - } - - return null; - } - - private function resolveTelegramWebhookUrl(): ?string - { - $baseUrl = $this->getTelegramWebhookBaseUrl(); - if (!$baseUrl) { - return null; - } - - if (str_contains($baseUrl, '/api/v1/guest/telegram/webhook')) { - return $baseUrl; - } - - return $baseUrl . '/api/v1/guest/telegram/webhook'; - } -} diff --git a/Xboard/app/Http/Controllers/V2/Admin/CouponController.php b/Xboard/app/Http/Controllers/V2/Admin/CouponController.php deleted file mode 100644 index 364dff4..0000000 --- a/Xboard/app/Http/Controllers/V2/Admin/CouponController.php +++ /dev/null @@ -1,186 +0,0 @@ -has('filter')) { - collect($request->input('filter'))->each(function ($filter) use ($builder) { - $key = $filter['id']; - $value = $filter['value']; - $builder->where(function ($query) use ($key, $value) { - if (is_array($value)) { - $query->whereIn($key, $value); - } else { - $query->where($key, 'like', "%{$value}%"); - } - }); - }); - } - - if ($request->has('sort')) { - collect($request->input('sort'))->each(function ($sort) use ($builder) { - $key = $sort['id']; - $value = $sort['desc'] ? 'DESC' : 'ASC'; - $builder->orderBy($key, $value); - }); - } - } - public function fetch(Request $request) - { - $current = $request->input('current', 1); - $pageSize = $request->input('pageSize', 10); - $builder = Coupon::query(); - $this->applyFiltersAndSorts($request, $builder); - $coupons = $builder - ->orderBy('created_at', 'desc') - ->paginate($pageSize, ["*"], 'page', $current); - return $this->paginate($coupons); - } - - public function update(Request $request) - { - $params = $request->validate([ - 'id' => 'required|numeric', - 'show' => 'nullable|boolean' - ], [ - 'id.required' => '优惠券ID不能为空', - 'id.numeric' => '优惠券ID必须为数字' - ]); - try { - DB::beginTransaction(); - $coupon = Coupon::find($request->input('id')); - if (!$coupon) { - throw new ApiException(400201, '优惠券不存在'); - } - $coupon->update($params); - DB::commit(); - } catch (\Exception $e) { - \Log::error($e); - return $this->fail([500, '保存失败']); - } - } - - public function show(Request $request) - { - $request->validate([ - 'id' => 'required|numeric' - ], [ - 'id.required' => '优惠券ID不能为空', - 'id.numeric' => '优惠券ID必须为数字' - ]); - $coupon = Coupon::find($request->input('id')); - if (!$coupon) { - return $this->fail([400202, '优惠券不存在']); - } - $coupon->show = !$coupon->show; - if (!$coupon->save()) { - return $this->fail([500, '保存失败']); - } - return $this->success(true); - } - - public function generate(CouponGenerate $request) - { - if ($request->input('generate_count')) { - $this->multiGenerate($request); - return; - } - - $params = $request->validated(); - if (!$request->input('id')) { - if (!isset($params['code'])) { - $params['code'] = Helper::randomChar(8); - } - if (!Coupon::create($params)) { - return $this->fail([500, '创建失败']); - } - } else { - try { - Coupon::find($request->input('id'))->update($params); - } catch (\Exception $e) { - \Log::error($e); - return $this->fail([500, '保存失败']); - } - } - - return $this->success(true); - } - - private function multiGenerate(CouponGenerate $request) - { - $coupons = []; - $coupon = $request->validated(); - $coupon['created_at'] = $coupon['updated_at'] = time(); - $coupon['show'] = 1; - unset($coupon['generate_count']); - for ($i = 0; $i < $request->input('generate_count'); $i++) { - $coupon['code'] = Helper::randomChar(8); - array_push($coupons, $coupon); - } - try { - DB::beginTransaction(); - if ( - !Coupon::insert(array_map(function ($item) use ($coupon) { - // format data - if (isset($item['limit_plan_ids']) && is_array($item['limit_plan_ids'])) { - $item['limit_plan_ids'] = json_encode($coupon['limit_plan_ids']); - } - if (isset($item['limit_period']) && is_array($item['limit_period'])) { - $item['limit_period'] = json_encode($coupon['limit_period']); - } - return $item; - }, $coupons)) - ) { - throw new \Exception(); - } - DB::commit(); - } catch (\Exception $e) { - DB::rollBack(); - return $this->fail([500, '生成失败']); - } - - $data = "名称,类型,金额或比例,开始时间,结束时间,可用次数,可用于订阅,券码,生成时间\r\n"; - foreach ($coupons as $coupon) { - $type = ['', '金额', '比例'][$coupon['type']]; - $value = ['', ($coupon['value'] / 100), $coupon['value']][$coupon['type']]; - $startTime = date('Y-m-d H:i:s', $coupon['started_at']); - $endTime = date('Y-m-d H:i:s', $coupon['ended_at']); - $limitUse = $coupon['limit_use'] ?? '不限制'; - $createTime = date('Y-m-d H:i:s', $coupon['created_at']); - $limitPlanIds = isset($coupon['limit_plan_ids']) ? implode("/", $coupon['limit_plan_ids']) : '不限制'; - $data .= "{$coupon['name']},{$type},{$value},{$startTime},{$endTime},{$limitUse},{$limitPlanIds},{$coupon['code']},{$createTime}\r\n"; - } - echo $data; - } - - public function drop(Request $request) - { - $request->validate([ - 'id' => 'required|numeric' - ], [ - 'id.required' => '优惠券ID不能为空', - 'id.numeric' => '优惠券ID必须为数字' - ]); - $coupon = Coupon::find($request->input('id')); - if (!$coupon) { - return $this->fail([400202, '优惠券不存在']); - } - if (!$coupon->delete()) { - return $this->fail([500, '删除失败']); - } - - return $this->success(true); - } -} diff --git a/Xboard/app/Http/Controllers/V2/Admin/GiftCardController.php b/Xboard/app/Http/Controllers/V2/Admin/GiftCardController.php deleted file mode 100644 index 2c8ecf5..0000000 --- a/Xboard/app/Http/Controllers/V2/Admin/GiftCardController.php +++ /dev/null @@ -1,622 +0,0 @@ -validate([ - 'type' => 'integer|min:1|max:10', - 'status' => 'integer|in:0,1', - 'page' => 'integer|min:1', - 'per_page' => 'integer|min:1|max:1000', - ]); - - $query = GiftCardTemplate::query(); - - if ($request->has('type')) { - $query->where('type', $request->input('type')); - } - - if ($request->has('status')) { - $query->where('status', $request->input('status')); - } - - $perPage = $request->input('per_page', 15); - $templates = $query->orderBy('sort', 'asc') - ->orderBy('created_at', 'desc') - ->paginate($perPage); - - $data = $templates->getCollection()->map(function ($template) { - return [ - 'id' => $template->id, - 'name' => $template->name, - 'description' => $template->description, - 'type' => $template->type, - 'type_name' => $template->type_name, - 'status' => $template->status, - 'conditions' => $template->conditions, - 'rewards' => $template->rewards, - 'limits' => $template->limits, - 'special_config' => $template->special_config, - 'icon' => $template->icon, - 'background_image' => $template->background_image, - 'theme_color' => $template->theme_color, - 'sort' => $template->sort, - 'admin_id' => $template->admin_id, - 'created_at' => $template->created_at, - 'updated_at' => $template->updated_at, - // 统计信息 - 'codes_count' => $template->codes()->count(), - 'used_count' => $template->usages()->count(), - ]; - })->values(); - - return $this->paginate( $templates); - } - - /** - * 创建礼品卡模板 - */ - public function createTemplate(Request $request) - { - $request->validate([ - 'name' => 'required|string|max:255', - 'description' => 'nullable|string', - 'type' => [ - 'required', - 'integer', - Rule::in(array_keys(GiftCardTemplate::getTypeMap())) - ], - 'status' => 'boolean', - 'conditions' => 'nullable|array', - 'rewards' => 'required|array', - 'limits' => 'nullable|array', - 'special_config' => 'nullable|array', - 'icon' => 'nullable|string|max:255', - 'background_image' => 'nullable|string|url|max:255', - 'theme_color' => 'nullable|string|regex:/^#[0-9A-Fa-f]{6}$/', - 'sort' => 'integer|min:0', - ], [ - 'name.required' => '礼品卡名称不能为空', - 'type.required' => '礼品卡类型不能为空', - 'type.in' => '无效的礼品卡类型', - 'rewards.required' => '奖励配置不能为空', - 'theme_color.regex' => '主题色格式不正确', - 'background_image.url' => '背景图片必须是有效的URL', - ]); - - try { - $template = GiftCardTemplate::create([ - 'name' => $request->input('name'), - 'description' => $request->input('description'), - 'type' => $request->input('type'), - 'status' => $request->input('status', true), - 'conditions' => $request->input('conditions'), - 'rewards' => $request->input('rewards'), - 'limits' => $request->input('limits'), - 'special_config' => $request->input('special_config'), - 'icon' => $request->input('icon'), - 'background_image' => $request->input('background_image'), - 'theme_color' => $request->input('theme_color', '#1890ff'), - 'sort' => $request->input('sort', 0), - 'admin_id' => $request->user()->id, - 'created_at' => time(), - 'updated_at' => time(), - ]); - - return $this->success($template); - } catch (\Exception $e) { - Log::error('创建礼品卡模板失败', [ - 'admin_id' => $request->user()->id, - 'data' => $request->all(), - 'error' => $e->getMessage(), - ]); - return $this->fail([500, '创建失败']); - } - } - - /** - * 更新礼品卡模板 - */ - public function updateTemplate(Request $request) - { - $validatedData = $request->validate([ - 'id' => 'required|integer|exists:v2_gift_card_template,id', - 'name' => 'sometimes|required|string|max:255', - 'description' => 'sometimes|nullable|string', - 'type' => [ - 'sometimes', - 'required', - 'integer', - Rule::in(array_keys(GiftCardTemplate::getTypeMap())) - ], - 'status' => 'sometimes|boolean', - 'conditions' => 'sometimes|nullable|array', - 'rewards' => 'sometimes|required|array', - 'limits' => 'sometimes|nullable|array', - 'special_config' => 'sometimes|nullable|array', - 'icon' => 'sometimes|nullable|string|max:255', - 'background_image' => 'sometimes|nullable|string|url|max:255', - 'theme_color' => 'sometimes|nullable|string|regex:/^#[0-9A-Fa-f]{6}$/', - 'sort' => 'sometimes|integer|min:0', - ]); - - $template = GiftCardTemplate::find($validatedData['id']); - if (!$template) { - return $this->fail([404, '模板不存在']); - } - - try { - $updateData = collect($validatedData)->except('id')->all(); - - if (empty($updateData)) { - return $this->success($template); - } - - $updateData['updated_at'] = time(); - - $template->update($updateData); - - return $this->success($template->fresh()); - } catch (\Exception $e) { - Log::error('更新礼品卡模板失败', [ - 'admin_id' => $request->user()->id, - 'template_id' => $template->id, - 'error' => $e->getMessage(), - ]); - return $this->fail([500, '更新失败']); - } - } - - /** - * 删除礼品卡模板 - */ - public function deleteTemplate(Request $request) - { - $request->validate([ - 'id' => 'required|integer|exists:v2_gift_card_template,id', - ]); - - $template = GiftCardTemplate::find($request->input('id')); - if (!$template) { - return $this->fail([404, '模板不存在']); - } - - // 检查是否有关联的兑换码 - if ($template->codes()->exists()) { - return $this->fail([400, '该模板下存在兑换码,无法删除']); - } - - try { - $template->delete(); - return $this->success(true); - } catch (\Exception $e) { - Log::error('删除礼品卡模板失败', [ - 'admin_id' => $request->user()->id, - 'template_id' => $template->id, - 'error' => $e->getMessage(), - ]); - return $this->fail([500, '删除失败']); - } - } - - /** - * 生成兑换码 - */ - public function generateCodes(Request $request) - { - $request->validate([ - 'template_id' => 'required|integer|exists:v2_gift_card_template,id', - 'count' => 'required|integer|min:1|max:10000', - 'prefix' => 'nullable|string|max:10|regex:/^[A-Z0-9]*$/', - 'expires_hours' => 'nullable|integer|min:1', - 'max_usage' => 'integer|min:1|max:1000', - ], [ - 'template_id.required' => '请选择礼品卡模板', - 'count.required' => '请指定生成数量', - 'count.max' => '单次最多生成10000个兑换码', - 'prefix.regex' => '前缀只能包含大写字母和数字', - ]); - - $template = GiftCardTemplate::find($request->input('template_id')); - if (!$template->isAvailable()) { - return $this->fail([400, '模板已被禁用']); - } - - try { - $options = [ - 'prefix' => $request->input('prefix', 'GC'), - 'max_usage' => $request->input('max_usage', 1), - ]; - - if ($request->has('expires_hours')) { - $options['expires_at'] = time() + ($request->input('expires_hours') * 3600); - } - - $batchId = GiftCardCode::batchGenerate( - $request->input('template_id'), - $request->input('count'), - $options - ); - - // 查询本次生成的所有兑换码 - $codes = GiftCardCode::where('batch_id', $batchId)->get(); - - // 判断是否导出 CSV - if ($request->input('download_csv')) { - $headers = [ - 'Content-Type' => 'text/csv', - 'Content-Disposition' => 'attachment; filename="gift_codes.csv"', - ]; - $callback = function () use ($codes, $template) { - $handle = fopen('php://output', 'w'); - // 表头 - fputcsv($handle, [ - '兑换码', - '前缀', - '有效期', - '最大使用次数', - '批次号', - '创建时间', - '模板名称', - '模板类型', - '模板奖励', - '状态', - '使用者', - '使用时间', - '备注' - ]); - foreach ($codes as $code) { - $expireDate = $code->expires_at ? date('Y-m-d H:i:s', $code->expires_at) : '长期有效'; - $createDate = date('Y-m-d H:i:s', $code->created_at); - $templateName = $template->name ?? ''; - $templateType = $template->type ?? ''; - $templateRewards = $template->rewards ? json_encode($template->rewards, JSON_UNESCAPED_UNICODE) : ''; - // 状态判断 - $status = $code->status_name; - $usedBy = $code->user_id ?? ''; - $usedAt = $code->used_at ? date('Y-m-d H:i:s', $code->used_at) : ''; - $remark = $code->remark ?? ''; - fputcsv($handle, [ - $code->code, - $code->prefix ?? '', - $expireDate, - $code->max_usage, - $code->batch_id, - $createDate, - $templateName, - $templateType, - $templateRewards, - $status, - $usedBy, - $usedAt, - $remark, - ]); - } - fclose($handle); - }; - return response()->streamDownload($callback, 'gift_codes.csv', $headers); - } - - Log::info('批量生成兑换码', [ - 'admin_id' => $request->user()->id, - 'template_id' => $request->input('template_id'), - 'count' => $request->input('count'), - 'batch_id' => $batchId, - ]); - - return $this->success([ - 'batch_id' => $batchId, - 'count' => $request->input('count'), - 'message' => '生成成功', - ]); - } catch (\Exception $e) { - Log::error('生成兑换码失败', [ - 'admin_id' => $request->user()->id, - 'data' => $request->all(), - 'error' => $e->getMessage(), - ]); - return $this->fail([500, '生成失败']); - } - } - - /** - * 获取兑换码列表 - */ - public function codes(Request $request) - { - $request->validate([ - 'template_id' => 'integer|exists:v2_gift_card_template,id', - 'batch_id' => 'string', - 'status' => 'integer|in:0,1,2,3', - 'page' => 'integer|min:1', - 'per_page' => 'integer|min:1|max:500', - ]); - - $query = GiftCardCode::with(['template', 'user']); - - if ($request->has('template_id')) { - $query->where('template_id', $request->input('template_id')); - } - - if ($request->has('batch_id')) { - $query->where('batch_id', $request->input('batch_id')); - } - - if ($request->has('status')) { - $query->where('status', $request->input('status')); - } - - $perPage = $request->input('per_page', 15); - $codes = $query->orderBy('created_at', 'desc')->paginate($perPage); - - $data = $codes->getCollection()->map(function ($code) { - return [ - 'id' => $code->id, - 'template_id' => $code->template_id, - 'template_name' => $code->template->name ?? '', - 'code' => $code->code, - 'batch_id' => $code->batch_id, - 'status' => $code->status, - 'status_name' => $code->status_name, - 'user_id' => $code->user_id, - 'user_email' => $code->user ? (substr($code->user->email ?? '', 0, 3) . '***@***') : null, - 'used_at' => $code->used_at, - 'expires_at' => $code->expires_at, - 'usage_count' => $code->usage_count, - 'max_usage' => $code->max_usage, - 'created_at' => $code->created_at, - ]; - })->values(); - - return $this->paginate($codes); - } - - /** - * 禁用/启用兑换码 - */ - public function toggleCode(Request $request) - { - $request->validate([ - 'id' => 'required|integer|exists:v2_gift_card_code,id', - 'action' => 'required|string|in:disable,enable', - ]); - - $code = GiftCardCode::find($request->input('id')); - if (!$code) { - return $this->fail([404, '兑换码不存在']); - } - - try { - if ($request->input('action') === 'disable') { - $code->markAsDisabled(); - } else { - if ($code->status === GiftCardCode::STATUS_DISABLED) { - $code->status = GiftCardCode::STATUS_UNUSED; - $code->save(); - } - } - - return $this->success([ - 'message' => $request->input('action') === 'disable' ? '已禁用' : '已启用', - ]); - } catch (\Exception $e) { - return $this->fail([500, '操作失败']); - } - } - - /** - * 导出兑换码 - */ - public function exportCodes(Request $request) - { - $request->validate([ - 'batch_id' => 'required|string|exists:v2_gift_card_code,batch_id', - ]); - - $codes = GiftCardCode::where('batch_id', $request->input('batch_id')) - ->orderBy('created_at', 'asc') - ->get(['code']); - - $content = $codes->pluck('code')->implode("\n"); - - return response($content) - ->header('Content-Type', 'text/plain') - ->header('Content-Disposition', 'attachment; filename="gift_cards_' . $request->input('batch_id') . '.txt"'); - } - - /** - * 获取使用记录 - */ - public function usages(Request $request) - { - $request->validate([ - 'template_id' => 'integer|exists:v2_gift_card_template,id', - 'user_id' => 'integer|exists:v2_user,id', - 'page' => 'integer|min:1', - 'per_page' => 'integer|min:1|max:500', - ]); - - $query = GiftCardUsage::with(['template', 'code', 'user', 'inviteUser']); - - if ($request->has('template_id')) { - $query->where('template_id', $request->input('template_id')); - } - - if ($request->has('user_id')) { - $query->where('user_id', $request->input('user_id')); - } - - $perPage = $request->input('per_page', 15); - $usages = $query->orderBy('created_at', 'desc')->paginate($perPage); - - $usages->transform(function ($usage) { - return [ - 'id' => $usage->id, - 'code' => $usage->code->code ?? '', - 'template_name' => $usage->template->name ?? '', - 'user_email' => $usage->user->email ?? '', - 'invite_user_email' => $usage->inviteUser ? (substr($usage->inviteUser->email ?? '', 0, 3) . '***@***') : null, - 'rewards_given' => $usage->rewards_given, - 'invite_rewards' => $usage->invite_rewards, - 'multiplier_applied' => $usage->multiplier_applied, - 'created_at' => $usage->created_at, - ]; - })->values(); - return $this->paginate($usages); - } - - /** - * 获取统计数据 - */ - public function statistics(Request $request) - { - $request->validate([ - 'start_date' => 'date_format:Y-m-d', - 'end_date' => 'date_format:Y-m-d', - ]); - - $startDate = $request->input('start_date', date('Y-m-d', strtotime('-30 days'))); - $endDate = $request->input('end_date', date('Y-m-d')); - - // 总体统计 - $totalStats = [ - 'templates_count' => GiftCardTemplate::count(), - 'active_templates_count' => GiftCardTemplate::where('status', 1)->count(), - 'codes_count' => GiftCardCode::count(), - 'used_codes_count' => GiftCardCode::where('status', GiftCardCode::STATUS_USED)->count(), - 'usages_count' => GiftCardUsage::count(), - ]; - - // 每日使用统计 - $driver = DB::connection()->getDriverName(); - $dateExpression = "date(created_at, 'unixepoch')"; // Default for SQLite - if ($driver === 'mysql') { - $dateExpression = 'DATE(FROM_UNIXTIME(created_at))'; - } elseif ($driver === 'pgsql') { - $dateExpression = 'date(to_timestamp(created_at))'; - } - - $dailyUsages = GiftCardUsage::selectRaw("{$dateExpression} as date, COUNT(*) as count") - ->whereRaw("{$dateExpression} BETWEEN ? AND ?", [$startDate, $endDate]) - ->groupBy('date') - ->orderBy('date') - ->get(); - - // 类型统计 - $typeStats = GiftCardUsage::with('template') - ->selectRaw('template_id, COUNT(*) as count') - ->groupBy('template_id') - ->get() - ->map(function ($item) { - return [ - 'template_name' => $item->template->name ?? '', - 'type_name' => $item->template->type_name ?? '', - 'count' => $item->count ?? 0, - ]; - }); - - return $this->success([ - 'total_stats' => $totalStats, - 'daily_usages' => $dailyUsages, - 'type_stats' => $typeStats, - ]); - } - - /** - * 获取所有可用的礼品卡类型 - */ - public function types() - { - return $this->success(GiftCardTemplate::getTypeMap()); - } - - /** - * 更新单个兑换码 - */ - public function updateCode(Request $request) - { - $validatedData = $request->validate([ - 'id' => 'required|integer|exists:v2_gift_card_code,id', - 'expires_at' => 'sometimes|nullable|integer', - 'max_usage' => 'sometimes|integer|min:1|max:1000', - 'status' => 'sometimes|integer|in:0,1,2,3', - ]); - - $code = GiftCardCode::find($validatedData['id']); - if (!$code) { - return $this->fail([404, '礼品卡不存在']); - } - - try { - $updateData = collect($validatedData)->except('id')->all(); - - if (empty($updateData)) { - return $this->success($code); - } - - $updateData['updated_at'] = time(); - $code->update($updateData); - - return $this->success($code->fresh()); - } catch (\Exception $e) { - Log::error('更新礼品卡信息失败', [ - 'admin_id' => $request->user()->id, - 'code_id' => $code->id, - 'error' => $e->getMessage(), - ]); - return $this->fail([500, '更新失败']); - } - } - - /** - * 删除礼品卡 - */ - public function deleteCode(Request $request) - { - $request->validate([ - 'id' => 'required|integer|exists:v2_gift_card_code,id', - ]); - - $code = GiftCardCode::find($request->input('id')); - if (!$code) { - return $this->fail([404, '礼品卡不存在']); - } - - // 检查是否已被使用 - if ($code->status === GiftCardCode::STATUS_USED) { - return $this->fail([400, '该礼品卡已被使用,无法删除']); - } - - try { - // 检查是否有关联的使用记录 - if ($code->usages()->exists()) { - return $this->fail([400, '该礼品卡存在使用记录,无法删除']); - } - - $code->delete(); - return $this->success(['message' => '删除成功']); - } catch (\Exception $e) { - Log::error('删除礼品卡失败', [ - 'admin_id' => $request->user()->id, - 'code_id' => $code->id, - 'error' => $e->getMessage(), - ]); - return $this->fail([500, '删除失败']); - } - } -} diff --git a/Xboard/app/Http/Controllers/V2/Admin/KnowledgeController.php b/Xboard/app/Http/Controllers/V2/Admin/KnowledgeController.php deleted file mode 100644 index d773a8d..0000000 --- a/Xboard/app/Http/Controllers/V2/Admin/KnowledgeController.php +++ /dev/null @@ -1,113 +0,0 @@ -input('id')) { - $knowledge = Knowledge::find($request->input('id'))->toArray(); - if (!$knowledge) - return $this->fail([400202, '知识不存在']); - return $this->success($knowledge); - } - $data = Knowledge::select(['title', 'id', 'updated_at', 'category', 'show']) - ->orderBy('sort', 'ASC') - ->get(); - return $this->success($data); - } - - public function getCategory(Request $request) - { - return $this->success(array_keys(Knowledge::get()->groupBy('category')->toArray())); - } - - public function save(KnowledgeSave $request) - { - $params = $request->validated(); - - if (!$request->input('id')) { - if (!Knowledge::create($params)) { - return $this->fail([500, '创建失败']); - } - } else { - try { - Knowledge::find($request->input('id'))->update($params); - } catch (\Exception $e) { - \Log::error($e); - return $this->fail([500, '创建失败']); - } - } - - return $this->success(true); - } - - public function show(Request $request) - { - $request->validate([ - 'id' => 'required|numeric' - ], [ - 'id.required' => '知识库ID不能为空' - ]); - $knowledge = Knowledge::find($request->input('id')); - if (!$knowledge) { - throw new ApiException('知识不存在'); - } - $knowledge->show = !$knowledge->show; - if (!$knowledge->save()) { - throw new ApiException('保存失败'); - } - - return $this->success(true); - } - - public function sort(Request $request) - { - $request->validate([ - 'ids' => 'required|array' - ], [ - 'ids.required' => '参数有误', - 'ids.array' => '参数有误' - ]); - try { - DB::beginTransaction(); - foreach ($request->input('ids') as $k => $v) { - $knowledge = Knowledge::find($v); - $knowledge->timestamps = false; - $knowledge->update(['sort' => $k + 1]); - } - DB::commit(); - } catch (\Exception $e) { - DB::rollBack(); - throw new ApiException('保存失败'); - } - return $this->success(true); - } - - public function drop(Request $request) - { - $request->validate([ - 'id' => 'required|numeric' - ], [ - 'id.required' => '知识库ID不能为空' - ]); - $knowledge = Knowledge::find($request->input('id')); - if (!$knowledge) { - return $this->fail([400202, '知识不存在']); - } - if (!$knowledge->delete()) { - return $this->fail([500, '删除失败']); - } - - return $this->success(true); - } -} diff --git a/Xboard/app/Http/Controllers/V2/Admin/NoticeController.php b/Xboard/app/Http/Controllers/V2/Admin/NoticeController.php deleted file mode 100644 index 854b313..0000000 --- a/Xboard/app/Http/Controllers/V2/Admin/NoticeController.php +++ /dev/null @@ -1,101 +0,0 @@ -success( - Notice::orderBy('sort', 'ASC') - ->orderBy('id', 'DESC') - ->get() - ); - } - - public function save(NoticeSave $request) - { - $data = $request->only([ - 'title', - 'content', - 'img_url', - 'tags', - 'show', - 'popup' - ]); - if (!$request->input('id')) { - if (!Notice::create($data)) { - return $this->fail([500, '保存失败']); - } - } else { - try { - Notice::find($request->input('id'))->update($data); - } catch (\Exception $e) { - return $this->fail([500, '保存失败']); - } - } - return $this->success(true); - } - - - - public function show(Request $request) - { - if (empty($request->input('id'))) { - return $this->fail([500, '公告ID不能为空']); - } - $notice = Notice::find($request->input('id')); - if (!$notice) { - return $this->fail([400202, '公告不存在']); - } - $notice->show = $notice->show ? 0 : 1; - if (!$notice->save()) { - return $this->fail([500, '保存失败']); - } - - return $this->success(true); - } - - public function drop(Request $request) - { - if (empty($request->input('id'))) { - return $this->fail([422, '公告ID不能为空']); - } - $notice = Notice::find($request->input('id')); - if (!$notice) { - return $this->fail([400202, '公告不存在']); - } - if (!$notice->delete()) { - return $this->fail([500, '删除失败']); - } - return $this->success(true); - } - - public function sort(Request $request) - { - $params = $request->validate([ - 'ids' => 'required|array' - ]); - - try { - DB::beginTransaction(); - foreach ($params['ids'] as $k => $v) { - $notice = Notice::findOrFail($v); - $notice->update(['sort' => $k + 1]); - } - DB::commit(); - return $this->success(true); - } catch (\Exception $e) { - DB::rollBack(); - \Log::error($e); - return $this->fail([500, '排序保存失败']); - } - } -} diff --git a/Xboard/app/Http/Controllers/V2/Admin/OrderController.php b/Xboard/app/Http/Controllers/V2/Admin/OrderController.php deleted file mode 100644 index c06c7c8..0000000 --- a/Xboard/app/Http/Controllers/V2/Admin/OrderController.php +++ /dev/null @@ -1,252 +0,0 @@ -find($request->input('id')); - if (!$order) - return $this->fail([400202, '订单不存在']); - if ($order->surplus_order_ids) { - $order['surplus_orders'] = Order::whereIn('id', $order->surplus_order_ids)->get(); - } - $order['period'] = PlanService::getLegacyPeriod((string) $order->period); - return $this->success($order); - } - - public function fetch(Request $request) - { - $current = $request->input('current', 1); - $pageSize = $request->input('pageSize', 10); - $orderModel = Order::with('plan:id,name'); - - if ($request->boolean('is_commission')) { - $orderModel->whereNotNull('invite_user_id') - ->whereNotIn('status', [0, 2]) - ->where('commission_balance', '>', 0); - } - - $this->applyFiltersAndSorts($request, $orderModel); - - /** @var \Illuminate\Pagination\LengthAwarePaginator $paginatedResults */ - $paginatedResults = $orderModel - ->latest('created_at') - ->paginate( - perPage: $pageSize, - page: $current - ); - - $paginatedResults->getCollection()->transform(function ($order) { - $orderArray = $order->toArray(); - $orderArray['period'] = PlanService::getLegacyPeriod((string) $order->period); - return $orderArray; - }); - - return $this->paginate($paginatedResults); - } - - private function applyFiltersAndSorts(Request $request, Builder $builder): void - { - $this->applyFilters($request, $builder); - $this->applySorting($request, $builder); - } - - private function applyFilters(Request $request, Builder $builder): void - { - if (!$request->has('filter')) { - return; - } - - collect($request->input('filter'))->each(function ($filter) use ($builder) { - $field = $filter['id']; - $value = $filter['value']; - - $builder->where(function ($query) use ($field, $value) { - $this->buildFilterQuery($query, $field, $value); - }); - }); - } - - private function buildFilterQuery(Builder $query, string $field, mixed $value): void - { - // Handle array values for 'in' operations - if (is_array($value)) { - $query->whereIn($field, $value); - return; - } - - // Handle operator-based filtering - if (!is_string($value) || !str_contains($value, ':')) { - $query->where($field, 'like', "%{$value}%"); - return; - } - - [$operator, $filterValue] = explode(':', $value, 2); - - // Convert numeric strings to appropriate type - if (is_numeric($filterValue)) { - $filterValue = strpos($filterValue, '.') !== false - ? (float) $filterValue - : (int) $filterValue; - } - - // Apply operator - $query->where($field, match (strtolower($operator)) { - 'eq' => '=', - 'gt' => '>', - 'gte' => '>=', - 'lt' => '<', - 'lte' => '<=', - 'like' => 'like', - 'notlike' => 'not like', - 'null' => static fn($q) => $q->whereNull($field), - 'notnull' => static fn($q) => $q->whereNotNull($field), - default => 'like' - }, match (strtolower($operator)) { - 'like', 'notlike' => "%{$filterValue}%", - 'null', 'notnull' => null, - default => $filterValue - }); - } - - private function applySorting(Request $request, Builder $builder): void - { - if (!$request->has('sort')) { - return; - } - - collect($request->input('sort'))->each(function ($sort) use ($builder) { - $field = $sort['id']; - $direction = $sort['desc'] ? 'DESC' : 'ASC'; - $builder->orderBy($field, $direction); - }); - } - - public function paid(Request $request) - { - $order = Order::where('trade_no', $request->input('trade_no')) - ->first(); - if (!$order) { - return $this->fail([400202, '订单不存在']); - } - if ($order->status !== 0) - return $this->fail([400, '只能对待支付的订单进行操作']); - - $orderService = new OrderService($order); - if (!$orderService->paid('manual_operation')) { - return $this->fail([500, '更新失败']); - } - return $this->success(true); - } - - public function cancel(Request $request) - { - $order = Order::where('trade_no', $request->input('trade_no')) - ->first(); - if (!$order) { - return $this->fail([400202, '订单不存在']); - } - if ($order->status !== 0) - return $this->fail([400, '只能对待支付的订单进行操作']); - - $orderService = new OrderService($order); - if (!$orderService->cancel()) { - return $this->fail([400, '更新失败']); - } - return $this->success(true); - } - - public function update(OrderUpdate $request) - { - $params = $request->only([ - 'commission_status' - ]); - - $order = Order::where('trade_no', $request->input('trade_no')) - ->first(); - if (!$order) { - return $this->fail([400202, '订单不存在']); - } - - try { - $order->update($params); - } catch (\Exception $e) { - Log::error($e); - return $this->fail([500, '更新失败']); - } - - return $this->success(true); - } - - public function assign(OrderAssign $request) - { - $plan = Plan::find($request->input('plan_id')); - $user = User::byEmail($request->input('email'))->first(); - - if (!$user) { - return $this->fail([400202, '该用户不存在']); - } - - if (!$plan) { - return $this->fail([400202, '该订阅不存在']); - } - - $userService = new UserService(); - if ($userService->isNotCompleteOrderByUserId($user->id)) { - return $this->fail([400, '该用户还有待支付的订单,无法分配']); - } - - try { - DB::beginTransaction(); - $order = new Order(); - $orderService = new OrderService($order); - $order->user_id = $user->id; - $order->plan_id = $plan->id; - $period = $request->input('period'); - $order->period = PlanService::getPeriodKey((string) $period); - $order->trade_no = Helper::guid(); - $order->total_amount = $request->input('total_amount'); - - if (PlanService::getPeriodKey((string) $order->period) === Plan::PERIOD_RESET_TRAFFIC) { - $order->type = Order::TYPE_RESET_TRAFFIC; - } else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id) { - $order->type = Order::TYPE_UPGRADE; - } else if ($user->expired_at > time() && $order->plan_id == $user->plan_id) { - $order->type = Order::TYPE_RENEWAL; - } else { - $order->type = Order::TYPE_NEW_PURCHASE; - } - - $orderService->setInvite($user); - - if (!$order->save()) { - DB::rollBack(); - return $this->fail([500, '订单创建失败']); - } - DB::commit(); - } catch (\Exception $e) { - DB::rollBack(); - throw $e; - } - - return $this->success($order->trade_no); - } -} diff --git a/Xboard/app/Http/Controllers/V2/Admin/PaymentController.php b/Xboard/app/Http/Controllers/V2/Admin/PaymentController.php deleted file mode 100644 index 6649aaa..0000000 --- a/Xboard/app/Http/Controllers/V2/Admin/PaymentController.php +++ /dev/null @@ -1,133 +0,0 @@ -success(array_unique($methods)); - } - - public function fetch() - { - $payments = Payment::orderBy('sort', 'ASC')->get(); - foreach ($payments as $k => $v) { - $notifyUrl = url("/api/v1/guest/payment/notify/{$v->payment}/{$v->uuid}"); - if ($v->notify_domain) { - $parseUrl = parse_url($notifyUrl); - $notifyUrl = $v->notify_domain . $parseUrl['path']; - } - $payments[$k]['notify_url'] = $notifyUrl; - } - return $this->success($payments); - } - - public function getPaymentForm(Request $request) - { - try { - $paymentService = new PaymentService($request->input('payment'), $request->input('id')); - return $this->success(collect($paymentService->form())); - } catch (\Exception $e) { - return $this->fail([400, '支付方式不存在或未启用']); - } - } - - public function show(Request $request) - { - $payment = Payment::find($request->input('id')); - if (!$payment) - return $this->fail([400202, '支付方式不存在']); - $payment->enable = !$payment->enable; - if (!$payment->save()) - return $this->fail([500, '保存失败']); - return $this->success(true); - } - - public function save(Request $request) - { - if (!admin_setting('app_url')) { - return $this->fail([400, '请在站点配置中配置站点地址']); - } - $params = $request->validate([ - 'name' => 'required', - 'icon' => 'nullable', - 'payment' => 'required', - 'config' => 'required', - 'notify_domain' => 'nullable|url', - 'handling_fee_fixed' => 'nullable|integer', - 'handling_fee_percent' => 'nullable|numeric|between:0,100' - ], [ - 'name.required' => '显示名称不能为空', - 'payment.required' => '网关参数不能为空', - 'config.required' => '配置参数不能为空', - 'notify_domain.url' => '自定义通知域名格式有误', - 'handling_fee_fixed.integer' => '固定手续费格式有误', - 'handling_fee_percent.between' => '百分比手续费范围须在0-100之间' - ]); - if ($request->input('id')) { - $payment = Payment::find($request->input('id')); - if (!$payment) - return $this->fail([400202, '支付方式不存在']); - try { - $payment->update($params); - } catch (\Exception $e) { - Log::error($e); - return $this->fail([500, '保存失败']); - } - return $this->success(true); - } - $params['uuid'] = Helper::randomChar(8); - if (!Payment::create($params)) { - return $this->fail([500, '保存失败']); - } - return $this->success(true); - } - - public function drop(Request $request) - { - $payment = Payment::find($request->input('id')); - if (!$payment) - return $this->fail([400202, '支付方式不存在']); - return $this->success($payment->delete()); - } - - - public function sort(Request $request) - { - $request->validate([ - 'ids' => 'required|array' - ], [ - 'ids.required' => '参数有误', - 'ids.array' => '参数有误' - ]); - try { - DB::beginTransaction(); - foreach ($request->input('ids') as $k => $v) { - if (!Payment::find($v)->update(['sort' => $k + 1])) { - throw new \Exception(); - } - } - DB::commit(); - } catch (\Exception $e) { - DB::rollBack(); - return $this->fail([500, '保存失败']); - } - - return $this->success(true); - } -} diff --git a/Xboard/app/Http/Controllers/V2/Admin/PlanController.php b/Xboard/app/Http/Controllers/V2/Admin/PlanController.php deleted file mode 100644 index 6a39f9f..0000000 --- a/Xboard/app/Http/Controllers/V2/Admin/PlanController.php +++ /dev/null @@ -1,132 +0,0 @@ -with([ - 'group:id,name' - ]) - ->withCount([ - 'users', - 'users as active_users_count' => function ($query) { - $query->where(function ($q) { - $q->where('expired_at', '>', time()) - ->orWhereNull('expired_at'); - }); - } - ]) - ->get(); - - return $this->success($plans); - } - - public function save(PlanSave $request) - { - $params = $request->validated(); - - if ($request->input('id')) { - $plan = Plan::find($request->input('id')); - if (!$plan) { - return $this->fail([400202, '该订阅不存在']); - } - - DB::beginTransaction(); - try { - if ($request->input('force_update')) { - User::where('plan_id', $plan->id)->update([ - 'group_id' => $params['group_id'], - 'transfer_enable' => $params['transfer_enable'] * 1073741824, - 'speed_limit' => $params['speed_limit'], - 'device_limit' => $params['device_limit'], - ]); - } - $plan->update($params); - DB::commit(); - return $this->success(true); - } catch (\Exception $e) { - DB::rollBack(); - Log::error($e); - return $this->fail([500, '保存失败']); - } - } - if (!Plan::create($params)) { - return $this->fail([500, '创建失败']); - } - return $this->success(true); - } - - public function drop(Request $request) - { - if (Order::where('plan_id', $request->input('id'))->first()) { - return $this->fail([400201, '该订阅下存在订单无法删除']); - } - if (User::where('plan_id', $request->input('id'))->first()) { - return $this->fail([400201, '该订阅下存在用户无法删除']); - } - - $plan = Plan::find($request->input('id')); - if (!$plan) { - return $this->fail([400202, '该订阅不存在']); - } - - return $this->success($plan->delete()); - } - - public function update(Request $request) - { - $updateData = $request->only([ - 'show', - 'renew', - 'sell' - ]); - - $plan = Plan::find($request->input('id')); - if (!$plan) { - return $this->fail([400202, '该订阅不存在']); - } - - try { - $plan->update($updateData); - } catch (\Exception $e) { - Log::error($e); - return $this->fail([500, '保存失败']); - } - - return $this->success(true); - } - - public function sort(Request $request) - { - $params = $request->validate([ - 'ids' => 'required|array' - ]); - - try { - DB::beginTransaction(); - foreach ($params['ids'] as $k => $v) { - if (!Plan::find($v)->update(['sort' => $k + 1])) { - throw new \Exception(); - } - } - DB::commit(); - } catch (\Exception $e) { - DB::rollBack(); - Log::error($e); - return $this->fail([500, '保存失败']); - } - return $this->success(true); - } -} diff --git a/Xboard/app/Http/Controllers/V2/Admin/PluginController.php b/Xboard/app/Http/Controllers/V2/Admin/PluginController.php deleted file mode 100644 index da41477..0000000 --- a/Xboard/app/Http/Controllers/V2/Admin/PluginController.php +++ /dev/null @@ -1,333 +0,0 @@ -pluginManager = $pluginManager; - $this->configService = $configService; - } - - /** - * 获取所有插件类型 - */ - public function types() - { - return response()->json([ - 'data' => [ - [ - 'value' => Plugin::TYPE_FEATURE, - 'label' => '功能', - 'description' => '提供功能扩展的插件,如Telegram登录、邮件通知等', - 'icon' => '🔧' - ], - [ - 'value' => Plugin::TYPE_PAYMENT, - 'label' => '支付方式', - 'description' => '提供支付接口的插件,如支付宝、微信支付等', - 'icon' => '💳' - ] - ] - ]); - } - - /** - * 获取插件列表 - */ - public function index(Request $request) - { - $type = $request->query('type'); - - $installedPlugins = Plugin::when($type, function ($query) use ($type) { - return $query->byType($type); - }) - ->get() - ->keyBy('code') - ->toArray(); - - $pluginPath = base_path('plugins'); - $plugins = []; - - if (File::exists($pluginPath)) { - $directories = File::directories($pluginPath); - foreach ($directories as $directory) { - $pluginName = basename($directory); - $configFile = $directory . '/config.json'; - if (File::exists($configFile)) { - $config = json_decode(File::get($configFile), true); - $code = $config['code']; - $pluginType = $config['type'] ?? Plugin::TYPE_FEATURE; - - // 如果指定了类型,过滤插件 - if ($type && $pluginType !== $type) { - continue; - } - - $installed = isset($installedPlugins[$code]); - $pluginConfig = $installed ? $this->configService->getConfig($code) : ($config['config'] ?? []); - $readmeFile = collect(['README.md', 'readme.md']) - ->map(fn($f) => $directory . '/' . $f) - ->first(fn($path) => File::exists($path)); - $readmeContent = $readmeFile ? File::get($readmeFile) : ''; - $needUpgrade = false; - if ($installed) { - $installedVersion = $installedPlugins[$code]['version'] ?? null; - $localVersion = $config['version'] ?? null; - if ($installedVersion && $localVersion && version_compare($localVersion, $installedVersion, '>')) { - $needUpgrade = true; - } - } - $plugins[] = [ - 'code' => $config['code'], - 'name' => $config['name'], - 'version' => $config['version'], - 'description' => $config['description'], - 'author' => $config['author'], - 'type' => $pluginType, - 'is_installed' => $installed, - 'is_enabled' => $installed ? $installedPlugins[$code]['is_enabled'] : false, - 'is_protected' => in_array($code, Plugin::PROTECTED_PLUGINS), - 'can_be_deleted' => !in_array($code, Plugin::PROTECTED_PLUGINS), - 'config' => $pluginConfig, - 'readme' => $readmeContent, - 'need_upgrade' => $needUpgrade, - ]; - } - } - } - - return response()->json([ - 'data' => $plugins - ]); - } - - /** - * 安装插件 - */ - public function install(Request $request) - { - $request->validate([ - 'code' => 'required|string' - ]); - - try { - $this->pluginManager->install($request->input('code')); - return response()->json([ - 'message' => '插件安装成功' - ]); - } catch (\Exception $e) { - return response()->json([ - 'message' => '插件安装失败:' . $e->getMessage() - ], 400); - } - } - - /** - * 卸载插件 - */ - public function uninstall(Request $request) - { - $request->validate([ - 'code' => 'required|string' - ]); - - $code = $request->input('code'); - $plugin = Plugin::where('code', $code)->first(); - if ($plugin && $plugin->is_enabled) { - return response()->json([ - 'message' => '请先禁用插件后再卸载' - ], 400); - } - - try { - $this->pluginManager->uninstall($code); - return response()->json([ - 'message' => '插件卸载成功' - ]); - } catch (\Exception $e) { - return response()->json([ - 'message' => '插件卸载失败:' . $e->getMessage() - ], 400); - } - } - - /** - * 升级插件 - */ - public function upgrade(Request $request) - { - $request->validate([ - 'code' => 'required|string', - ]); - try { - $this->pluginManager->update($request->input('code')); - return response()->json([ - 'message' => '插件升级成功' - ]); - } catch (\Exception $e) { - return response()->json([ - 'message' => '插件升级失败:' . $e->getMessage() - ], 400); - } - } - - /** - * 启用插件 - */ - public function enable(Request $request) - { - $request->validate([ - 'code' => 'required|string' - ]); - - try { - $this->pluginManager->enable($request->input('code')); - return response()->json([ - 'message' => '插件启用成功' - ]); - } catch (\Exception $e) { - return response()->json([ - 'message' => '插件启用失败:' . $e->getMessage() - ], 400); - } - } - - /** - * 禁用插件 - */ - public function disable(Request $request) - { - $request->validate([ - 'code' => 'required|string' - ]); - - $this->pluginManager->disable($request->input('code')); - return response()->json([ - 'message' => '插件禁用成功' - ]); - - } - - /** - * 获取插件配置 - */ - public function getConfig(Request $request) - { - $request->validate([ - 'code' => 'required|string' - ]); - - try { - $config = $this->configService->getConfig($request->input('code')); - return response()->json([ - 'data' => $config - ]); - } catch (\Exception $e) { - return response()->json([ - 'message' => '获取配置失败:' . $e->getMessage() - ], 400); - } - } - - /** - * 更新插件配置 - */ - public function updateConfig(Request $request) - { - $request->validate([ - 'code' => 'required|string', - 'config' => 'required|array' - ]); - - try { - $this->configService->updateConfig( - $request->input('code'), - $request->input('config') - ); - - return response()->json([ - 'message' => '配置更新成功' - ]); - } catch (\Exception $e) { - return response()->json([ - 'message' => '配置更新失败:' . $e->getMessage() - ], 400); - } - } - - /** - * 上传插件 - */ - public function upload(Request $request) - { - $request->validate([ - 'file' => [ - 'required', - 'file', - 'mimes:zip', - 'max:10240', // 最大10MB - ] - ], [ - 'file.required' => '请选择插件包文件', - 'file.file' => '无效的文件类型', - 'file.mimes' => '插件包必须是zip格式', - 'file.max' => '插件包大小不能超过10MB' - ]); - - try { - $this->pluginManager->upload($request->file('file')); - return response()->json([ - 'message' => '插件上传成功' - ]); - } catch (\Exception $e) { - return response()->json([ - 'message' => '插件上传失败:' . $e->getMessage() - ], 400); - } - } - - /** - * 删除插件 - */ - public function delete(Request $request) - { - $request->validate([ - 'code' => 'required|string' - ]); - - $code = $request->input('code'); - - // 检查是否为受保护的插件 - if (in_array($code, Plugin::PROTECTED_PLUGINS)) { - return response()->json([ - 'message' => '该插件为系统默认插件,不允许删除' - ], 403); - } - - try { - $this->pluginManager->delete($code); - return response()->json([ - 'message' => '插件删除成功' - ]); - } catch (\Exception $e) { - return response()->json([ - 'message' => '插件删除失败:' . $e->getMessage() - ], 400); - } - } -} \ No newline at end of file diff --git a/Xboard/app/Http/Controllers/V2/Admin/Server/GroupController.php b/Xboard/app/Http/Controllers/V2/Admin/Server/GroupController.php deleted file mode 100644 index 83a53ac..0000000 --- a/Xboard/app/Http/Controllers/V2/Admin/Server/GroupController.php +++ /dev/null @@ -1,66 +0,0 @@ -orderByDesc('id') - ->withCount('users') - ->get(); - - // 只在需要时手动加载server_count - $serverGroups->each(function ($group) { - $group->setAttribute('server_count', $group->server_count); - }); - - return $this->success($serverGroups); - } - - public function save(Request $request) - { - if (empty($request->input('name'))) { - return $this->fail([422, '组名不能为空']); - } - - if ($request->input('id')) { - $serverGroup = ServerGroup::find($request->input('id')); - } else { - $serverGroup = new ServerGroup(); - } - - $serverGroup->name = $request->input('name'); - return $this->success($serverGroup->save()); - } - - public function drop(Request $request) - { - $groupId = $request->input('id'); - - $serverGroup = ServerGroup::find($groupId); - if (!$serverGroup) { - return $this->fail([400202, '组不存在']); - } - if (Server::whereJsonContains('group_ids', $groupId)->exists()) { - return $this->fail([400, '该组已被节点所使用,无法删除']); - } - - if (Plan::where('group_id', $groupId)->exists()) { - return $this->fail([400, '该组已被订阅所使用,无法删除']); - } - if (User::where('group_id', $groupId)->exists()) { - return $this->fail([400, '该组已被用户所使用,无法删除']); - } - return $this->success($serverGroup->delete()); - } -} diff --git a/Xboard/app/Http/Controllers/V2/Admin/Server/ManageController.php b/Xboard/app/Http/Controllers/V2/Admin/Server/ManageController.php deleted file mode 100644 index 41a4bac..0000000 --- a/Xboard/app/Http/Controllers/V2/Admin/Server/ManageController.php +++ /dev/null @@ -1,219 +0,0 @@ -map(function ($item) { - $item['groups'] = ServerGroup::whereIn('id', $item['group_ids'])->get(['name', 'id']); - $item['parent'] = $item->parent; - return $item; - }); - return $this->success($servers); - } - - public function sort(Request $request) - { - ini_set('post_max_size', '1m'); - $params = $request->validate([ - '*.id' => 'numeric', - '*.order' => 'numeric' - ]); - - try { - DB::beginTransaction(); - collect($params)->each(function ($item) { - if (isset($item['id']) && isset($item['order'])) { - Server::where('id', $item['id'])->update(['sort' => $item['order']]); - } - }); - DB::commit(); - } catch (\Exception $e) { - DB::rollBack(); - Log::error($e); - return $this->fail([500, '保存失败']); - - } - return $this->success(true); - } - - public function save(ServerSave $request) - { - $params = $request->validated(); - if ($request->input('id')) { - $server = Server::find($request->input('id')); - if (!$server) { - return $this->fail([400202, '服务器不存在']); - } - try { - $server->update($params); - return $this->success(true); - } catch (\Exception $e) { - Log::error($e); - return $this->fail([500, '保存失败']); - } - } - - try { - Server::create($params); - return $this->success(true); - } catch (\Exception $e) { - Log::error($e); - return $this->fail([500, '创建失败']); - } - - - } - - public function update(Request $request) - { - $request->validate([ - 'id' => 'required|integer', - 'show' => 'integer', - ]); - - $server = Server::find($request->id); - if (!$server) { - return $this->fail([400202, '服务器不存在']); - } - $server->show = (int) $request->show; - if (!$server->save()) { - return $this->fail([500, '保存失败']); - } - return $this->success(true); - } - - /** - * 删除 - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse - */ - public function drop(Request $request) - { - $request->validate([ - 'id' => 'required|integer', - ]); - if (Server::where('id', $request->id)->delete() === false) { - return $this->fail([500, '删除失败']); - } - return $this->success(true); - } - - /** - * 批量删除节点 - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse - */ - public function batchDelete(Request $request) - { - $request->validate([ - 'ids' => 'required|array', - 'ids.*' => 'integer', - ]); - - $ids = $request->input('ids'); - if (empty($ids)) { - return $this->fail([400, '请选择要删除的节点']); - } - - try { - $deleted = Server::whereIn('id', $ids)->delete(); - if ($deleted === false) { - return $this->fail([500, '批量删除失败']); - } - return $this->success(true); - } catch (\Exception $e) { - Log::error($e); - return $this->fail([500, '批量删除失败']); - } - } - - /** - * 重置节点流量 - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse - */ - public function resetTraffic(Request $request) - { - $request->validate([ - 'id' => 'required|integer', - ]); - - $server = Server::find($request->id); - if (!$server) { - return $this->fail([400202, '服务器不存在']); - } - - try { - $server->u = 0; - $server->d = 0; - $server->save(); - - Log::info("Server {$server->id} ({$server->name}) traffic reset by admin"); - return $this->success(true); - } catch (\Exception $e) { - Log::error($e); - return $this->fail([500, '重置失败']); - } - } - - /** - * 批量重置节点流量 - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse - */ - public function batchResetTraffic(Request $request) - { - $request->validate([ - 'ids' => 'required|array', - 'ids.*' => 'integer', - ]); - - $ids = $request->input('ids'); - if (empty($ids)) { - return $this->fail([400, '请选择要重置的节点']); - } - - try { - Server::whereIn('id', $ids)->update([ - 'u' => 0, - 'd' => 0, - ]); - - Log::info("Servers " . implode(',', $ids) . " traffic reset by admin"); - return $this->success(true); - } catch (\Exception $e) { - Log::error($e); - return $this->fail([500, '批量重置失败']); - } - } - - /** - * 复制节点 - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse - */ - public function copy(Request $request) - { - $server = Server::find($request->input('id')); - if (!$server) { - return $this->fail([400202, '服务器不存在']); - } - $server->show = 0; - $server->code = null; - Server::create($server->toArray()); - return $this->success(true); - } -} diff --git a/Xboard/app/Http/Controllers/V2/Admin/Server/RouteController.php b/Xboard/app/Http/Controllers/V2/Admin/Server/RouteController.php deleted file mode 100644 index 7f155ec..0000000 --- a/Xboard/app/Http/Controllers/V2/Admin/Server/RouteController.php +++ /dev/null @@ -1,64 +0,0 @@ - $routes - ]; - } - - public function save(Request $request) - { - $params = $request->validate([ - 'remarks' => 'required', - 'match' => 'required|array', - 'action' => 'required|in:block,direct,dns,proxy', - 'action_value' => 'nullable' - ], [ - 'remarks.required' => '备注不能为空', - 'match.required' => '匹配值不能为空', - 'action.required' => '动作类型不能为空', - 'action.in' => '动作类型参数有误' - ]); - $params['match'] = array_filter($params['match']); - // TODO: remove on 1.8.0 - if ($request->input('id')) { - try { - $route = ServerRoute::find($request->input('id')); - $route->update($params); - return $this->success(true); - } catch (\Exception $e) { - Log::error($e); - return $this->fail([500,'保存失败']); - } - } - try{ - ServerRoute::create($params); - return $this->success(true); - }catch(\Exception $e){ - Log::error($e); - return $this->fail([500,'创建失败']); - } - } - - public function drop(Request $request) - { - $route = ServerRoute::find($request->input('id')); - if (!$route) throw new ApiException('路由不存在'); - if (!$route->delete()) throw new ApiException('删除失败'); - return [ - 'data' => true - ]; - } -} diff --git a/Xboard/app/Http/Controllers/V2/Admin/StatController.php b/Xboard/app/Http/Controllers/V2/Admin/StatController.php deleted file mode 100644 index 805fa94..0000000 --- a/Xboard/app/Http/Controllers/V2/Admin/StatController.php +++ /dev/null @@ -1,508 +0,0 @@ -service = $service; - } - public function getOverride(Request $request) - { - // 获取在线节点数 - $onlineNodes = Server::all()->filter(function ($server) { - return !!$server->is_online; - })->count(); - // 获取在线设备数和在线用户数 - $onlineDevices = User::where('t', '>=', time() - 600) - ->sum('online_count'); - $onlineUsers = User::where('t', '>=', time() - 600) - ->count(); - - // 获取今日流量统计 - $todayStart = strtotime('today'); - $todayTraffic = StatServer::where('record_at', '>=', $todayStart) - ->where('record_at', '<', time()) - ->selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total') - ->first(); - - // 获取本月流量统计 - $monthStart = strtotime(date('Y-m-1')); - $monthTraffic = StatServer::where('record_at', '>=', $monthStart) - ->where('record_at', '<', time()) - ->selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total') - ->first(); - - // 获取总流量统计 - $totalTraffic = StatServer::selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total') - ->first(); - - return [ - 'data' => [ - 'month_income' => Order::where('created_at', '>=', strtotime(date('Y-m-1'))) - ->where('created_at', '<', time()) - ->whereNotIn('status', [0, 2]) - ->sum('total_amount'), - 'month_register_total' => User::where('created_at', '>=', strtotime(date('Y-m-1'))) - ->where('created_at', '<', time()) - ->count(), - 'ticket_pending_total' => Ticket::where('status', 0) - ->count(), - 'commission_pending_total' => Order::where('commission_status', 0) - ->where('invite_user_id', '!=', NULL) - ->whereNotIn('status', [0, 2]) - ->where('commission_balance', '>', 0) - ->count(), - 'day_income' => Order::where('created_at', '>=', strtotime(date('Y-m-d'))) - ->where('created_at', '<', time()) - ->whereNotIn('status', [0, 2]) - ->sum('total_amount'), - 'last_month_income' => Order::where('created_at', '>=', strtotime('-1 month', strtotime(date('Y-m-1')))) - ->where('created_at', '<', strtotime(date('Y-m-1'))) - ->whereNotIn('status', [0, 2]) - ->sum('total_amount'), - 'commission_month_payout' => CommissionLog::where('created_at', '>=', strtotime(date('Y-m-1'))) - ->where('created_at', '<', time()) - ->sum('get_amount'), - 'commission_last_month_payout' => CommissionLog::where('created_at', '>=', strtotime('-1 month', strtotime(date('Y-m-1')))) - ->where('created_at', '<', strtotime(date('Y-m-1'))) - ->sum('get_amount'), - // 新增统计数据 - 'online_nodes' => $onlineNodes, - 'online_devices' => $onlineDevices, - 'online_users' => $onlineUsers, - 'today_traffic' => [ - 'upload' => $todayTraffic->upload ?? 0, - 'download' => $todayTraffic->download ?? 0, - 'total' => $todayTraffic->total ?? 0 - ], - 'month_traffic' => [ - 'upload' => $monthTraffic->upload ?? 0, - 'download' => $monthTraffic->download ?? 0, - 'total' => $monthTraffic->total ?? 0 - ], - 'total_traffic' => [ - 'upload' => $totalTraffic->upload ?? 0, - 'download' => $totalTraffic->download ?? 0, - 'total' => $totalTraffic->total ?? 0 - ] - ] - ]; - } - - /** - * Get order statistics with filtering and pagination - * - * @param Request $request - * @return array - */ - public function getOrder(Request $request) - { - $request->validate([ - 'start_date' => 'nullable|date_format:Y-m-d', - 'end_date' => 'nullable|date_format:Y-m-d', - 'type' => 'nullable|in:paid_total,paid_count,commission_total,commission_count', - ]); - - $query = Stat::where('record_type', 'd'); - - // Apply date filters - if ($request->input('start_date')) { - $query->where('record_at', '>=', strtotime($request->input('start_date'))); - } - if ($request->input('end_date')) { - $query->where('record_at', '<=', strtotime($request->input('end_date') . ' 23:59:59')); - } - - $statistics = $query->orderBy('record_at', 'DESC') - ->get(); - - $summary = [ - 'paid_total' => 0, - 'paid_count' => 0, - 'commission_total' => 0, - 'commission_count' => 0, - 'start_date' => $request->input('start_date', date('Y-m-d', $statistics->last()?->record_at)), - 'end_date' => $request->input('end_date', date('Y-m-d', $statistics->first()?->record_at)), - 'avg_paid_amount' => 0, - 'avg_commission_amount' => 0 - ]; - - $dailyStats = []; - foreach ($statistics as $statistic) { - $date = date('Y-m-d', $statistic['record_at']); - - // Update summary - $summary['paid_total'] += $statistic['paid_total']; - $summary['paid_count'] += $statistic['paid_count']; - $summary['commission_total'] += $statistic['commission_total']; - $summary['commission_count'] += $statistic['commission_count']; - - // Calculate daily stats - $dailyData = [ - 'date' => $date, - 'paid_total' => $statistic['paid_total'], - 'paid_count' => $statistic['paid_count'], - 'commission_total' => $statistic['commission_total'], - 'commission_count' => $statistic['commission_count'], - 'avg_order_amount' => $statistic['paid_count'] > 0 ? round($statistic['paid_total'] / $statistic['paid_count'], 2) : 0, - 'avg_commission_amount' => $statistic['commission_count'] > 0 ? round($statistic['commission_total'] / $statistic['commission_count'], 2) : 0 - ]; - - if ($request->input('type')) { - $dailyStats[] = [ - 'date' => $date, - 'value' => $statistic[$request->input('type')], - 'type' => $this->getTypeLabel($request->input('type')) - ]; - } else { - $dailyStats[] = $dailyData; - } - } - - // Calculate averages for summary - if ($summary['paid_count'] > 0) { - $summary['avg_paid_amount'] = round($summary['paid_total'] / $summary['paid_count'], 2); - } - if ($summary['commission_count'] > 0) { - $summary['avg_commission_amount'] = round($summary['commission_total'] / $summary['commission_count'], 2); - } - - // Add percentage calculations to summary - $summary['commission_rate'] = $summary['paid_total'] > 0 - ? round(($summary['commission_total'] / $summary['paid_total']) * 100, 2) - : 0; - - return [ - 'code' => 0, - 'message' => 'success', - 'data' => [ - 'list' => array_reverse($dailyStats), - 'summary' => $summary, - ] - ]; - } - - /** - * Get human readable label for statistic type - * - * @param string $type - * @return string - */ - private function getTypeLabel(string $type): string - { - return match ($type) { - 'paid_total' => '收款金额', - 'paid_count' => '收款笔数', - 'commission_total' => '佣金金额(已发放)', - 'commission_count' => '佣金笔数(已发放)', - default => $type - }; - } - - // 获取当日实时流量排行 - public function getServerLastRank() - { - $data = $this->service->getServerRank(); - return $this->success(data: $data); - } - // 获取昨日节点流量排行 - public function getServerYesterdayRank() - { - $data = $this->service->getServerRank('yesterday'); - return $this->success($data); - } - - public function getStatUser(Request $request) - { - $request->validate([ - 'user_id' => 'required|integer' - ]); - - $pageSize = $request->input('pageSize', 10); - $records = StatUser::orderBy('record_at', 'DESC') - ->where('user_id', $request->input('user_id')) - ->paginate($pageSize); - - $data = $records->items(); - return [ - 'data' => $data, - 'total' => $records->total(), - ]; - } - - public function getStatRecord(Request $request) - { - return [ - 'data' => $this->service->getStatRecord($request->input('type')) - ]; - } - - /** - * Get comprehensive statistics data including income, users, and growth rates - */ - public function getStats() - { - $currentMonthStart = strtotime(date('Y-m-01')); - $lastMonthStart = strtotime('-1 month', $currentMonthStart); - $twoMonthsAgoStart = strtotime('-2 month', $currentMonthStart); - - // Today's start timestamp - $todayStart = strtotime('today'); - $yesterdayStart = strtotime('-1 day', $todayStart); - - // 获取在线节点数 - $onlineNodes = Server::all()->filter(function ($server) { - return !!$server->is_online; - })->count(); - - // 获取在线设备数和在线用户数 - $onlineDevices = User::where('t', '>=', time() - 600) - ->sum('online_count'); - $onlineUsers = User::where('t', '>=', time() - 600) - ->count(); - - // 获取今日流量统计 - $todayTraffic = StatServer::where('record_at', '>=', $todayStart) - ->where('record_at', '<', time()) - ->selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total') - ->first(); - - // 获取本月流量统计 - $monthTraffic = StatServer::where('record_at', '>=', $currentMonthStart) - ->where('record_at', '<', time()) - ->selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total') - ->first(); - - // 获取总流量统计 - $totalTraffic = StatServer::selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total') - ->first(); - - // Today's income - $todayIncome = Order::where('created_at', '>=', $todayStart) - ->where('created_at', '<', time()) - ->whereNotIn('status', [0, 2]) - ->sum('total_amount'); - - // Yesterday's income for day growth calculation - $yesterdayIncome = Order::where('created_at', '>=', $yesterdayStart) - ->where('created_at', '<', $todayStart) - ->whereNotIn('status', [0, 2]) - ->sum('total_amount'); - - // Current month income - $currentMonthIncome = Order::where('created_at', '>=', $currentMonthStart) - ->where('created_at', '<', time()) - ->whereNotIn('status', [0, 2]) - ->sum('total_amount'); - - // Last month income - $lastMonthIncome = Order::where('created_at', '>=', $lastMonthStart) - ->where('created_at', '<', $currentMonthStart) - ->whereNotIn('status', [0, 2]) - ->sum('total_amount'); - - // Last month commission payout - $lastMonthCommissionPayout = CommissionLog::where('created_at', '>=', $lastMonthStart) - ->where('created_at', '<', $currentMonthStart) - ->sum('get_amount'); - - // Current month commission payout - $currentMonthCommissionPayout = CommissionLog::where('created_at', '>=', $currentMonthStart) - ->where('created_at', '<', time()) - ->sum('get_amount'); - - // Current month new users - $currentMonthNewUsers = User::where('created_at', '>=', $currentMonthStart) - ->where('created_at', '<', time()) - ->count(); - - // Total users - $totalUsers = User::count(); - - // Active users (users with valid subscription) - $activeUsers = User::where(function ($query) { - $query->where('expired_at', '>=', time()) - ->orWhere('expired_at', NULL); - })->count(); - - // Previous month income for growth calculation - $twoMonthsAgoIncome = Order::where('created_at', '>=', $twoMonthsAgoStart) - ->where('created_at', '<', $lastMonthStart) - ->whereNotIn('status', [0, 2]) - ->sum('total_amount'); - - // Previous month commission for growth calculation - $twoMonthsAgoCommission = CommissionLog::where('created_at', '>=', $twoMonthsAgoStart) - ->where('created_at', '<', $lastMonthStart) - ->sum('get_amount'); - - // Previous month users for growth calculation - $lastMonthNewUsers = User::where('created_at', '>=', $lastMonthStart) - ->where('created_at', '<', $currentMonthStart) - ->count(); - - // Calculate growth rates - $monthIncomeGrowth = $lastMonthIncome > 0 ? round(($currentMonthIncome - $lastMonthIncome) / $lastMonthIncome * 100, 1) : 0; - $lastMonthIncomeGrowth = $twoMonthsAgoIncome > 0 ? round(($lastMonthIncome - $twoMonthsAgoIncome) / $twoMonthsAgoIncome * 100, 1) : 0; - $commissionGrowth = $twoMonthsAgoCommission > 0 ? round(($lastMonthCommissionPayout - $twoMonthsAgoCommission) / $twoMonthsAgoCommission * 100, 1) : 0; - $userGrowth = $lastMonthNewUsers > 0 ? round(($currentMonthNewUsers - $lastMonthNewUsers) / $lastMonthNewUsers * 100, 1) : 0; - $dayIncomeGrowth = $yesterdayIncome > 0 ? round(($todayIncome - $yesterdayIncome) / $yesterdayIncome * 100, 1) : 0; - - // 获取待处理工单和佣金数据 - $ticketPendingTotal = Ticket::where('status', 0)->count(); - $commissionPendingTotal = Order::where('commission_status', 0) - ->where('invite_user_id', '!=', NULL) - ->whereIn('status', [Order::STATUS_COMPLETED]) - ->where('commission_balance', '>', 0) - ->count(); - - return [ - 'data' => [ - // 收入相关 - 'todayIncome' => $todayIncome, - 'dayIncomeGrowth' => $dayIncomeGrowth, - 'currentMonthIncome' => $currentMonthIncome, - 'lastMonthIncome' => $lastMonthIncome, - 'monthIncomeGrowth' => $monthIncomeGrowth, - 'lastMonthIncomeGrowth' => $lastMonthIncomeGrowth, - - // 佣金相关 - 'currentMonthCommissionPayout' => $currentMonthCommissionPayout, - 'lastMonthCommissionPayout' => $lastMonthCommissionPayout, - 'commissionGrowth' => $commissionGrowth, - 'commissionPendingTotal' => $commissionPendingTotal, - - // 用户相关 - 'currentMonthNewUsers' => $currentMonthNewUsers, - 'totalUsers' => $totalUsers, - 'activeUsers' => $activeUsers, - 'userGrowth' => $userGrowth, - 'onlineUsers' => $onlineUsers, - 'onlineDevices' => $onlineDevices, - - // 工单相关 - 'ticketPendingTotal' => $ticketPendingTotal, - - // 节点相关 - 'onlineNodes' => $onlineNodes, - - // 流量统计 - 'todayTraffic' => [ - 'upload' => $todayTraffic->upload ?? 0, - 'download' => $todayTraffic->download ?? 0, - 'total' => $todayTraffic->total ?? 0 - ], - 'monthTraffic' => [ - 'upload' => $monthTraffic->upload ?? 0, - 'download' => $monthTraffic->download ?? 0, - 'total' => $monthTraffic->total ?? 0 - ], - 'totalTraffic' => [ - 'upload' => $totalTraffic->upload ?? 0, - 'download' => $totalTraffic->download ?? 0, - 'total' => $totalTraffic->total ?? 0 - ] - ] - ]; - } - - /** - * Get traffic ranking data for nodes or users - * - * @param Request $request - * @return array - */ - public function getTrafficRank(Request $request) - { - $request->validate([ - 'type' => 'required|in:node,user', - 'start_time' => 'nullable|integer|min:1000000000|max:9999999999', - 'end_time' => 'nullable|integer|min:1000000000|max:9999999999' - ]); - - $type = $request->input('type'); - $startDate = $request->input('start_time', strtotime('-7 days')); - $endDate = $request->input('end_time', time()); - $previousStartDate = $startDate - ($endDate - $startDate); - $previousEndDate = $startDate; - - if ($type === 'node') { - // Get node traffic data - $currentData = StatServer::selectRaw('server_id as id, SUM(u + d) as value') - ->where('record_at', '>=', $startDate) - ->where('record_at', '<=', $endDate) - ->groupBy('server_id') - ->orderBy('value', 'DESC') - ->limit(10) - ->get(); - - // Get previous period data for comparison - $previousData = StatServer::selectRaw('server_id as id, SUM(u + d) as value') - ->where('record_at', '>=', $previousStartDate) - ->where('record_at', '<', $previousEndDate) - ->whereIn('server_id', $currentData->pluck('id')) - ->groupBy('server_id') - ->get() - ->keyBy('id'); - - } else { - // Get user traffic data - $currentData = StatUser::selectRaw('user_id as id, SUM(u + d) as value') - ->where('record_at', '>=', $startDate) - ->where('record_at', '<=', $endDate) - ->groupBy('user_id') - ->orderBy('value', 'DESC') - ->limit(10) - ->get(); - - // Get previous period data for comparison - $previousData = StatUser::selectRaw('user_id as id, SUM(u + d) as value') - ->where('record_at', '>=', $previousStartDate) - ->where('record_at', '<', $previousEndDate) - ->whereIn('user_id', $currentData->pluck('id')) - ->groupBy('user_id') - ->get() - ->keyBy('id'); - } - - $result = []; - $ids = $currentData->pluck('id'); - $names = $type === 'node' - ? Server::whereIn('id', $ids)->pluck('name', 'id') - : User::whereIn('id', $ids)->pluck('email', 'id'); - - foreach ($currentData as $data) { - $previousValue = isset($previousData[$data->id]) ? $previousData[$data->id]->value : 0; - $change = $previousValue > 0 ? round(($data->value - $previousValue) / $previousValue * 100, 1) : 0; - - $result[] = [ - 'id' => (string) $data->id, - 'name' => $names[$data->id] ?? ($type === 'node' ? "Node {$data->id}" : "User {$data->id}"), - 'value' => $data->value, - 'previousValue' => $previousValue, - 'change' => $change, - 'timestamp' => date('c', $endDate) - ]; - } - - return [ - 'timestamp' => date('c'), - 'data' => $result - ]; - } -} diff --git a/Xboard/app/Http/Controllers/V2/Admin/SystemController.php b/Xboard/app/Http/Controllers/V2/Admin/SystemController.php deleted file mode 100644 index 2ba35b5..0000000 --- a/Xboard/app/Http/Controllers/V2/Admin/SystemController.php +++ /dev/null @@ -1,144 +0,0 @@ - $this->getScheduleStatus(), - 'horizon' => $this->getHorizonStatus(), - 'schedule_last_runtime' => Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null)), - ]; - return $this->success($data); - } - - public function getQueueWorkload(WorkloadRepository $workload) - { - return $this->success(collect($workload->get())->sortBy('name')->values()->toArray()); - } - - protected function getScheduleStatus(): bool - { - return (time() - 120) < Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null)); - } - - protected function getHorizonStatus(): bool - { - if (!$masters = app(MasterSupervisorRepository::class)->all()) { - return false; - } - - return collect($masters)->contains(function ($master) { - return $master->status === 'paused'; - }) ? false : true; - } - - public function getQueueStats() - { - $data = [ - 'failedJobs' => app(JobRepository::class)->countRecentlyFailed(), - 'jobsPerMinute' => app(MetricsRepository::class)->jobsProcessedPerMinute(), - 'pausedMasters' => $this->totalPausedMasters(), - 'periods' => [ - 'failedJobs' => config('horizon.trim.recent_failed', config('horizon.trim.failed')), - 'recentJobs' => config('horizon.trim.recent'), - ], - 'processes' => $this->totalProcessCount(), - 'queueWithMaxRuntime' => app(MetricsRepository::class)->queueWithMaximumRuntime(), - 'queueWithMaxThroughput' => app(MetricsRepository::class)->queueWithMaximumThroughput(), - 'recentJobs' => app(JobRepository::class)->countRecent(), - 'status' => $this->getHorizonStatus(), - 'wait' => collect(app(WaitTimeCalculator::class)->calculate())->take(1), - ]; - return $this->success($data); - } - - /** - * Get the total process count across all supervisors. - * - * @return int - */ - protected function totalProcessCount() - { - $supervisors = app(SupervisorRepository::class)->all(); - - return collect($supervisors)->reduce(function ($carry, $supervisor) { - return $carry + collect($supervisor->processes)->sum(); - }, 0); - } - - /** - * Get the number of master supervisors that are currently paused. - * - * @return int - */ - protected function totalPausedMasters() - { - if (!$masters = app(MasterSupervisorRepository::class)->all()) { - return 0; - } - - return collect($masters)->filter(function ($master) { - return $master->status === 'paused'; - })->count(); - } - - public function getAuditLog(Request $request) - { - $current = max(1, (int) $request->input('current', 1)); - $pageSize = max(10, (int) $request->input('page_size', 10)); - - $builder = AdminAuditLog::with('admin:id,email') - ->orderBy('id', 'DESC') - ->when($request->input('action'), fn($q, $v) => $q->where('action', $v)) - ->when($request->input('admin_id'), fn($q, $v) => $q->where('admin_id', $v)) - ->when($request->input('keyword'), function ($q, $keyword) { - $q->where(function ($q) use ($keyword) { - $q->where('uri', 'like', '%' . $keyword . '%') - ->orWhere('request_data', 'like', '%' . $keyword . '%'); - }); - }); - - $total = $builder->count(); - $res = $builder->forPage($current, $pageSize)->get(); - - return response(['data' => $res, 'total' => $total]); - } - - public function getHorizonFailedJobs(Request $request, JobRepository $jobRepository) - { - $current = max(1, (int) $request->input('current', 1)); - $pageSize = max(10, (int) $request->input('page_size', 20)); - $offset = ($current - 1) * $pageSize; - - $failedJobs = collect($jobRepository->getFailed()) - ->sortByDesc('failed_at') - ->slice($offset, $pageSize) - ->values(); - - $total = $jobRepository->countFailed(); - - return response()->json([ - 'data' => $failedJobs, - 'total' => $total, - 'current' => $current, - 'page_size' => $pageSize, - ]); - } - -} diff --git a/Xboard/app/Http/Controllers/V2/Admin/ThemeController.php b/Xboard/app/Http/Controllers/V2/Admin/ThemeController.php deleted file mode 100644 index 727cd27..0000000 --- a/Xboard/app/Http/Controllers/V2/Admin/ThemeController.php +++ /dev/null @@ -1,150 +0,0 @@ -themeService = $themeService; - } - - /** - * 上传新主题 - * - * @throws ApiException - */ - public function upload(Request $request) - { - $request->validate([ - 'file' => [ - 'required', - 'file', - 'mimes:zip', - 'max:10240', // 最大10MB - ] - ], [ - 'file.required' => '请选择主题包文件', - 'file.file' => '无效的文件类型', - 'file.mimes' => '主题包必须是zip格式', - 'file.max' => '主题包大小不能超过10MB' - ]); - - try { - // 检查上传目录权限 - $uploadPath = storage_path('tmp'); - if (!File::exists($uploadPath)) { - File::makeDirectory($uploadPath, 0755, true); - } - - if (!is_writable($uploadPath)) { - throw new ApiException('上传目录无写入权限'); - } - - // 检查主题目录权限 - $themePath = base_path('theme'); - if (!is_writable($themePath)) { - throw new ApiException('主题目录无写入权限'); - } - - $file = $request->file('file'); - - // 检查文件MIME类型 - $mimeType = $file->getMimeType(); - if (!in_array($mimeType, ['application/zip', 'application/x-zip-compressed'])) { - throw new ApiException('无效的文件类型,仅支持ZIP格式'); - } - - // 检查文件名安全性 - $originalName = $file->getClientOriginalName(); - if (!preg_match('/^[a-zA-Z0-9\-\_\.]+\.zip$/', $originalName)) { - throw new ApiException('主题包文件名只能包含字母、数字、下划线、中划线和点'); - } - - $this->themeService->upload($file); - return $this->success(true); - - } catch (ApiException $e) { - throw $e; - } catch (\Exception $e) { - Log::error('Theme upload failed', [ - 'error' => $e->getMessage(), - 'file' => $request->file('file')?->getClientOriginalName() - ]); - throw new ApiException('主题上传失败:' . $e->getMessage()); - } - } - - /** - * 删除主题 - */ - public function delete(Request $request) - { - $payload = $request->validate([ - 'name' => 'required' - ]); - $this->themeService->delete($payload['name']); - return $this->success(true); - } - - /** - * 获取所有主题和其配置列 - * - * @return \Illuminate\Http\JsonResponse - */ - public function getThemes() - { - $data = [ - 'themes' => $this->themeService->getList(), - 'active' => admin_setting('frontend_theme', 'Xboard') - ]; - return $this->success($data); - } - - /** - * 切换主题 - */ - public function switchTheme(Request $request) - { - $payload = $request->validate([ - 'name' => 'required' - ]); - $this->themeService->switch($payload['name']); - return $this->success(true); - } - - /** - * 获取主题配置 - */ - public function getThemeConfig(Request $request) - { - $payload = $request->validate([ - 'name' => 'required' - ]); - $data = $this->themeService->getConfig($payload['name']); - return $this->success($data); - } - - /** - * 保存主题配置 - */ - public function saveThemeConfig(Request $request) - { - $payload = $request->validate([ - 'name' => 'required', - 'config' => 'required' - ]); - $this->themeService->updateConfig($payload['name'], $payload['config']); - $config = $this->themeService->getConfig($payload['name']); - return $this->success($config); - } -} diff --git a/Xboard/app/Http/Controllers/V2/Admin/TicketController.php b/Xboard/app/Http/Controllers/V2/Admin/TicketController.php deleted file mode 100644 index ca6d8c4..0000000 --- a/Xboard/app/Http/Controllers/V2/Admin/TicketController.php +++ /dev/null @@ -1,156 +0,0 @@ -has('filter')) { - collect($request->input('filter'))->each(function ($filter) use ($builder) { - $key = $filter['id']; - $value = $filter['value']; - $builder->where(function ($query) use ($key, $value) { - if (is_array($value)) { - $query->whereIn($key, $value); - } else { - $query->where($key, 'like', "%{$value}%"); - } - }); - }); - } - - if ($request->has('sort')) { - collect($request->input('sort'))->each(function ($sort) use ($builder) { - $key = $sort['id']; - $value = $sort['desc'] ? 'DESC' : 'ASC'; - $builder->orderBy($key, $value); - }); - } - } - public function fetch(Request $request) - { - if ($request->input('id')) { - return $this->fetchTicketById($request); - } else { - return $this->fetchTickets($request); - } - } - - /** - * Summary of fetchTicketById - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse - */ - private function fetchTicketById(Request $request) - { - $ticket = Ticket::with('messages', 'user')->find($request->input('id')); - - if (!$ticket) { - return $this->fail([400202, '工单不存在']); - } - $result = $ticket->toArray(); - $result['user'] = UserController::transformUserData($ticket->user); - - return $this->success($result); - } - - /** - * Summary of fetchTickets - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response - */ - private function fetchTickets(Request $request) - { - $ticketModel = Ticket::with('user') - ->when($request->has('status'), function ($query) use ($request) { - $query->where('status', $request->input('status')); - }) - ->when($request->has('reply_status'), function ($query) use ($request) { - $query->whereIn('reply_status', $request->input('reply_status')); - }) - ->when($request->has('email'), function ($query) use ($request) { - $query->whereHas('user', function ($q) use ($request) { - $q->where('email', $request->input('email')); - }); - }); - - $this->applyFiltersAndSorts($request, $ticketModel); - $tickets = $ticketModel - ->latest('updated_at') - ->paginate( - perPage: $request->integer('pageSize', 10), - page: $request->integer('current', 1) - ); - - // 获取items然后映射转换 - $items = collect($tickets->items())->map(function ($ticket) { - $ticketData = $ticket->toArray(); - $ticketData['user'] = UserController::transformUserData($ticket->user); - return $ticketData; - })->all(); - - return response([ - 'data' => $items, - 'total' => $tickets->total() - ]); - } - - public function reply(Request $request) - { - $request->validate([ - 'id' => 'required|numeric', - 'message' => 'required|string' - ], [ - 'id.required' => '工单ID不能为空', - 'message.required' => '消息不能为空' - ]); - $ticketService = new TicketService(); - $ticketService->replyByAdmin( - $request->input('id'), - $request->input('message'), - $request->user()->id - ); - return $this->success(true); - } - - public function close(Request $request) - { - $request->validate([ - 'id' => 'required|numeric' - ], [ - 'id.required' => '工单ID不能为空' - ]); - try { - $ticket = Ticket::findOrFail($request->input('id')); - $ticket->status = Ticket::STATUS_CLOSED; - $ticket->save(); - return $this->success(true); - } catch (ModelNotFoundException $e) { - return $this->fail([400202, '工单不存在']); - } catch (\Exception $e) { - return $this->fail([500101, '关闭失败']); - } - } - - public function show($ticketId) - { - $ticket = Ticket::with([ - 'user', - 'messages' => function ($query) { - $query->with(['user']); // 如果需要用户信息 - } - ])->findOrFail($ticketId); - - // 自动包含 is_me 属性 - return response()->json([ - 'data' => $ticket - ]); - } -} diff --git a/Xboard/app/Http/Controllers/V2/Admin/TrafficResetController.php b/Xboard/app/Http/Controllers/V2/Admin/TrafficResetController.php deleted file mode 100644 index 53e4f54..0000000 --- a/Xboard/app/Http/Controllers/V2/Admin/TrafficResetController.php +++ /dev/null @@ -1,235 +0,0 @@ -trafficResetService = $trafficResetService; - } - - /** - * 获取流量重置日志列表 - */ - public function logs(Request $request): JsonResponse - { - $request->validate([ - 'user_id' => 'nullable|integer', - 'user_email' => 'nullable|string', - 'reset_type' => 'nullable|string|in:' . implode(',', array_keys(TrafficResetLog::getResetTypeNames())), - 'trigger_source' => 'nullable|string|in:' . implode(',', array_keys(TrafficResetLog::getSourceNames())), - 'start_date' => 'nullable|date', - 'end_date' => 'nullable|date|after_or_equal:start_date', - 'per_page' => 'nullable|integer|min:1|max:10000', - 'page' => 'nullable|integer|min:1', - ]); - - $query = TrafficResetLog::with(['user:id,email']) - ->orderBy('reset_time', 'desc'); - - // 筛选条件 - if ($request->filled('user_id')) { - $query->where('user_id', $request->user_id); - } - - if ($request->filled('user_email')) { - $query->whereHas('user', function ($query) use ($request) { - $query->where('email', 'like', '%' . $request->user_email . '%'); - }); - } - - if ($request->filled('reset_type')) { - $query->where('reset_type', $request->reset_type); - } - - if ($request->filled('trigger_source')) { - $query->where('trigger_source', $request->trigger_source); - } - - if ($request->filled('start_date')) { - $query->where('reset_time', '>=', $request->start_date); - } - - if ($request->filled('end_date')) { - $query->where('reset_time', '<=', $request->end_date . ' 23:59:59'); - } - - $perPage = $request->get('per_page', 20); - $logs = $query->paginate($perPage); - - // 格式化数据 - $formattedLogs = $logs->getCollection()->map(function (TrafficResetLog $log) { - return [ - 'id' => $log->id, - 'user_id' => $log->user_id, - 'user_email' => $log->user->email ?? 'N/A', - 'reset_type' => $log->reset_type, - 'reset_type_name' => $log->getResetTypeName(), - 'reset_time' => $log->reset_time, - 'old_traffic' => [ - 'upload' => $log->old_upload, - 'download' => $log->old_download, - 'total' => $log->old_total, - 'formatted' => $log->formatTraffic($log->old_total), - ], - 'new_traffic' => [ - 'upload' => $log->new_upload, - 'download' => $log->new_download, - 'total' => $log->new_total, - 'formatted' => $log->formatTraffic($log->new_total), - ], - 'trigger_source' => $log->trigger_source, - 'trigger_source_name' => $log->getSourceName(), - 'metadata' => $log->metadata, - 'created_at' => $log->created_at, - ]; - }); - - return response()->json([ - 'data' => $formattedLogs->toArray(), - 'pagination' => [ - 'current_page' => $logs->currentPage(), - 'last_page' => $logs->lastPage(), - 'per_page' => $logs->perPage(), - 'total' => $logs->total(), - ], - ]); - } - - /** - * 获取流量重置统计信息 - */ - public function stats(Request $request): JsonResponse - { - $request->validate([ - 'days' => 'nullable|integer|min:1|max:365', - ]); - - $days = $request->get('days', 30); - $startDate = now()->subDays($days)->startOfDay(); - - $stats = [ - 'total_resets' => TrafficResetLog::where('reset_time', '>=', $startDate)->count(), - 'auto_resets' => TrafficResetLog::where('reset_time', '>=', $startDate) - ->where('trigger_source', TrafficResetLog::SOURCE_AUTO) - ->count(), - 'manual_resets' => TrafficResetLog::where('reset_time', '>=', $startDate) - ->where('trigger_source', TrafficResetLog::SOURCE_MANUAL) - ->count(), - 'cron_resets' => TrafficResetLog::where('reset_time', '>=', $startDate) - ->where('trigger_source', TrafficResetLog::SOURCE_CRON) - ->count(), - ]; - - return response()->json([ - 'data' => $stats - ]); - } - - /** - * 手动重置用户流量 - */ - public function resetUser(Request $request): JsonResponse - { - $request->validate([ - 'user_id' => 'required|integer|exists:v2_user,id', - 'reason' => 'nullable|string|max:255', - ]); - - $user = User::find($request->user_id); - - if (!$this->trafficResetService->canReset($user)) { - return response()->json([ - 'message' => __('traffic_reset.user_cannot_reset') - ], 400); - } - - $metadata = []; - if ($request->filled('reason')) { - $metadata['reason'] = $request->reason; - $metadata['admin_id'] = auth()->user()?->id; - } - - $success = $this->trafficResetService->manualReset($user, $metadata); - - if (!$success) { - return response()->json([ - 'message' => __('traffic_reset.reset_failed') - ], 500); - } - - return response()->json([ - 'message' => __('traffic_reset.reset_success'), - 'data' => [ - 'user_id' => $user->id, - 'email' => $user->email, - 'reset_time' => now(), - 'next_reset_at' => $user->fresh()->next_reset_at, - ] - ]); - } - - - - /** - * 获取用户重置历史 - */ - public function userHistory(Request $request, int $userId): JsonResponse - { - $request->validate([ - 'limit' => 'nullable|integer|min:1|max:50', - ]); - - $user = User::findOrFail($userId); - $limit = $request->get('limit', 10); - - $history = $this->trafficResetService->getUserResetHistory($user, $limit); - - /** @var \Illuminate\Database\Eloquent\Collection $history */ - $data = $history->map(function (TrafficResetLog $log) { - return [ - 'id' => $log->id, - 'reset_type' => $log->reset_type, - 'reset_type_name' => $log->getResetTypeName(), - 'reset_time' => $log->reset_time, - 'old_traffic' => [ - 'upload' => $log->old_upload, - 'download' => $log->old_download, - 'total' => $log->old_total, - 'formatted' => $log->formatTraffic($log->old_total), - ], - 'trigger_source' => $log->trigger_source, - 'trigger_source_name' => $log->getSourceName(), - 'metadata' => $log->metadata, - ]; - }); - - return response()->json([ - "data" => [ - 'user' => [ - 'id' => $user->id, - 'email' => $user->email, - 'reset_count' => $user->reset_count, - 'last_reset_at' => $user->last_reset_at, - 'next_reset_at' => $user->next_reset_at, - ], - 'history' => $data, - ] - ]); - } - - -} \ No newline at end of file diff --git a/Xboard/app/Http/Controllers/V2/Admin/UpdateController.php b/Xboard/app/Http/Controllers/V2/Admin/UpdateController.php deleted file mode 100644 index d846466..0000000 --- a/Xboard/app/Http/Controllers/V2/Admin/UpdateController.php +++ /dev/null @@ -1,28 +0,0 @@ -updateService = $updateService; - } - - public function checkUpdate() - { - return $this->success($this->updateService->checkForUpdates()); - } - - public function executeUpdate() - { - $result = $this->updateService->executeUpdate(); - return $result['success'] ? $this->success($result) : $this->fail([500, $result['message']]); - } -} \ No newline at end of file diff --git a/Xboard/app/Http/Controllers/V2/Admin/UserController.php b/Xboard/app/Http/Controllers/V2/Admin/UserController.php deleted file mode 100644 index b135270..0000000 --- a/Xboard/app/Http/Controllers/V2/Admin/UserController.php +++ /dev/null @@ -1,682 +0,0 @@ -input('id')); - if (!$user) - return $this->fail([400202, '用户不存在']); - $user->token = Helper::guid(); - $user->uuid = Helper::guid(true); - return $this->success($user->save()); - } - - // Apply filters and sorts to the query builder. - private function applyFiltersAndSorts(Request $request, Builder|QueryBuilder $builder): void - { - $this->applyFilters($request, $builder); - $this->applySorting($request, $builder); - } - - // Apply filters to the query builder. - private function applyFilters(Request $request, Builder|QueryBuilder $builder): void - { - if (!$request->has('filter')) { - return; - } - - collect($request->input('filter'))->each(function ($filter) use ($builder) { - $field = $filter['id']; - $value = $filter['value']; - $logic = strtolower($filter['logic'] ?? 'and'); - - if ($logic === 'or') { - $builder->orWhere(function ($query) use ($field, $value) { - $this->buildFilterQuery($query, $field, $value); - }); - } else { - $builder->where(function ($query) use ($field, $value) { - $this->buildFilterQuery($query, $field, $value); - }); - } - }); - } - - // Build one filter query condition. - private function buildFilterQuery(Builder|QueryBuilder $query, string $field, mixed $value): void - { - // 处理关联查询 - if (str_contains($field, '.')) { - if (!method_exists($query, 'whereHas')) { - return; - } - [$relation, $relationField] = explode('.', $field); - $query->whereHas($relation, function ($q) use ($relationField, $value) { - if (is_array($value)) { - $q->whereIn($relationField, $value); - } else if (is_string($value) && str_contains($value, ':')) { - [$operator, $filterValue] = explode(':', $value, 2); - $this->applyQueryCondition($q, $relationField, $operator, $filterValue); - } else { - $q->where($relationField, 'like', "%{$value}%"); - } - }); - return; - } - - // 处理数组值的 'in' 操作 - if (is_array($value)) { - $query->whereIn($field === 'group_ids' ? 'group_id' : $field, $value); - return; - } - - // 处理基于运算符的过滤 - if (!is_string($value) || !str_contains($value, ':')) { - $query->where($field, 'like', "%{$value}%"); - return; - } - - [$operator, $filterValue] = explode(':', $value, 2); - - // 转换数字字符串为适当的类型 - if (is_numeric($filterValue)) { - $filterValue = strpos($filterValue, '.') !== false - ? (float) $filterValue - : (int) $filterValue; - } - - // 处理计算字段 - $queryField = match ($field) { - 'total_used' => DB::raw('(u + d)'), - default => $field - }; - - $this->applyQueryCondition($query, $queryField, $operator, $filterValue); - } - - // Apply sorting rules to the query builder. - private function applySorting(Request $request, Builder|QueryBuilder $builder): void - { - if (!$request->has('sort')) { - return; - } - - collect($request->input('sort'))->each(function ($sort) use ($builder) { - $field = $sort['id']; - $direction = $sort['desc'] ? 'DESC' : 'ASC'; - $builder->orderBy($field, $direction); - }); - } - - // Resolve bulk operation scope and normalize user_ids. - private function resolveScope(Request $request): array - { - $scope = $request->input('scope'); - $userIds = $request->input('user_ids'); - - $hasSelection = is_array($userIds) && count(array_filter($userIds, static fn($v) => is_numeric($v))) > 0; - $hasFilter = $request->has('filter') && !empty($request->input('filter')); - - if (!in_array($scope, ['selected', 'filtered', 'all'], true)) { - if ($hasSelection) { - $scope = 'selected'; - } elseif ($hasFilter) { - $scope = 'filtered'; - } else { - $scope = 'all'; - } - } - - $normalizedIds = []; - if ($scope === 'selected') { - $normalizedIds = is_array($userIds) ? $userIds : []; - $normalizedIds = array_values(array_unique(array_map(static function ($v) { - return is_numeric($v) ? (int) $v : null; - }, $normalizedIds))); - $normalizedIds = array_values(array_filter($normalizedIds, static fn($v) => is_int($v))); - } - - return [ - 'scope' => $scope, - 'user_ids' => $normalizedIds, - ]; - } - - // Fetch paginated user list (filters + sorting). - public function fetch(Request $request) - { - $current = $request->input('current', 1); - $pageSize = $request->input('pageSize', 10); - - $userModel = User::query() - ->with(['plan:id,name', 'invite_user:id,email', 'group:id,name']) - ->select((new User())->getTable() . '.*') - ->selectRaw('(u + d) as total_used'); - - $this->applyFiltersAndSorts($request, $userModel); - - $users = $userModel->orderBy('id', 'desc') - ->paginate($pageSize, ['*'], 'page', $current); - - $users->getCollection()->transform(function ($user): array { - return self::transformUserData($user); - }); - - return $this->paginate($users); - } - - // Transform user fields for API response. - public static function transformUserData(User $user): array - { - $user = $user->toArray(); - $user['balance'] = $user['balance'] / 100; - $user['commission_balance'] = $user['commission_balance'] / 100; - $user['subscribe_url'] = Helper::getSubscribeUrl($user['token']); - return $user; - } - - public function getUserInfoById(Request $request) - { - $request->validate([ - 'id' => 'required|numeric' - ], [ - 'id.required' => '用户ID不能为空' - ]); - $user = User::find($request->input('id'))->load('invite_user'); - return $this->success($user); - } - - public function update(UserUpdate $request) - { - $params = $request->validated(); - - $user = User::find($request->input('id')); - if (!$user) { - return $this->fail([400202, '用户不存在']); - } - if (isset($params['email'])) { - if (User::byEmail($params['email'])->first() && $user->email !== $params['email']) { - return $this->fail([400201, '邮箱已被使用']); - } - } - // 处理密码 - if (isset($params['password'])) { - $params['password'] = password_hash($params['password'], PASSWORD_DEFAULT); - $params['password_algo'] = NULL; - } else { - unset($params['password']); - } - // 处理订阅计划 - if (isset($params['plan_id'])) { - $plan = Plan::find($params['plan_id']); - if (!$plan) { - return $this->fail([400202, '订阅计划不存在']); - } - $params['group_id'] = $plan->group_id; - } - // 处理邀请用户 - if ($request->input('invite_user_email') && $inviteUser = User::byEmail($request->input('invite_user_email'))->first()) { - $params['invite_user_id'] = $inviteUser->id; - } else { - $params['invite_user_id'] = null; - } - - if (isset($params['banned']) && (int) $params['banned'] === 1) { - $authService = new AuthService($user); - $authService->removeAllSessions(); - } - if (isset($params['balance'])) { - $params['balance'] = $params['balance'] * 100; - } - if (isset($params['commission_balance'])) { - $params['commission_balance'] = $params['commission_balance'] * 100; - } - - try { - $user->update($params); - } catch (\Exception $e) { - Log::error($e); - return $this->fail([500, '保存失败']); - } - return $this->success(true); - } - - // Export users to CSV. - public function dumpCSV(Request $request) - { - ini_set('memory_limit', '-1'); - gc_enable(); // 启用垃圾回收 - - $scopeInfo = $this->resolveScope($request); - $scope = $scopeInfo['scope']; - $userIds = $scopeInfo['user_ids']; - - if ($scope === 'selected') { - if (empty($userIds)) { - return $this->fail([422, 'user_ids不能为空']); - } - } - - // 优化查询:使用with预加载plan关系,避免N+1问题 - $query = User::query() - ->with('plan:id,name') - ->orderBy('id', 'asc') - ->select([ - 'email', - 'balance', - 'commission_balance', - 'transfer_enable', - 'u', - 'd', - 'expired_at', - 'token', - 'plan_id' - ]); - - if ($scope === 'selected') { - $query->whereIn('id', $userIds); - } elseif ($scope === 'filtered') { - $this->applyFiltersAndSorts($request, $query); - } // all: ignore filter/sort - - $filename = 'users_' . date('Y-m-d_His') . '.csv'; - - return response()->streamDownload(function () use ($query) { - // 打开输出流 - $output = fopen('php://output', 'w'); - - // 添加BOM标记,确保Excel正确显示中文 - fprintf($output, chr(0xEF) . chr(0xBB) . chr(0xBF)); - - // 写入CSV头部 - fputcsv($output, [ - '邮箱', - '余额', - '推广佣金', - '总流量', - '剩余流量', - '套餐到期时间', - '订阅计划', - '订阅地址' - ]); - - // 分批处理数据以减少内存使用 - $query->chunk(500, function ($users) use ($output) { - foreach ($users as $user) { - try { - $row = [ - $user->email, - number_format($user->balance / 100, 2), - number_format($user->commission_balance / 100, 2), - Helper::trafficConvert($user->transfer_enable), - Helper::trafficConvert($user->transfer_enable - ($user->u + $user->d)), - $user->expired_at ? date('Y-m-d H:i:s', $user->expired_at) : '长期有效', - $user->plan ? $user->plan->name : '无订阅', - Helper::getSubscribeUrl($user->token) - ]; - fputcsv($output, $row); - } catch (\Exception $e) { - Log::error('CSV导出错误: ' . $e->getMessage(), [ - 'user_id' => $user->id, - 'email' => $user->email - ]); - continue; // 继续处理下一条记录 - } - } - - // 清理内存 - gc_collect_cycles(); - }); - - fclose($output); - }, $filename, [ - 'Content-Type' => 'text/csv; charset=UTF-8', - 'Content-Disposition' => 'attachment; filename="' . $filename . '"' - ]); - } - - public function generate(UserGenerate $request) - { - if ($request->input('email_prefix')) { - // If generate_count is specified with email_prefix, generate multiple users with incremented emails - if ($request->input('generate_count')) { - return $this->multiGenerateWithPrefix($request); - } - - // Single user generation with email_prefix - $email = $request->input('email_prefix') . '@' . $request->input('email_suffix'); - - if (User::byEmail($email)->exists()) { - return $this->fail([400201, '邮箱已存在于系统中']); - } - - $userService = app(UserService::class); - $user = $userService->createUser([ - 'email' => $email, - 'password' => $request->input('password') ?? $email, - 'plan_id' => $request->input('plan_id'), - 'expired_at' => $request->input('expired_at'), - ]); - - if (!$user->save()) { - return $this->fail([500, '生成失败']); - } - return $this->success(true); - } - - if ($request->input('generate_count')) { - return $this->multiGenerate($request); - } - } - - private function multiGenerate(Request $request) - { - $userService = app(UserService::class); - $usersData = []; - - for ($i = 0; $i < $request->input('generate_count'); $i++) { - $email = Helper::randomChar(6) . '@' . $request->input('email_suffix'); - $usersData[] = [ - 'email' => $email, - 'password' => $request->input('password') ?? $email, - 'plan_id' => $request->input('plan_id'), - 'expired_at' => $request->input('expired_at'), - ]; - } - - - - try { - DB::beginTransaction(); - $users = []; - foreach ($usersData as $userData) { - $user = $userService->createUser($userData); - $user->save(); - $users[] = $user; - } - DB::commit(); - } catch (\Exception $e) { - DB::rollBack(); - return $this->fail([500, '生成失败']); - } - - // 判断是否导出 CSV - if ($request->input('download_csv')) { - $headers = [ - 'Content-Type' => 'text/csv', - 'Content-Disposition' => 'attachment; filename="users.csv"', - ]; - $callback = function () use ($users, $request) { - $handle = fopen('php://output', 'w'); - fputcsv($handle, ['账号', '密码', '过期时间', 'UUID', '创建时间', '订阅地址']); - foreach ($users as $user) { - $user = $user->refresh(); - $expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']); - $createDate = date('Y-m-d H:i:s', $user['created_at']); - $password = $request->input('password') ?? $user['email']; - $subscribeUrl = Helper::getSubscribeUrl($user['token']); - fputcsv($handle, [$user['email'], $password, $expireDate, $user['uuid'], $createDate, $subscribeUrl]); - } - fclose($handle); - }; - return response()->streamDownload($callback, 'users.csv', $headers); - } - - // 默认返回 JSON - $data = collect($users)->map(function ($user) use ($request) { - return [ - 'email' => $user['email'], - 'password' => $request->input('password') ?? $user['email'], - 'expired_at' => $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']), - 'uuid' => $user['uuid'], - 'created_at' => date('Y-m-d H:i:s', $user['created_at']), - 'subscribe_url' => Helper::getSubscribeUrl($user['token']), - ]; - }); - return response()->json([ - 'code' => 0, - 'message' => '批量生成成功', - 'data' => $data, - ]); - } - - private function multiGenerateWithPrefix(Request $request) - { - $userService = app(UserService::class); - $usersData = []; - $emailPrefix = $request->input('email_prefix'); - $emailSuffix = $request->input('email_suffix'); - $generateCount = $request->input('generate_count'); - - // Check if any of the emails with prefix already exist - for ($i = 1; $i <= $generateCount; $i++) { - $email = $emailPrefix . '_' . $i . '@' . $emailSuffix; - if (User::where('email', $email)->exists()) { - return $this->fail([400201, '邮箱 ' . $email . ' 已存在于系统中']); - } - } - - // Generate user data for batch creation - for ($i = 1; $i <= $generateCount; $i++) { - $email = $emailPrefix . '_' . $i . '@' . $emailSuffix; - $usersData[] = [ - 'email' => $email, - 'password' => $request->input('password') ?? $email, - 'plan_id' => $request->input('plan_id'), - 'expired_at' => $request->input('expired_at'), - ]; - } - - try { - DB::beginTransaction(); - $users = []; - foreach ($usersData as $userData) { - $user = $userService->createUser($userData); - $user->save(); - $users[] = $user; - } - DB::commit(); - } catch (\Exception $e) { - DB::rollBack(); - return $this->fail([500, '生成失败']); - } - - // 判断是否导出 CSV - if ($request->input('download_csv')) { - $headers = [ - 'Content-Type' => 'text/csv', - 'Content-Disposition' => 'attachment; filename="users.csv"', - ]; - $callback = function () use ($users, $request) { - $handle = fopen('php://output', 'w'); - fputcsv($handle, ['账号', '密码', '过期时间', 'UUID', '创建时间', '订阅地址']); - foreach ($users as $user) { - $user = $user->refresh(); - $expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']); - $createDate = date('Y-m-d H:i:s', $user['created_at']); - $password = $request->input('password') ?? $user['email']; - $subscribeUrl = Helper::getSubscribeUrl($user['token']); - fputcsv($handle, [$user['email'], $password, $expireDate, $user['uuid'], $createDate, $subscribeUrl]); - } - fclose($handle); - }; - return response()->streamDownload($callback, 'users.csv', $headers); - } - - // 默认返回 JSON - $data = collect($users)->map(function ($user) use ($request) { - return [ - 'email' => $user['email'], - 'password' => $request->input('password') ?? $user['email'], - 'expired_at' => $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']), - 'uuid' => $user['uuid'], - 'created_at' => date('Y-m-d H:i:s', $user['created_at']), - 'subscribe_url' => Helper::getSubscribeUrl($user['token']), - ]; - }); - return response()->json([ - 'code' => 0, - 'message' => '批量生成成功', - 'data' => $data, - ]); - } - - public function sendMail(UserSendMail $request) - { - ini_set('memory_limit', '-1'); - $scopeInfo = $this->resolveScope($request); - $scope = $scopeInfo['scope']; - $userIds = $scopeInfo['user_ids']; - - if ($scope === 'selected') { - if (empty($userIds)) { - return $this->fail([422, 'user_ids不能为空']); - } - } - - $sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC'; - $sort = $request->input('sort') ? $request->input('sort') : 'created_at'; - - $builder = User::query() - ->with('plan:id,name') - ->orderBy('id', 'desc'); - - if ($scope === 'filtered') { - // filtered: apply filters/sort - $builder->orderBy($sort, $sortType); - $this->applyFiltersAndSorts($request, $builder); - } elseif ($scope === 'selected') { - $builder->whereIn('id', $userIds); - } // all: ignore filter/sort - - $subject = $request->input('subject'); - $content = $request->input('content'); - $appName = admin_setting('app_name', 'XBoard'); - $appUrl = admin_setting('app_url'); - - $chunkSize = 1000; - - $builder->chunk($chunkSize, function ($users) use ($subject, $content, $appName, $appUrl) { - foreach ($users as $user) { - $vars = [ - 'app.name' => $appName, - 'app.url' => $appUrl, - 'now' => now()->format('Y-m-d H:i:s'), - 'user.id' => $user->id, - 'user.email' => $user->email, - 'user.uuid' => $user->uuid, - 'user.plan_name' => $user->plan?->name ?? '', - 'user.expired_at' => $user->expired_at ? date('Y-m-d H:i:s', $user->expired_at) : '', - 'user.transfer_enable' => (int) ($user->transfer_enable ?? 0), - 'user.transfer_used' => (int) (($user->u ?? 0) + ($user->d ?? 0)), - 'user.transfer_left' => (int) (($user->transfer_enable ?? 0) - (($user->u ?? 0) + ($user->d ?? 0))), - ]; - - $templateValue = [ - 'name' => $appName, - 'url' => $appUrl, - 'content' => $content, - 'vars' => $vars, - 'content_mode' => 'text', - ]; - - dispatch(new SendEmailJob([ - 'email' => $user->email, - 'subject' => $subject, - 'template_name' => 'notify', - 'template_value' => $templateValue - ], 'send_email_mass')); - } - }); - - return $this->success(true); - } - - public function ban(Request $request) - { - $scopeInfo = $this->resolveScope($request); - $scope = $scopeInfo['scope']; - $userIds = $scopeInfo['user_ids']; - - if ($scope === 'selected') { - if (empty($userIds)) { - return $this->fail([422, 'user_ids不能为空']); - } - } - - $sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC'; - $sort = $request->input('sort') ? $request->input('sort') : 'created_at'; - - $builder = User::query()->orderBy('id', 'desc'); - - if ($scope === 'filtered') { - // filtered: keep current semantics - $builder->orderBy($sort, $sortType); - $this->applyFiltersAndSorts($request, $builder); - } elseif ($scope === 'selected') { - $builder->whereIn('id', $userIds); - } // all: ignore filter/sort - - try { - $builder->update([ - 'banned' => 1 - ]); - } catch (\Exception $e) { - Log::error($e); - return $this->fail([500, '处理失败']); - } - // Full refresh not implemented. - return $this->success(true); - } - - // Delete user and related data. - public function destroy(Request $request) - { - $request->validate([ - 'id' => 'required|exists:App\Models\User,id' - ], [ - 'id.required' => '用户ID不能为空', - 'id.exists' => '用户不存在' - ]); - $user = User::find($request->input('id')); - try { - DB::beginTransaction(); - $user->orders()->delete(); - $user->codes()->delete(); - $user->stat()->delete(); - $user->tickets()->delete(); - $user->delete(); - DB::commit(); - return $this->success(true); - } catch (\Exception $e) { - DB::rollBack(); - Log::error($e); - return $this->fail([500, '删除失败']); - } - } -} diff --git a/Xboard/app/Http/Controllers/V2/Client/AppController.php b/Xboard/app/Http/Controllers/V2/Client/AppController.php deleted file mode 100644 index 85ec531..0000000 --- a/Xboard/app/Http/Controllers/V2/Client/AppController.php +++ /dev/null @@ -1,153 +0,0 @@ - [ - 'app_name' => admin_setting('app_name', 'XB加速器'), // 应用名称 - 'app_description' => admin_setting('app_description', '专业的网络加速服务'), // 应用描述 - 'app_url' => admin_setting('app_url', 'https://app.example.com'), // 应用官网 URL - 'logo' => admin_setting('logo', 'https://example.com/logo.png'), // 应用 Logo URL - 'version' => admin_setting('app_version', '1.0.0'), // 应用版本号 - ], - 'features' => [ - 'enable_register' => (bool) admin_setting('app_enable_register', true), // 是否开启注册功能 - 'enable_invite_system' => (bool) admin_setting('app_enable_invite_system', true), // 是否开启邀请系统 - 'enable_telegram_bot' => (bool) admin_setting('telegram_bot_enable', false), // 是否开启 Telegram 机器人 - 'enable_ticket_system' => (bool) admin_setting('app_enable_ticket_system', true), // 是否开启工单系统 - 'ticket_must_wait_reply' => (bool) admin_setting('ticket_must_wait_reply', 0), // 工单是否需要等待管理员回复后才可继续发消息 - 'enable_commission_system' => (bool) admin_setting('app_enable_commission_system', true), // 是否开启佣金系统 - 'enable_traffic_log' => (bool) admin_setting('app_enable_traffic_log', true), // 是否开启流量日志 - 'enable_knowledge_base' => (bool) admin_setting('app_enable_knowledge_base', true), // 是否开启知识库 - 'enable_announcements' => (bool) admin_setting('app_enable_announcements', true), // 是否开启公告系统 - 'enable_auto_renewal' => (bool) admin_setting('app_enable_auto_renewal', false), // 是否开启自动续费 - 'enable_coupon_system' => (bool) admin_setting('app_enable_coupon_system', true), // 是否开启优惠券系统 - 'enable_speed_test' => (bool) admin_setting('app_enable_speed_test', true), // 是否开启测速功能 - 'enable_server_ping' => (bool) admin_setting('app_enable_server_ping', true), // 是否开启服务器延迟检测 - ], - 'ui_config' => [ - 'theme' => [ - 'primary_color' => admin_setting('app_primary_color', '#00C851'), // 主色调 (十六进制) - 'secondary_color' => admin_setting('app_secondary_color', '#007E33'), // 辅助色 (十六进制) - 'accent_color' => admin_setting('app_accent_color', '#FF6B35'), // 强调色 (十六进制) - 'background_color' => admin_setting('app_background_color', '#F5F5F5'), // 背景色 (十六进制) - 'text_color' => admin_setting('app_text_color', '#333333'), // 文字色 (十六进制) - ], - 'home_screen' => [ - 'show_speed_test' => (bool) admin_setting('app_show_speed_test', true), // 是否显示测速 - 'show_traffic_chart' => (bool) admin_setting('app_show_traffic_chart', true), // 是否显示流量图表 - 'show_server_ping' => (bool) admin_setting('app_show_server_ping', true), // 是否显示服务器延迟 - 'default_server_sort' => admin_setting('app_default_server_sort', 'ping'), // 默认服务器排序方式 - 'show_connection_status' => (bool) admin_setting('app_show_connection_status', true), // 是否显示连接状态 - ], - 'server_list' => [ - 'show_country_flags' => (bool) admin_setting('app_show_country_flags', true), // 是否显示国家旗帜 - 'show_ping_values' => (bool) admin_setting('app_show_ping_values', true), // 是否显示延迟值 - 'show_traffic_usage' => (bool) admin_setting('app_show_traffic_usage', true), // 是否显示流量使用 - 'group_by_country' => (bool) admin_setting('app_group_by_country', false), // 是否按国家分组 - 'show_server_status' => (bool) admin_setting('app_show_server_status', true), // 是否显示服务器状态 - ], - ], - 'business_rules' => [ - 'min_password_length' => (int) admin_setting('app_min_password_length', 8), // 最小密码长度 - 'max_login_attempts' => (int) admin_setting('app_max_login_attempts', 5), // 最大登录尝试次数 - 'session_timeout_minutes' => (int) admin_setting('app_session_timeout_minutes', 30), // 会话超时时间(分钟) - 'auto_disconnect_after_minutes' => (int) admin_setting('app_auto_disconnect_after_minutes', 60), // 自动断开连接时间(分钟) - 'max_concurrent_connections' => (int) admin_setting('app_max_concurrent_connections', 3), // 最大并发连接数 - 'traffic_warning_threshold' => (float) admin_setting('app_traffic_warning_threshold', 0.8), // 流量警告阈值(0-1) - 'subscription_reminder_days' => admin_setting('app_subscription_reminder_days', [7, 3, 1]), // 订阅到期提醒天数 - 'connection_timeout_seconds' => (int) admin_setting('app_connection_timeout_seconds', 10), // 连接超时时间(秒) - 'health_check_interval_seconds' => (int) admin_setting('app_health_check_interval_seconds', 30), // 健康检查间隔(秒) - ], - 'server_config' => [ - 'default_kernel' => admin_setting('app_default_kernel', 'clash'), // 默认内核 (clash/singbox) - 'auto_select_fastest' => (bool) admin_setting('app_auto_select_fastest', true), // 是否自动选择最快服务器 - 'fallback_servers' => admin_setting('app_fallback_servers', ['server1', 'server2']), // 备用服务器列表 - 'enable_auto_switch' => (bool) admin_setting('app_enable_auto_switch', true), // 是否开启自动切换 - 'switch_threshold_ms' => (int) admin_setting('app_switch_threshold_ms', 1000), // 切换阈值(毫秒) - ], - 'security_config' => [ - 'tos_url' => admin_setting('tos_url', 'https://example.com/tos'), // 服务条款 URL - 'privacy_policy_url' => admin_setting('app_privacy_policy_url', 'https://example.com/privacy'), // 隐私政策 URL - 'is_email_verify' => (int) admin_setting('email_verify', 1), // 是否开启邮箱验证 (0/1) - 'is_invite_force' => (int) admin_setting('invite_force', 0), // 是否强制邀请码 (0/1) - 'email_whitelist_suffix' => (int) admin_setting('email_whitelist_suffix', 0), // 邮箱白名单后缀 (0/1) - 'is_captcha' => (int) admin_setting('captcha_enable', 1), // 是否开启验证码 (0/1) - 'captcha_type' => admin_setting('captcha_type', 'recaptcha'), // 验证码类型 (recaptcha/turnstile) - 'recaptcha_site_key' => admin_setting('recaptcha_site_key', '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI'), // reCAPTCHA 站点密钥 - 'recaptcha_v3_site_key' => admin_setting('recaptcha_v3_site_key', '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI'), // reCAPTCHA v3 站点密钥 - 'recaptcha_v3_score_threshold' => (float) admin_setting('recaptcha_v3_score_threshold', 0.5), // reCAPTCHA v3 分数阈值 - 'turnstile_site_key' => admin_setting('turnstile_site_key', '0x4AAAAAAAABkMYinukE8nzUg'), // Turnstile 站点密钥 - ], - 'payment_config' => [ - 'currency' => admin_setting('currency', 'CNY'), // 货币类型 - 'currency_symbol' => admin_setting('currency_symbol', '¥'), // 货币符号 - 'withdraw_methods' => admin_setting('app_withdraw_methods', ['alipay', 'wechat', 'bank']), // 提现方式列表 - 'min_withdraw_amount' => (int) admin_setting('app_min_withdraw_amount', 100), // 最小提现金额(分) - 'withdraw_fee_rate' => (float) admin_setting('app_withdraw_fee_rate', 0.01), // 提现手续费率 - ], - 'notification_config' => [ - 'enable_push_notifications' => (bool) admin_setting('app_enable_push_notifications', true), // 是否开启推送通知 - 'enable_email_notifications' => (bool) admin_setting('app_enable_email_notifications', true), // 是否开启邮件通知 - 'enable_sms_notifications' => (bool) admin_setting('app_enable_sms_notifications', false), // 是否开启短信通知 - 'notification_schedule' => [ - 'traffic_warning' => (bool) admin_setting('app_notification_traffic_warning', true), // 流量警告通知 - 'subscription_expiry' => (bool) admin_setting('app_notification_subscription_expiry', true), // 订阅到期通知 - 'server_maintenance' => (bool) admin_setting('app_notification_server_maintenance', true), // 服务器维护通知 - 'promotional_offers' => (bool) admin_setting('app_notification_promotional_offers', false), // 促销优惠通知 - ], - ], - 'cache_config' => [ - 'config_cache_duration' => (int) admin_setting('app_config_cache_duration', 3600), // 配置缓存时长(秒) - 'server_list_cache_duration' => (int) admin_setting('app_server_list_cache_duration', 1800), // 服务器列表缓存时长(秒) - 'user_info_cache_duration' => (int) admin_setting('app_user_info_cache_duration', 900), // 用户信息缓存时长(秒) - ], - 'last_updated' => time(), // 最后更新时间戳 - ]; - $config['config_hash'] = md5(json_encode($config)); // 配置哈希值(用于校验) - - $config = $config ?? []; - return response()->json(['data' => $config]); - } - - public function getVersion(Request $request) - { - if ( - strpos($request->header('user-agent'), 'tidalab/4.0.0') !== false - || strpos($request->header('user-agent'), 'tunnelab/4.0.0') !== false - ) { - if (strpos($request->header('user-agent'), 'Win64') !== false) { - $data = [ - 'version' => admin_setting('windows_version'), - 'download_url' => admin_setting('windows_download_url') - ]; - } else { - $data = [ - 'version' => admin_setting('macos_version'), - 'download_url' => admin_setting('macos_download_url') - ]; - } - } else { - $data = [ - 'windows_version' => admin_setting('windows_version'), - 'windows_download_url' => admin_setting('windows_download_url'), - 'macos_version' => admin_setting('macos_version'), - 'macos_download_url' => admin_setting('macos_download_url'), - 'android_version' => admin_setting('android_version'), - 'android_download_url' => admin_setting('android_download_url') - ]; - } - return $this->success($data); - } -} diff --git a/Xboard/app/Http/Controllers/V2/Server/ServerController.php b/Xboard/app/Http/Controllers/V2/Server/ServerController.php deleted file mode 100644 index ff58951..0000000 --- a/Xboard/app/Http/Controllers/V2/Server/ServerController.php +++ /dev/null @@ -1,138 +0,0 @@ - false]; - - if ((bool) admin_setting('server_ws_enable', 1)) { - $customUrl = trim((string) admin_setting('server_ws_url', '')); - - if ($customUrl !== '') { - $wsUrl = rtrim($customUrl, '/'); - } else { - $wsScheme = $request->isSecure() ? 'wss' : 'ws'; - $wsUrl = "{$wsScheme}://{$request->getHost()}:8076"; - } - - $websocket = [ - 'enabled' => true, - 'ws_url' => $wsUrl, - ]; - } - - return response()->json([ - 'websocket' => $websocket - ]); - } - - /** - * node report api - merge traffic + alive + status - * POST /api/v2/server/node/report - */ - public function report(Request $request): JsonResponse - { - $node = $request->attributes->get('node_info'); - $nodeType = $node->type; - $nodeId = $node->id; - - Cache::put(CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_CHECK_AT', $nodeId), time(), 3600); - - // hanle traffic data - $traffic = $request->input('traffic'); - if (is_array($traffic) && !empty($traffic)) { - $data = array_filter($traffic, function ($item) { - return is_array($item) - && count($item) === 2 - && is_numeric($item[0]) - && is_numeric($item[1]); - }); - - if (!empty($data)) { - Cache::put( - CacheKey::get('SERVER_' . strtoupper($nodeType) . '_ONLINE_USER', $nodeId), - count($data), - 3600 - ); - Cache::put( - CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_PUSH_AT', $nodeId), - time(), - 3600 - ); - $userService = new UserService(); - $userService->trafficFetch($node, $nodeType, $data); - } - } - - // handle alive data - $alive = $request->input('alive'); - if (is_array($alive) && !empty($alive)) { - $deviceStateService = app(DeviceStateService::class); - foreach ($alive as $uid => $ips) { - $deviceStateService->setDevices((int) $uid, $nodeId, (array) $ips); - } - } - - // handle active connections - $online = $request->input('online'); - if (is_array($online) && !empty($online)) { - $cacheTime = max(300, (int) admin_setting('server_push_interval', 60) * 3); - foreach ($online as $uid => $conn) { - $cacheKey = CacheKey::get("USER_ONLINE_CONN_{$nodeType}_{$nodeId}", $uid); - Cache::put($cacheKey, (int) $conn, $cacheTime); - } - } - - // handle node status - $status = $request->input('status'); - if (is_array($status) && !empty($status)) { - $statusData = [ - 'cpu' => (float) ($status['cpu'] ?? 0), - 'mem' => [ - 'total' => (int) ($status['mem']['total'] ?? 0), - 'used' => (int) ($status['mem']['used'] ?? 0), - ], - 'swap' => [ - 'total' => (int) ($status['swap']['total'] ?? 0), - 'used' => (int) ($status['swap']['used'] ?? 0), - ], - 'disk' => [ - 'total' => (int) ($status['disk']['total'] ?? 0), - 'used' => (int) ($status['disk']['used'] ?? 0), - ], - 'updated_at' => now()->timestamp, - 'kernel_status' => $status['kernel_status'] ?? null, - ]; - - $cacheTime = max(300, (int) admin_setting('server_push_interval', 60) * 3); - cache([ - CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LOAD_STATUS', $nodeId) => $statusData, - CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_LOAD_AT', $nodeId) => now()->timestamp, - ], $cacheTime); - } - - // handle node metrics (Metrics) - $metrics = $request->input('metrics'); - if (is_array($metrics) && !empty($metrics)) { - ServerService::updateMetrics($node, $metrics); - } - - return response()->json(['data' => true]); - } -} diff --git a/Xboard/app/Http/Kernel.php b/Xboard/app/Http/Kernel.php deleted file mode 100644 index 6fd86ab..0000000 --- a/Xboard/app/Http/Kernel.php +++ /dev/null @@ -1,99 +0,0 @@ - - */ - protected $middleware = [ - \Illuminate\Http\Middleware\HandleCors::class, - \App\Http\Middleware\TrustProxies::class, - \App\Http\Middleware\CheckForMaintenanceMode::class, - \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, - \App\Http\Middleware\TrimStrings::class, - \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, - \App\Http\Middleware\InitializePlugins::class, - ]; - - /** - * The application's route middleware groups. - * - * @var array> - */ - protected $middlewareGroups = [ - 'web' => [ - // \App\Http\Middleware\EncryptCookies::class, -// \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, -// \Illuminate\Session\Middleware\StartSession::class, - // \Illuminate\Session\Middleware\AuthenticateSession::class, -// \Illuminate\View\Middleware\ShareErrorsFromSession::class, -// \App\Http\Middleware\VerifyCsrfToken::class, -// \Illuminate\Routing\Middleware\SubstituteBindings::class, - \App\Http\Middleware\ApplyRuntimeSettings::class, - ], - - 'api' => [ - // \App\Http\Middleware\EncryptCookies::class, -// \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, -// \Illuminate\Session\Middleware\StartSession::class, - // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, - // \Illuminate\Routing\Middleware\ThrottleRequests::class . ':api', - // \Illuminate\Routing\Middleware\SubstituteBindings::class, - \App\Http\Middleware\ApplyRuntimeSettings::class, - \App\Http\Middleware\ForceJson::class, - \App\Http\Middleware\Language::class, - 'bindings', - ], - ]; - - /** - * The application's route middleware. - * - * These middleware may be assigned to groups or used individually. - * - * @var array - */ - protected $middlewareAliases = [ - 'auth' => \App\Http\Middleware\Authenticate::class, - 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, - 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, - 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, - 'can' => \Illuminate\Auth\Middleware\Authorize::class, - 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, - 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, - 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, - 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, - 'user' => \App\Http\Middleware\User::class, - 'admin' => \App\Http\Middleware\Admin::class, - 'client' => \App\Http\Middleware\Client::class, - 'staff' => \App\Http\Middleware\Staff::class, - 'log' => \App\Http\Middleware\RequestLog::class, - 'server' => \App\Http\Middleware\Server::class, - 'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class, - 'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class, - ]; - - /** - * The priority-sorted list of middleware. - * - * This forces non-global middleware to always be in the given order. - * - * @var array - */ - protected $middlewarePriority = [ - \Illuminate\Session\Middleware\StartSession::class, - \Illuminate\View\Middleware\ShareErrorsFromSession::class, - \Illuminate\Routing\Middleware\ThrottleRequests::class, - \Illuminate\Session\Middleware\AuthenticateSession::class, - \Illuminate\Routing\Middleware\SubstituteBindings::class, - \Illuminate\Auth\Middleware\Authorize::class, - ]; -} diff --git a/Xboard/app/Http/Middleware/Admin.php b/Xboard/app/Http/Middleware/Admin.php deleted file mode 100644 index 1a63b4f..0000000 --- a/Xboard/app/Http/Middleware/Admin.php +++ /dev/null @@ -1,30 +0,0 @@ -user(); - - if (!$user || !$user->is_admin) { - return response()->json(['message' => 'Unauthorized'], 403); - } - - return $next($request); - } -} diff --git a/Xboard/app/Http/Middleware/ApplyRuntimeSettings.php b/Xboard/app/Http/Middleware/ApplyRuntimeSettings.php deleted file mode 100644 index 2c3e96a..0000000 --- a/Xboard/app/Http/Middleware/ApplyRuntimeSettings.php +++ /dev/null @@ -1,25 +0,0 @@ -expectsJson() ? null : null; - } -} diff --git a/Xboard/app/Http/Middleware/CheckForMaintenanceMode.php b/Xboard/app/Http/Middleware/CheckForMaintenanceMode.php deleted file mode 100644 index 53fcdd5..0000000 --- a/Xboard/app/Http/Middleware/CheckForMaintenanceMode.php +++ /dev/null @@ -1,18 +0,0 @@ - - */ - protected $except = [ - // 示例: - // '/api/health-check', - // '/status' - ]; -} diff --git a/Xboard/app/Http/Middleware/Client.php b/Xboard/app/Http/Middleware/Client.php deleted file mode 100644 index 77645e0..0000000 --- a/Xboard/app/Http/Middleware/Client.php +++ /dev/null @@ -1,33 +0,0 @@ -input('token', $request->route('token')); - if (empty($token)) { - throw new ApiException('token is null',403); - } - $user = User::where('token', $token)->first(); - if (!$user) { - throw new ApiException('token is error',403); - } - - Auth::setUser($user); - return $next($request); - } -} diff --git a/Xboard/app/Http/Middleware/EncryptCookies.php b/Xboard/app/Http/Middleware/EncryptCookies.php deleted file mode 100644 index 31e9d1a..0000000 --- a/Xboard/app/Http/Middleware/EncryptCookies.php +++ /dev/null @@ -1,16 +0,0 @@ - - */ - protected $except = [ - // - ]; -} diff --git a/Xboard/app/Http/Middleware/EnsureTransactionState.php b/Xboard/app/Http/Middleware/EnsureTransactionState.php deleted file mode 100644 index 595dc50..0000000 --- a/Xboard/app/Http/Middleware/EnsureTransactionState.php +++ /dev/null @@ -1,29 +0,0 @@ - 0) { - DB::rollBack(); - } - } - } -} diff --git a/Xboard/app/Http/Middleware/ForceJson.php b/Xboard/app/Http/Middleware/ForceJson.php deleted file mode 100644 index ef87160..0000000 --- a/Xboard/app/Http/Middleware/ForceJson.php +++ /dev/null @@ -1,22 +0,0 @@ -headers->set('accept', 'application/json'); - return $next($request); - } -} diff --git a/Xboard/app/Http/Middleware/InitializePlugins.php b/Xboard/app/Http/Middleware/InitializePlugins.php deleted file mode 100644 index 0c5ae8d..0000000 --- a/Xboard/app/Http/Middleware/InitializePlugins.php +++ /dev/null @@ -1,37 +0,0 @@ -pluginManager = $pluginManager; - } - - /** - * Handle an incoming request. - * - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * @return mixed - */ - public function handle(Request $request, Closure $next) - { - // This single method call handles loading and booting all enabled plugins. - // It's safe to call multiple times, as it will only run once per request. - $this->pluginManager->initializeEnabledPlugins(); - - return $next($request); - } -} \ No newline at end of file diff --git a/Xboard/app/Http/Middleware/Language.php b/Xboard/app/Http/Middleware/Language.php deleted file mode 100644 index 8bb51e7..0000000 --- a/Xboard/app/Http/Middleware/Language.php +++ /dev/null @@ -1,17 +0,0 @@ -header('content-language')) { - App::setLocale($request->header('content-language')); - } - return $next($request); - } -} diff --git a/Xboard/app/Http/Middleware/RedirectIfAuthenticated.php b/Xboard/app/Http/Middleware/RedirectIfAuthenticated.php deleted file mode 100644 index a7ef27c..0000000 --- a/Xboard/app/Http/Middleware/RedirectIfAuthenticated.php +++ /dev/null @@ -1,26 +0,0 @@ -check()) { - return redirect('/home'); - } - - return $next($request); - } -} diff --git a/Xboard/app/Http/Middleware/RequestLog.php b/Xboard/app/Http/Middleware/RequestLog.php deleted file mode 100644 index 62dded3..0000000 --- a/Xboard/app/Http/Middleware/RequestLog.php +++ /dev/null @@ -1,60 +0,0 @@ -method() !== 'POST') { - return $next($request); - } - - $response = $next($request); - - try { - $admin = $request->user(); - if (!$admin || !$admin->is_admin) { - return $response; - } - - $action = $this->resolveAction($request->path()); - $data = collect($request->all())->except(self::SENSITIVE_KEYS)->toArray(); - - AdminAuditLog::insert([ - 'admin_id' => $admin->id, - 'action' => $action, - 'method' => $request->method(), - 'uri' => $request->getRequestUri(), - 'request_data' => json_encode($data, JSON_UNESCAPED_UNICODE), - 'ip' => $request->getClientIp(), - 'created_at' => time(), - 'updated_at' => time(), - ]); - } catch (\Throwable $e) { - \Log::warning('Audit log write failed: ' . $e->getMessage()); - } - - return $response; - } - - private function resolveAction(string $path): string - { - // api/v2/{secure_path}/user/update → user.update - $path = preg_replace('#^api/v[12]/[^/]+/#', '', $path); - // gift-card/create-template → gift_card.create_template - $path = str_replace('-', '_', $path); - // user/update → user.update, server/manage/sort → server_manage.sort - $segments = explode('/', $path); - $method = array_pop($segments); - $resource = implode('_', $segments); - - return $resource . '.' . $method; - } -} - diff --git a/Xboard/app/Http/Middleware/Server.php b/Xboard/app/Http/Middleware/Server.php deleted file mode 100644 index 15dd494..0000000 --- a/Xboard/app/Http/Middleware/Server.php +++ /dev/null @@ -1,59 +0,0 @@ -validateRequest($request); - $nodeType = $request->input('node_type', $nodeType); - $normalizedNodeType = ServerModel::normalizeType($nodeType); - $serverInfo = ServerService::getServer( - $request->input('node_id'), - $normalizedNodeType - ); - if (!$serverInfo) { - throw new ApiException('Server does not exist'); - } - - $request->attributes->set('node_info', $serverInfo); - return $next($request); - } - - private function validateRequest(Request $request): void - { - $request->validate([ - 'token' => [ - 'string', - 'required', - function ($attribute, $value, $fail) { - if ($value !== admin_setting('server_token')) { - $fail("Invalid {$attribute}"); - } - }, - ], - 'node_id' => 'required', - 'node_type' => [ - 'nullable', - function ($attribute, $value, $fail) use ($request) { - if ($value === "v2node") { - $value = null; - } - if (!ServerModel::isValidType($value)) { - $fail("Invalid node type specified"); - return; - } - $request->merge([$attribute => ServerModel::normalizeType($value)]); - }, - ] - ]); - } -} diff --git a/Xboard/app/Http/Middleware/Staff.php b/Xboard/app/Http/Middleware/Staff.php deleted file mode 100644 index 5c700c1..0000000 --- a/Xboard/app/Http/Middleware/Staff.php +++ /dev/null @@ -1,30 +0,0 @@ -input('auth_data') ?? $request->header('authorization'); - if (!$authorization) throw new ApiException( '未登录或登陆已过期', 403); - - $user = AuthService::decryptAuthData($authorization); - if (!$user || !$user['is_staff']) throw new ApiException('未登录或登陆已过期', 403); - $request->merge([ - 'user' => $user - ]); - return $next($request); - } -} diff --git a/Xboard/app/Http/Middleware/TrimStrings.php b/Xboard/app/Http/Middleware/TrimStrings.php deleted file mode 100644 index fb507a4..0000000 --- a/Xboard/app/Http/Middleware/TrimStrings.php +++ /dev/null @@ -1,19 +0,0 @@ - - */ - protected $except = [ - 'password', - 'password_confirmation', - 'encrypted_data', - 'signature' - ]; -} diff --git a/Xboard/app/Http/Middleware/TrustProxies.php b/Xboard/app/Http/Middleware/TrustProxies.php deleted file mode 100644 index 83c5400..0000000 --- a/Xboard/app/Http/Middleware/TrustProxies.php +++ /dev/null @@ -1,47 +0,0 @@ -|string|null - */ - protected $proxies = [ - "173.245.48.0/20", - "103.21.244.0/22", - "103.22.200.0/22", - "103.31.4.0/22", - "141.101.64.0/18", - "108.162.192.0/18", - "190.93.240.0/20", - "188.114.96.0/20", - "197.234.240.0/22", - "198.41.128.0/17", - "162.158.0.0/15", - "104.16.0.0/13", - "104.24.0.0/14", - "172.64.0.0/13", - "131.0.72.0/22", - "10.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16", - "169.254.0.0/16", - "127.0.0.0/8", - ]; - - /** - * 代理头映射 - * @var int - */ - protected $headers = - Request::HEADER_X_FORWARDED_FOR | - Request::HEADER_X_FORWARDED_HOST | - Request::HEADER_X_FORWARDED_PORT | - Request::HEADER_X_FORWARDED_PROTO | - Request::HEADER_X_FORWARDED_AWS_ELB; -} diff --git a/Xboard/app/Http/Middleware/User.php b/Xboard/app/Http/Middleware/User.php deleted file mode 100644 index 14f3049..0000000 --- a/Xboard/app/Http/Middleware/User.php +++ /dev/null @@ -1,27 +0,0 @@ -check()) { - throw new ApiException('未登录或登陆已过期', 403); - } - return $next($request); - } -} diff --git a/Xboard/app/Http/Middleware/VerifyCsrfToken.php b/Xboard/app/Http/Middleware/VerifyCsrfToken.php deleted file mode 100644 index 9e7c0bd..0000000 --- a/Xboard/app/Http/Middleware/VerifyCsrfToken.php +++ /dev/null @@ -1,22 +0,0 @@ - - */ - protected $except = [ - // - ]; -} diff --git a/Xboard/app/Http/Requests/Admin/ConfigSave.php b/Xboard/app/Http/Requests/Admin/ConfigSave.php deleted file mode 100644 index bec69a6..0000000 --- a/Xboard/app/Http/Requests/Admin/ConfigSave.php +++ /dev/null @@ -1,146 +0,0 @@ - '', - 'invite_commission' => 'integer|nullable', - 'invite_gen_limit' => 'integer|nullable', - 'invite_never_expire' => '', - 'commission_first_time_enable' => '', - 'commission_auto_check_enable' => '', - 'commission_withdraw_limit' => 'nullable|numeric', - 'commission_withdraw_method' => 'nullable|array', - 'withdraw_close_enable' => '', - 'commission_distribution_enable' => '', - 'commission_distribution_l1' => 'nullable|numeric', - 'commission_distribution_l2' => 'nullable|numeric', - 'commission_distribution_l3' => 'nullable|numeric', - // site - 'logo' => 'nullable|url', - 'force_https' => '', - 'stop_register' => '', - 'app_name' => '', - 'app_description' => '', - 'app_url' => 'nullable|url', - 'subscribe_url' => 'nullable', - 'try_out_enable' => '', - 'try_out_plan_id' => 'integer', - 'try_out_hour' => 'numeric', - 'tos_url' => 'nullable|url', - 'currency' => '', - 'currency_symbol' => '', - 'ticket_must_wait_reply' => '', - // subscribe - 'plan_change_enable' => '', - 'reset_traffic_method' => 'in:0,1,2,3,4', - 'surplus_enable' => '', - 'new_order_event_id' => '', - 'renew_order_event_id' => '', - 'change_order_event_id' => '', - 'show_info_to_server_enable' => '', - 'show_protocol_to_server_enable' => '', - 'subscribe_path' => '', - // server - 'server_token' => 'nullable|min:16', - 'server_pull_interval' => 'integer', - 'server_push_interval' => 'integer', - 'device_limit_mode' => 'integer', - 'server_ws_enable' => 'boolean', - 'server_ws_url' => 'nullable|url', - // frontend - 'frontend_theme' => '', - 'frontend_theme_sidebar' => 'nullable|in:dark,light', - 'frontend_theme_header' => 'nullable|in:dark,light', - 'frontend_theme_color' => 'nullable|in:default,darkblue,black,green', - 'frontend_background_url' => 'nullable|url', - // email - 'email_template' => '', - 'email_host' => '', - 'email_port' => '', - 'email_username' => '', - 'email_password' => '', - 'email_encryption' => '', - 'email_from_address' => '', - 'remind_mail_enable' => '', - // telegram - 'telegram_bot_enable' => '', - 'telegram_bot_token' => '', - 'telegram_webhook_url' => 'nullable|url', - 'telegram_discuss_id' => '', - 'telegram_channel_id' => '', - 'telegram_discuss_link' => 'nullable|url', - // app - 'windows_version' => '', - 'windows_download_url' => '', - 'macos_version' => '', - 'macos_download_url' => '', - 'android_version' => '', - 'android_download_url' => '', - // safe - 'email_whitelist_enable' => 'boolean', - 'email_whitelist_suffix' => 'nullable|array', - 'email_gmail_limit_enable' => 'boolean', - 'captcha_enable' => 'boolean', - 'captcha_type' => 'in:recaptcha,turnstile,recaptcha-v3', - 'recaptcha_enable' => 'boolean', - 'recaptcha_key' => '', - 'recaptcha_site_key' => '', - 'recaptcha_v3_secret_key' => '', - 'recaptcha_v3_site_key' => '', - 'recaptcha_v3_score_threshold' => 'numeric|min:0|max:1', - 'turnstile_secret_key' => '', - 'turnstile_site_key' => '', - 'email_verify' => 'bool', - 'safe_mode_enable' => 'boolean', - 'register_limit_by_ip_enable' => 'boolean', - 'register_limit_count' => 'integer', - 'register_limit_expire' => 'integer', - 'secure_path' => 'min:8|regex:/^[\w-]*$/', - 'password_limit_enable' => 'boolean', - 'password_limit_count' => 'integer', - 'password_limit_expire' => 'integer', - 'default_remind_expire' => 'boolean', - 'default_remind_traffic' => 'boolean', - 'subscribe_template_singbox' => 'nullable', - 'subscribe_template_clash' => 'nullable', - 'subscribe_template_clashmeta' => 'nullable', - 'subscribe_template_stash' => 'nullable', - 'subscribe_template_surge' => 'nullable', - 'subscribe_template_surfboard' => 'nullable' - ]; - /** - * Get the validation rules that apply to the request. - * - * @return array - */ - public function rules() - { - return self::RULES; - } - - public function messages() - { - // illiteracy prompt - return [ - 'app_url.url' => '站点URL格式不正确,必须携带http(s)://', - 'subscribe_url.url' => '订阅URL格式不正确,必须携带http(s)://', - 'server_token.min' => '通讯密钥长度必须大于16位', - 'tos_url.url' => '服务条款URL格式不正确,必须携带http(s)://', - 'telegram_webhook_url.url' => 'Telegram Webhook地址格式不正确,必须携带http(s)://', - 'telegram_discuss_link.url' => 'Telegram群组地址必须为URL格式,必须携带http(s)://', - 'logo.url' => 'LOGO URL格式不正确,必须携带https(s)://', - 'secure_path.min' => '后台路径长度最小为8位', - 'secure_path.regex' => '后台路径只能为字母或数字', - 'captcha_type.in' => '人机验证类型只能选择 recaptcha、turnstile 或 recaptcha-v3', - 'recaptcha_v3_score_threshold.numeric' => 'reCAPTCHA v3 分数阈值必须为数字', - 'recaptcha_v3_score_threshold.min' => 'reCAPTCHA v3 分数阈值不能小于0', - 'recaptcha_v3_score_threshold.max' => 'reCAPTCHA v3 分数阈值不能大于1' - ]; - } -} diff --git a/Xboard/app/Http/Requests/Admin/CouponGenerate.php b/Xboard/app/Http/Requests/Admin/CouponGenerate.php deleted file mode 100644 index 70f3efc..0000000 --- a/Xboard/app/Http/Requests/Admin/CouponGenerate.php +++ /dev/null @@ -1,51 +0,0 @@ - 'nullable|integer|max:500', - 'name' => 'required', - 'type' => 'required|in:1,2', - 'value' => 'required|integer', - 'started_at' => 'required|integer', - 'ended_at' => 'required|integer', - 'limit_use' => 'nullable|integer', - 'limit_use_with_user' => 'nullable|integer', - 'limit_plan_ids' => 'nullable|array', - 'limit_period' => 'nullable|array', - 'code' => '' - ]; - } - - public function messages() - { - return [ - 'generate_count.integer' => '生成数量必须为数字', - 'generate_count.max' => '生成数量最大为500个', - 'name.required' => '名称不能为空', - 'type.required' => '类型不能为空', - 'type.in' => '类型格式有误', - 'value.required' => '金额或比例不能为空', - 'value.integer' => '金额或比例格式有误', - 'started_at.required' => '开始时间不能为空', - 'started_at.integer' => '开始时间格式有误', - 'ended_at.required' => '结束时间不能为空', - 'ended_at.integer' => '结束时间格式有误', - 'limit_use.integer' => '最大使用次数格式有误', - 'limit_use_with_user.integer' => '限制用户使用次数格式有误', - 'limit_plan_ids.array' => '指定订阅格式有误', - 'limit_period.array' => '指定周期格式有误' - ]; - } -} diff --git a/Xboard/app/Http/Requests/Admin/KnowledgeCategorySave.php b/Xboard/app/Http/Requests/Admin/KnowledgeCategorySave.php deleted file mode 100644 index 9aabb7e..0000000 --- a/Xboard/app/Http/Requests/Admin/KnowledgeCategorySave.php +++ /dev/null @@ -1,29 +0,0 @@ - 'required', - 'language' => 'required' - ]; - } - - public function messages() - { - return [ - 'name.required' => '分类名称不能为空', - 'language.required' => '分类语言不能为空' - ]; - } -} diff --git a/Xboard/app/Http/Requests/Admin/KnowledgeCategorySort.php b/Xboard/app/Http/Requests/Admin/KnowledgeCategorySort.php deleted file mode 100644 index c76f810..0000000 --- a/Xboard/app/Http/Requests/Admin/KnowledgeCategorySort.php +++ /dev/null @@ -1,28 +0,0 @@ - 'required|array' - ]; - } - - public function messages() - { - return [ - 'knowledge_category_ids.required' => '分类不能为空', - 'knowledge_category_ids.array' => '分类格式有误' - ]; - } -} diff --git a/Xboard/app/Http/Requests/Admin/KnowledgeSave.php b/Xboard/app/Http/Requests/Admin/KnowledgeSave.php deleted file mode 100644 index 296ddbb..0000000 --- a/Xboard/app/Http/Requests/Admin/KnowledgeSave.php +++ /dev/null @@ -1,35 +0,0 @@ - 'required', - 'language' => 'required', - 'title' => 'required', - 'body' => 'required', - 'show' => 'nullable|boolean' - ]; - } - - public function messages() - { - return [ - 'title.required' => '标题不能为空', - 'category.required' => '分类不能为空', - 'body.required' => '内容不能为空', - 'language.required' => '语言不能为空', - 'show.boolean' => '显示状态必须为布尔值' - ]; - } -} diff --git a/Xboard/app/Http/Requests/Admin/KnowledgeSort.php b/Xboard/app/Http/Requests/Admin/KnowledgeSort.php deleted file mode 100644 index d29a899..0000000 --- a/Xboard/app/Http/Requests/Admin/KnowledgeSort.php +++ /dev/null @@ -1,28 +0,0 @@ - 'required|array' - ]; - } - - public function messages() - { - return [ - 'knowledge_ids.required' => '知识ID不能为空', - 'knowledge_ids.array' => '知识ID格式有误' - ]; - } -} diff --git a/Xboard/app/Http/Requests/Admin/MailSend.php b/Xboard/app/Http/Requests/Admin/MailSend.php deleted file mode 100644 index 86247a3..0000000 --- a/Xboard/app/Http/Requests/Admin/MailSend.php +++ /dev/null @@ -1,34 +0,0 @@ - 'required|in:1,2,3,4', - 'subject' => 'required', - 'content' => 'required', - 'receiver' => 'array' - ]; - } - - public function messages() - { - return [ - 'type.required' => '发送类型不能为空', - 'type.in' => '发送类型格式有误', - 'subject.required' => '主题不能为空', - 'content.required' => '内容不能为空', - 'receiver.array' => '收件人格式有误' - ]; - } -} diff --git a/Xboard/app/Http/Requests/Admin/NoticeSave.php b/Xboard/app/Http/Requests/Admin/NoticeSave.php deleted file mode 100644 index 0f6dc0b..0000000 --- a/Xboard/app/Http/Requests/Admin/NoticeSave.php +++ /dev/null @@ -1,33 +0,0 @@ - 'required', - 'content' => 'required', - 'img_url' => 'nullable|url', - 'tags' => 'nullable|array' - ]; - } - - public function messages() - { - return [ - 'title.required' => '标题不能为空', - 'content.required' => '内容不能为空', - 'img_url.url' => '图片URL格式不正确', - 'tags.array' => '标签格式不正确' - ]; - } -} diff --git a/Xboard/app/Http/Requests/Admin/OrderAssign.php b/Xboard/app/Http/Requests/Admin/OrderAssign.php deleted file mode 100644 index 4b259a0..0000000 --- a/Xboard/app/Http/Requests/Admin/OrderAssign.php +++ /dev/null @@ -1,34 +0,0 @@ - 'required', - 'email' => 'required', - 'total_amount' => 'required', - 'period' => 'required|in:month_price,quarter_price,half_year_price,year_price,two_year_price,three_year_price,onetime_price,reset_price' - ]; - } - - public function messages() - { - return [ - 'plan_id.required' => '订阅不能为空', - 'email.required' => '邮箱不能为空', - 'total_amount.required' => '支付金额不能为空', - 'period.required' => '订阅周期不能为空', - 'period.in' => '订阅周期格式有误' - ]; - } -} diff --git a/Xboard/app/Http/Requests/Admin/OrderFetch.php b/Xboard/app/Http/Requests/Admin/OrderFetch.php deleted file mode 100644 index 9c4765b..0000000 --- a/Xboard/app/Http/Requests/Admin/OrderFetch.php +++ /dev/null @@ -1,32 +0,0 @@ - 'required|in:email,trade_no,status,commission_status,user_id,invite_user_id,callback_no,commission_balance', - 'filter.*.condition' => 'required|in:>,<,=,>=,<=,模糊,!=', - 'filter.*.value' => '' - ]; - } - - public function messages() - { - return [ - 'filter.*.key.required' => '过滤键不能为空', - 'filter.*.key.in' => '过滤键参数有误', - 'filter.*.condition.required' => '过滤条件不能为空', - 'filter.*.condition.in' => '过滤条件参数有误', - ]; - } -} diff --git a/Xboard/app/Http/Requests/Admin/OrderUpdate.php b/Xboard/app/Http/Requests/Admin/OrderUpdate.php deleted file mode 100644 index 8a38d10..0000000 --- a/Xboard/app/Http/Requests/Admin/OrderUpdate.php +++ /dev/null @@ -1,29 +0,0 @@ - 'in:0,1,2,3', - 'commission_status' => 'in:0,1,3' - ]; - } - - public function messages() - { - return [ - 'status.in' => '销售状态格式不正确', - 'commission_status.in' => '佣金状态格式不正确' - ]; - } -} diff --git a/Xboard/app/Http/Requests/Admin/PlanSave.php b/Xboard/app/Http/Requests/Admin/PlanSave.php deleted file mode 100644 index c35e4a7..0000000 --- a/Xboard/app/Http/Requests/Admin/PlanSave.php +++ /dev/null @@ -1,157 +0,0 @@ - 'nullable|integer', - 'name' => 'required|string|max:255', - 'content' => 'nullable|string', - 'reset_traffic_method' => 'integer|nullable', - 'transfer_enable' => 'integer|required|min:1', - 'prices' => 'nullable|array', - 'prices.*' => 'nullable|numeric|min:0', - 'group_id' => 'integer|nullable', - 'speed_limit' => 'integer|nullable|min:0', - 'device_limit' => 'integer|nullable|min:0', - 'capacity_limit' => 'integer|nullable|min:0', - 'tags' => 'array|nullable', - ]; - } - - /** - * Configure the validator instance. - */ - public function withValidator(Validator $validator): void - { - $validator->after(function (Validator $validator) { - $this->validatePrices($validator); - }); - } - - /** - * 验证价格配置 - */ - protected function validatePrices(Validator $validator): void - { - $prices = $this->input('prices', []); - - if (empty($prices)) { - return; - } - - // 获取所有有效的周期 - $validPeriods = array_keys(Plan::getAvailablePeriods()); - - foreach ($prices as $period => $price) { - // 验证周期是否有效 - if (!in_array($period, $validPeriods)) { - $validator->errors()->add( - "prices.{$period}", - "不支持的订阅周期: {$period}" - ); - continue; - } - - // 价格可以为 null、空字符串或大于 0 的数字 - if ($price !== null && $price !== '') { - // 转换为数字进行验证 - $numericPrice = is_numeric($price) ? (float) $price : null; - - if ($numericPrice === null) { - $validator->errors()->add( - "prices.{$period}", - "价格必须是数字格式" - ); - } elseif ($numericPrice < 0) { - $validator->errors()->add( - "prices.{$period}", - "价格必须大于等于 0(如不需要此周期请留空)" - ); - } - } - } - } - - /** - * 处理验证后的数据 - */ - protected function passedValidation(): void - { - // 清理和格式化价格数据 - $prices = $this->input('prices', []); - $cleanedPrices = []; - - foreach ($prices as $period => $price) { - // 只保留有效的正数价格 - if ($price !== null && $price !== '' && is_numeric($price)) { - $numericPrice = (float) $price; - if ($numericPrice > 0) { - // 转换为浮点数并保留两位小数 - $cleanedPrices[$period] = round($numericPrice, 2); - } - } - } - - // 更新请求中的价格数据 - $this->merge(['prices' => $cleanedPrices]); - } - - /** - * Get custom error messages for validator errors. - */ - public function messages(): array - { - return [ - 'name.required' => '套餐名称不能为空', - 'name.max' => '套餐名称不能超过 255 个字符', - 'transfer_enable.required' => '流量配额不能为空', - 'transfer_enable.integer' => '流量配额必须是整数', - 'transfer_enable.min' => '流量配额必须大于 0', - 'prices.array' => '价格配置格式错误', - 'prices.*.numeric' => '价格必须是数字', - 'prices.*.min' => '价格不能为负数', - 'group_id.integer' => '权限组ID必须是整数', - 'speed_limit.integer' => '速度限制必须是整数', - 'speed_limit.min' => '速度限制不能为负数', - 'device_limit.integer' => '设备限制必须是整数', - 'device_limit.min' => '设备限制不能为负数', - 'capacity_limit.integer' => '容量限制必须是整数', - 'capacity_limit.min' => '容量限制不能为负数', - 'tags.array' => '标签格式必须是数组', - ]; - } - - /** - * Handle a failed validation attempt. - */ - protected function failedValidation(Validator $validator): void - { - throw new HttpResponseException( - response()->json([ - 'data' => false, - 'message' => $validator->errors()->first(), - 'errors' => $validator->errors()->toArray() - ], 422) - ); - } -} diff --git a/Xboard/app/Http/Requests/Admin/PlanSort.php b/Xboard/app/Http/Requests/Admin/PlanSort.php deleted file mode 100644 index eb7987a..0000000 --- a/Xboard/app/Http/Requests/Admin/PlanSort.php +++ /dev/null @@ -1,28 +0,0 @@ - 'required|array' - ]; - } - - public function messages() - { - return [ - 'plan_ids.required' => '订阅计划ID不能为空', - 'plan_ids.array' => '订阅计划ID格式有误' - ]; - } -} diff --git a/Xboard/app/Http/Requests/Admin/PlanUpdate.php b/Xboard/app/Http/Requests/Admin/PlanUpdate.php deleted file mode 100644 index d9463e2..0000000 --- a/Xboard/app/Http/Requests/Admin/PlanUpdate.php +++ /dev/null @@ -1,29 +0,0 @@ - 'in:0,1', - 'renew' => 'in:0,1' - ]; - } - - public function messages() - { - return [ - 'show.in' => '销售状态格式不正确', - 'renew.in' => '续费状态格式不正确' - ]; - } -} diff --git a/Xboard/app/Http/Requests/Admin/ServerSave.php b/Xboard/app/Http/Requests/Admin/ServerSave.php deleted file mode 100644 index bd1f6b2..0000000 --- a/Xboard/app/Http/Requests/Admin/ServerSave.php +++ /dev/null @@ -1,212 +0,0 @@ - 'nullable|boolean', - 'utls.fingerprint' => 'nullable|string', - ]; - - private const MULTIPLEX_RULES = [ - 'multiplex.enabled' => 'nullable|boolean', - 'multiplex.protocol' => 'nullable|string', - 'multiplex.max_connections' => 'nullable|integer', - 'multiplex.min_streams' => 'nullable|integer', - 'multiplex.max_streams' => 'nullable|integer', - 'multiplex.padding' => 'nullable|boolean', - 'multiplex.brutal.enabled' => 'nullable|boolean', - 'multiplex.brutal.up_mbps' => 'nullable|integer', - 'multiplex.brutal.down_mbps' => 'nullable|integer', - ]; - - private const PROTOCOL_RULES = [ - 'shadowsocks' => [ - 'cipher' => 'required|string', - 'obfs' => 'nullable|string', - 'obfs_settings.path' => 'nullable|string', - 'obfs_settings.host' => 'nullable|string', - 'plugin' => 'nullable|string', - 'plugin_opts' => 'nullable|string', - ], - 'vmess' => [ - 'tls' => 'required|integer', - 'network' => 'required|string', - 'network_settings' => 'nullable|array', - 'tls_settings.server_name' => 'nullable|string', - 'tls_settings.allow_insecure' => 'nullable|boolean', - ], - 'trojan' => [ - 'tls' => 'nullable|integer', - 'network' => 'required|string', - 'network_settings' => 'nullable|array', - 'server_name' => 'nullable|string', - 'allow_insecure' => 'nullable|boolean', - 'reality_settings.allow_insecure' => 'nullable|boolean', - 'reality_settings.server_name' => 'nullable|string', - 'reality_settings.server_port' => 'nullable|integer', - 'reality_settings.public_key' => 'nullable|string', - 'reality_settings.private_key' => 'nullable|string', - 'reality_settings.short_id' => 'nullable|string', - ], - 'hysteria' => [ - 'version' => 'required|integer', - 'alpn' => 'nullable|string', - 'obfs.open' => 'nullable|boolean', - 'obfs.type' => 'string|nullable', - 'obfs.password' => 'string|nullable', - 'tls.server_name' => 'nullable|string', - 'tls.allow_insecure' => 'nullable|boolean', - 'bandwidth.up' => 'nullable|integer', - 'bandwidth.down' => 'nullable|integer', - 'hop_interval' => 'integer|nullable', - ], - 'vless' => [ - 'tls' => 'required|integer', - 'network' => 'required|string', - 'network_settings' => 'nullable|array', - 'flow' => 'nullable|string', - 'encryption' => 'nullable|array', - 'encryption.enabled' => 'nullable|boolean', - 'encryption.encryption' => 'nullable|string', - 'encryption.decryption' => 'nullable|string', - 'tls_settings.server_name' => 'nullable|string', - 'tls_settings.allow_insecure' => 'nullable|boolean', - 'reality_settings.allow_insecure' => 'nullable|boolean', - 'reality_settings.server_name' => 'nullable|string', - 'reality_settings.server_port' => 'nullable|integer', - 'reality_settings.public_key' => 'nullable|string', - 'reality_settings.private_key' => 'nullable|string', - 'reality_settings.short_id' => 'nullable|string', - ], - 'socks' => [ - ], - 'naive' => [ - 'tls' => 'required|integer', - 'tls_settings' => 'nullable|array', - ], - 'http' => [ - 'tls' => 'required|integer', - 'tls_settings' => 'nullable|array', - ], - 'mieru' => [ - 'transport' => 'required|string|in:TCP,UDP', - 'traffic_pattern' => 'string' - ], - 'anytls' => [ - 'tls' => 'nullable|array', - 'alpn' => 'nullable|string', - 'padding_scheme' => 'nullable|array', - ], - ]; - - private function getBaseRules(): array - { - return [ - 'type' => 'required|in:' . implode(',', Server::VALID_TYPES), - 'spectific_key' => 'nullable|string', - 'code' => 'nullable|string', - 'show' => '', - 'name' => 'required|string', - 'group_ids' => 'nullable|array', - 'route_ids' => 'nullable|array', - 'parent_id' => 'nullable|integer', - 'host' => 'required', - 'port' => 'required', - 'server_port' => 'required', - 'tags' => 'nullable|array', - 'excludes' => 'nullable|array', - 'ips' => 'nullable|array', - 'rate' => 'required|numeric', - 'rate_time_enable' => 'nullable|boolean', - 'rate_time_ranges' => 'nullable|array', - 'custom_outbounds' => 'nullable|array', - 'custom_routes' => 'nullable|array', - 'cert_config' => 'nullable|array', - 'rate_time_ranges.*.start' => 'required_with:rate_time_ranges|string|date_format:H:i', - 'rate_time_ranges.*.end' => 'required_with:rate_time_ranges|string|date_format:H:i', - 'rate_time_ranges.*.rate' => 'required_with:rate_time_ranges|numeric|min:0', - 'protocol_settings' => 'array', - 'transfer_enable' => 'nullable|integer|min:0', - ]; - } - - public function rules(): array - { - $type = $this->input('type'); - $rules = $this->getBaseRules(); - - $protocolRules = self::PROTOCOL_RULES[$type] ?? []; - if (in_array($type, ['vmess', 'vless', 'trojan', 'mieru'])) { - $protocolRules = array_merge($protocolRules, self::MULTIPLEX_RULES, self::UTLS_RULES); - } - - foreach ($protocolRules as $field => $rule) { - $rules['protocol_settings.' . $field] = $rule; - } - - return $rules; - } - - public function attributes(): array - { - return [ - 'protocol_settings.cipher' => '加密方式', - 'protocol_settings.obfs' => '混淆类型', - 'protocol_settings.network' => '传输协议', - 'protocol_settings.port_range' => '端口范围', - 'protocol_settings.traffic_pattern' => 'Traffic Pattern', - 'protocol_settings.transport' => '传输方式', - 'protocol_settings.version' => '协议版本', - 'protocol_settings.password' => '密码', - 'protocol_settings.handshake.server' => '握手服务器', - 'protocol_settings.handshake.server_port' => '握手端口', - 'protocol_settings.multiplex.enabled' => '多路复用', - 'protocol_settings.multiplex.protocol' => '复用协议', - 'protocol_settings.multiplex.max_connections' => '最大连接数', - 'protocol_settings.multiplex.min_streams' => '最小流数', - 'protocol_settings.multiplex.max_streams' => '最大流数', - 'protocol_settings.multiplex.padding' => '复用填充', - 'protocol_settings.multiplex.brutal.enabled' => 'Brutal加速', - 'protocol_settings.multiplex.brutal.up_mbps' => 'Brutal上行速率', - 'protocol_settings.multiplex.brutal.down_mbps' => 'Brutal下行速率', - 'protocol_settings.utls.enabled' => 'uTLS', - 'protocol_settings.utls.fingerprint' => 'uTLS指纹', - ]; - } - - public function messages() - { - return [ - 'name.required' => '节点名称不能为空', - 'group_ids.required' => '权限组不能为空', - 'group_ids.array' => '权限组格式不正确', - 'route_ids.array' => '路由组格式不正确', - 'parent_id.integer' => '父ID格式不正确', - 'host.required' => '节点地址不能为空', - 'port.required' => '连接端口不能为空', - 'server_port.required' => '后端服务端口不能为空', - 'tls.required' => 'TLS不能为空', - 'tags.array' => '标签格式不正确', - 'rate.required' => '倍率不能为空', - 'rate.numeric' => '倍率格式不正确', - 'network.required' => '传输协议不能为空', - 'network.in' => '传输协议格式不正确', - 'networkSettings.array' => '传输协议配置有误', - 'ruleSettings.array' => '规则配置有误', - 'tlsSettings.array' => 'tls配置有误', - 'dnsSettings.array' => 'dns配置有误', - 'protocol_settings.*.required' => ':attribute 不能为空', - 'protocol_settings.*.string' => ':attribute 必须是字符串', - 'protocol_settings.*.integer' => ':attribute 必须是整数', - 'protocol_settings.*.in' => ':attribute 的值不合法', - 'transfer_enable.integer' => '流量上限必须是整数', - 'transfer_enable.min' => '流量上限不能小于0', - ]; - } -} diff --git a/Xboard/app/Http/Requests/Admin/UserFetch.php b/Xboard/app/Http/Requests/Admin/UserFetch.php deleted file mode 100644 index 899c6a9..0000000 --- a/Xboard/app/Http/Requests/Admin/UserFetch.php +++ /dev/null @@ -1,33 +0,0 @@ - 'required|in:id,email,transfer_enable,d,expired_at,uuid,token,invite_by_email,invite_user_id,plan_id,banned,remarks,is_admin', - 'filter.*.condition' => 'required|in:>,<,=,>=,<=,模糊,!=', - 'filter.*.value' => 'required' - ]; - } - - public function messages() - { - return [ - 'filter.*.key.required' => '过滤键不能为空', - 'filter.*.key.in' => '过滤键参数有误', - 'filter.*.condition.required' => '过滤条件不能为空', - 'filter.*.condition.in' => '过滤条件参数有误', - 'filter.*.value.required' => '过滤值不能为空' - ]; - } -} diff --git a/Xboard/app/Http/Requests/Admin/UserGenerate.php b/Xboard/app/Http/Requests/Admin/UserGenerate.php deleted file mode 100644 index 41b0722..0000000 --- a/Xboard/app/Http/Requests/Admin/UserGenerate.php +++ /dev/null @@ -1,33 +0,0 @@ - 'nullable|integer|max:500', - 'expired_at' => 'nullable|integer', - 'plan_id' => 'nullable|integer', - 'email_prefix' => 'nullable', - 'email_suffix' => 'required', - 'password' => 'nullable' - ]; - } - - public function messages() - { - return [ - 'generate_count.integer' => '生成数量必须为数字', - 'generate_count.max' => '生成数量最大为500个' - ]; - } -} diff --git a/Xboard/app/Http/Requests/Admin/UserSendMail.php b/Xboard/app/Http/Requests/Admin/UserSendMail.php deleted file mode 100644 index f885c36..0000000 --- a/Xboard/app/Http/Requests/Admin/UserSendMail.php +++ /dev/null @@ -1,29 +0,0 @@ - 'required', - 'content' => 'required', - ]; - } - - public function messages() - { - return [ - 'subject.required' => '主题不能为空', - 'content.required' => '发送内容不能为空' - ]; - } -} diff --git a/Xboard/app/Http/Requests/Admin/UserUpdate.php b/Xboard/app/Http/Requests/Admin/UserUpdate.php deleted file mode 100644 index afbf922..0000000 --- a/Xboard/app/Http/Requests/Admin/UserUpdate.php +++ /dev/null @@ -1,69 +0,0 @@ - 'required|integer', - 'email' => 'email:strict', - 'password' => 'nullable|min:8', - 'transfer_enable' => 'numeric', - 'expired_at' => 'nullable|integer', - 'banned' => 'bool', - 'plan_id' => 'nullable|integer', - 'commission_rate' => 'nullable|integer|min:0|max:100', - 'discount' => 'nullable|integer|min:0|max:100', - 'is_admin' => 'boolean', - 'is_staff' => 'boolean', - 'u' => 'integer', - 'd' => 'integer', - 'balance' => 'numeric', - 'commission_type' => 'integer', - 'commission_balance' => 'numeric', - 'remarks' => 'nullable', - 'speed_limit' => 'nullable|integer', - 'device_limit' => 'nullable|integer' - ]; - } - - public function messages() - { - return [ - 'email.required' => '邮箱不能为空', - 'email.email' => '邮箱格式不正确', - 'transfer_enable.numeric' => '流量格式不正确', - 'expired_at.integer' => '到期时间格式不正确', - 'banned.in' => '是否封禁格式不正确', - 'is_admin.required' => '是否管理员不能为空', - 'is_admin.in' => '是否管理员格式不正确', - 'is_staff.required' => '是否员工不能为空', - 'is_staff.in' => '是否员工格式不正确', - 'plan_id.integer' => '订阅计划格式不正确', - 'commission_rate.integer' => '推荐返利比例格式不正确', - 'commission_rate.nullable' => '推荐返利比例格式不正确', - 'commission_rate.min' => '推荐返利比例最小为0', - 'commission_rate.max' => '推荐返利比例最大为100', - 'discount.integer' => '专属折扣比例格式不正确', - 'discount.nullable' => '专属折扣比例格式不正确', - 'discount.min' => '专属折扣比例最小为0', - 'discount.max' => '专属折扣比例最大为100', - 'u.integer' => '上行流量格式不正确', - 'd.integer' => '下行流量格式不正确', - 'balance.integer' => '余额格式不正确', - 'commission_balance.integer' => '佣金格式不正确', - 'password.min' => '密码长度最小8位', - 'speed_limit.integer' => '限速格式不正确', - 'device_limit.integer' => '设备数量格式不正确' - ]; - } -} diff --git a/Xboard/app/Http/Requests/Passport/AuthForget.php b/Xboard/app/Http/Requests/Passport/AuthForget.php deleted file mode 100644 index 8106f28..0000000 --- a/Xboard/app/Http/Requests/Passport/AuthForget.php +++ /dev/null @@ -1,33 +0,0 @@ - 'required|email:strict', - 'password' => 'required|min:8', - 'email_code' => 'required' - ]; - } - - public function messages() - { - return [ - 'email.required' => __('Email can not be empty'), - 'email.email' => __('Email format is incorrect'), - 'password.required' => __('Password can not be empty'), - 'password.min' => __('Password must be greater than 8 digits'), - 'email_code.required' => __('Email verification code cannot be empty') - ]; - } -} diff --git a/Xboard/app/Http/Requests/Passport/AuthLogin.php b/Xboard/app/Http/Requests/Passport/AuthLogin.php deleted file mode 100644 index 6aa832c..0000000 --- a/Xboard/app/Http/Requests/Passport/AuthLogin.php +++ /dev/null @@ -1,31 +0,0 @@ - 'required|email:strict', - 'password' => 'required|min:8' - ]; - } - - public function messages() - { - return [ - 'email.required' => __('Email can not be empty'), - 'email.email' => __('Email format is incorrect'), - 'password.required' => __('Password can not be empty'), - 'password.min' => __('Password must be greater than 8 digits') - ]; - } -} diff --git a/Xboard/app/Http/Requests/Passport/AuthRegister.php b/Xboard/app/Http/Requests/Passport/AuthRegister.php deleted file mode 100644 index 63e053a..0000000 --- a/Xboard/app/Http/Requests/Passport/AuthRegister.php +++ /dev/null @@ -1,31 +0,0 @@ - 'required|email:strict', - 'password' => 'required|min:8' - ]; - } - - public function messages() - { - return [ - 'email.required' => __('Email can not be empty'), - 'email.email' => __('Email format is incorrect'), - 'password.required' => __('Password can not be empty'), - 'password.min' => __('Password must be greater than 8 digits') - ]; - } -} diff --git a/Xboard/app/Http/Requests/Passport/CommSendEmailVerify.php b/Xboard/app/Http/Requests/Passport/CommSendEmailVerify.php deleted file mode 100644 index ff5ecdd..0000000 --- a/Xboard/app/Http/Requests/Passport/CommSendEmailVerify.php +++ /dev/null @@ -1,28 +0,0 @@ - 'required|email:strict' - ]; - } - - public function messages() - { - return [ - 'email.required' => __('Email can not be empty'), - 'email.email' => __('Email format is incorrect') - ]; - } -} diff --git a/Xboard/app/Http/Requests/Staff/UserUpdate.php b/Xboard/app/Http/Requests/Staff/UserUpdate.php deleted file mode 100644 index be22144..0000000 --- a/Xboard/app/Http/Requests/Staff/UserUpdate.php +++ /dev/null @@ -1,56 +0,0 @@ - 'required|email:strict', - 'password' => 'nullable', - 'transfer_enable' => 'numeric', - 'expired_at' => 'nullable|integer', - 'banned' => 'required|in:0,1', - 'plan_id' => 'nullable|integer', - 'commission_rate' => 'nullable|integer|min:0|max:100', - 'discount' => 'nullable|integer|min:0|max:100', - 'u' => 'integer', - 'd' => 'integer', - 'balance' => 'integer', - 'commission_balance' => 'integer' - ]; - } - - public function messages() - { - return [ - 'email.required' => '邮箱不能为空', - 'email.email' => '邮箱格式不正确', - 'transfer_enable.numeric' => '流量格式不正确', - 'expired_at.integer' => '到期时间格式不正确', - 'banned.required' => '是否封禁不能为空', - 'banned.in' => '是否封禁格式不正确', - 'plan_id.integer' => '订阅计划格式不正确', - 'commission_rate.integer' => '推荐返利比例格式不正确', - 'commission_rate.nullable' => '推荐返利比例格式不正确', - 'commission_rate.min' => '推荐返利比例最小为0', - 'commission_rate.max' => '推荐返利比例最大为100', - 'discount.integer' => '专属折扣比例格式不正确', - 'discount.nullable' => '专属折扣比例格式不正确', - 'discount.min' => '专属折扣比例最小为0', - 'discount.max' => '专属折扣比例最大为100', - 'u.integer' => '上行流量格式不正确', - 'd.integer' => '下行流量格式不正确', - 'balance.integer' => '余额格式不正确', - 'commission_balance.integer' => '佣金格式不正确' - ]; - } -} diff --git a/Xboard/app/Http/Requests/User/GiftCardCheckRequest.php b/Xboard/app/Http/Requests/User/GiftCardCheckRequest.php deleted file mode 100644 index ed0b5e3..0000000 --- a/Xboard/app/Http/Requests/User/GiftCardCheckRequest.php +++ /dev/null @@ -1,28 +0,0 @@ -|string> - */ - public function rules(): array - { - return [ - // - ]; - } -} diff --git a/Xboard/app/Http/Requests/User/GiftCardRedeemRequest.php b/Xboard/app/Http/Requests/User/GiftCardRedeemRequest.php deleted file mode 100644 index 7feb4b4..0000000 --- a/Xboard/app/Http/Requests/User/GiftCardRedeemRequest.php +++ /dev/null @@ -1,44 +0,0 @@ - 'required|string|min:8|max:32', - ]; - } - - /** - * Get custom messages for validator errors. - * - * @return array - */ - public function messages() - { - return [ - 'code.required' => '请输入兑换码', - 'code.min' => '兑换码长度不能少于8位', - 'code.max' => '兑换码长度不能超过32位', - ]; - } -} diff --git a/Xboard/app/Http/Requests/User/OrderSave.php b/Xboard/app/Http/Requests/User/OrderSave.php deleted file mode 100644 index 449bcaa..0000000 --- a/Xboard/app/Http/Requests/User/OrderSave.php +++ /dev/null @@ -1,30 +0,0 @@ - 'required', - 'period' => 'required|in:month_price,quarter_price,half_year_price,year_price,two_year_price,three_year_price,onetime_price,reset_price' - ]; - } - - public function messages() - { - return [ - 'plan_id.required' => __('Plan ID cannot be empty'), - 'period.required' => __('Plan period cannot be empty'), - 'period.in' => __('Wrong plan period') - ]; - } -} diff --git a/Xboard/app/Http/Requests/User/TicketSave.php b/Xboard/app/Http/Requests/User/TicketSave.php deleted file mode 100644 index 412778f..0000000 --- a/Xboard/app/Http/Requests/User/TicketSave.php +++ /dev/null @@ -1,32 +0,0 @@ - 'required', - 'level' => 'required|in:0,1,2', - 'message' => 'required' - ]; - } - - public function messages() - { - return [ - 'subject.required' => __('Ticket subject cannot be empty'), - 'level.required' => __('Ticket level cannot be empty'), - 'level.in' => __('Incorrect ticket level format'), - 'message.required' => __('Message cannot be empty') - ]; - } -} diff --git a/Xboard/app/Http/Requests/User/TicketWithdraw.php b/Xboard/app/Http/Requests/User/TicketWithdraw.php deleted file mode 100644 index d0da905..0000000 --- a/Xboard/app/Http/Requests/User/TicketWithdraw.php +++ /dev/null @@ -1,29 +0,0 @@ - 'required', - 'withdraw_account' => 'required' - ]; - } - - public function messages() - { - return [ - 'withdraw_method.required' => __('The withdrawal method cannot be empty'), - 'withdraw_account.required' => __('The withdrawal account cannot be empty') - ]; - } -} diff --git a/Xboard/app/Http/Requests/User/UserChangePassword.php b/Xboard/app/Http/Requests/User/UserChangePassword.php deleted file mode 100644 index 04e70c7..0000000 --- a/Xboard/app/Http/Requests/User/UserChangePassword.php +++ /dev/null @@ -1,30 +0,0 @@ - 'required', - 'new_password' => 'required|min:8' - ]; - } - - public function messages() - { - return [ - 'old_password.required' => __('Old password cannot be empty'), - 'new_password.required' => __('New password cannot be empty'), - 'new_password.min' => __('Password must be greater than 8 digits') - ]; - } -} diff --git a/Xboard/app/Http/Requests/User/UserTransfer.php b/Xboard/app/Http/Requests/User/UserTransfer.php deleted file mode 100644 index 478c825..0000000 --- a/Xboard/app/Http/Requests/User/UserTransfer.php +++ /dev/null @@ -1,29 +0,0 @@ - 'required|integer|min:1' - ]; - } - - public function messages() - { - return [ - 'transfer_amount.required' => __('The transfer amount cannot be empty'), - 'transfer_amount.integer' => __('The transfer amount parameter is wrong'), - 'transfer_amount.min' => __('The transfer amount parameter is wrong') - ]; - } -} diff --git a/Xboard/app/Http/Requests/User/UserUpdate.php b/Xboard/app/Http/Requests/User/UserUpdate.php deleted file mode 100644 index 5ba6604..0000000 --- a/Xboard/app/Http/Requests/User/UserUpdate.php +++ /dev/null @@ -1,29 +0,0 @@ - 'in:0,1', - 'remind_traffic' => 'in:0,1' - ]; - } - - public function messages() - { - return [ - 'show.in' => __('Incorrect format of expiration reminder'), - 'renew.in' => __('Incorrect traffic alert format') - ]; - } -} diff --git a/Xboard/app/Http/Resources/ComissionLogResource.php b/Xboard/app/Http/Resources/ComissionLogResource.php deleted file mode 100644 index 8d86769..0000000 --- a/Xboard/app/Http/Resources/ComissionLogResource.php +++ /dev/null @@ -1,25 +0,0 @@ - - */ - public function toArray(Request $request): array - { - return [ - "id"=> $this['id'], - "order_amount" => $this['order_amount'], - "trade_no" => $this['trade_no'], - "get_amount" => $this['get_amount'], - "created_at" => $this['created_at'] - ]; - } -} diff --git a/Xboard/app/Http/Resources/CouponResource.php b/Xboard/app/Http/Resources/CouponResource.php deleted file mode 100644 index 049a61f..0000000 --- a/Xboard/app/Http/Resources/CouponResource.php +++ /dev/null @@ -1,38 +0,0 @@ - 转换后的数组 - */ - public function toArray(Request $request): array - { - return [ - ...$this->resource->toArray(), - 'limit_plan_ids' => empty($this->limit_plan_ids) ? null : collect($this->limit_plan_ids) - ->map(fn(mixed $id): string => (string) $id) - ->values() - ->all(), - 'limit_period' => empty($this->limit_period) ? null : collect($this->limit_period) - ->map(fn(mixed $period): string => (string) PlanService::convertToLegacyPeriod($period)) - ->values() - ->all(), - ]; - } -} diff --git a/Xboard/app/Http/Resources/InviteCodeResource.php b/Xboard/app/Http/Resources/InviteCodeResource.php deleted file mode 100644 index 927c782..0000000 --- a/Xboard/app/Http/Resources/InviteCodeResource.php +++ /dev/null @@ -1,28 +0,0 @@ - - */ - public function toArray(Request $request): array - { - $data = [ - "user_id" => $this['user_id'], - "code" => $this['code'], - "pv" => $this['pv'], - "status" => $this['status'], - "created_at" => $this['created_at'], - "updated_at" => $this['updated_at'] - ]; - if(!config('hidden_features.enable_exposed_user_count_fix')) $data['user_id']= $this['user_id']; - return $data; - } -} diff --git a/Xboard/app/Http/Resources/KnowledgeResource.php b/Xboard/app/Http/Resources/KnowledgeResource.php deleted file mode 100644 index cda796c..0000000 --- a/Xboard/app/Http/Resources/KnowledgeResource.php +++ /dev/null @@ -1,28 +0,0 @@ - - */ - public function toArray(Request $request): array - { - $data = [ - 'id' => $this['id'], - 'category' => $this['category'], - 'title' => $this['title'], - 'body' => $this->when(isset($this['body']), $this['body']), - 'updated_at' => $this['updated_at'], - ]; - - return HookManager::filter('user.knowledge.resource', $data, $request, $this); - } -} diff --git a/Xboard/app/Http/Resources/MessageResource.php b/Xboard/app/Http/Resources/MessageResource.php deleted file mode 100644 index 9a72e1c..0000000 --- a/Xboard/app/Http/Resources/MessageResource.php +++ /dev/null @@ -1,26 +0,0 @@ - - */ - public function toArray(Request $request): array - { - return [ - "id" => $this['id'], - "ticket_id" => $this['ticket_id'], - "is_me" => $this['is_from_user'], - "message" => $this["message"], - "created_at" => $this['created_at'], - "updated_at" => $this['updated_at'] - ]; - } -} diff --git a/Xboard/app/Http/Resources/NodeResource.php b/Xboard/app/Http/Resources/NodeResource.php deleted file mode 100644 index d7f90af..0000000 --- a/Xboard/app/Http/Resources/NodeResource.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ - public function toArray(Request $request): array - { - return [ - 'id' => $this['id'], - 'type' => $this['type'], - 'version' => $this['version'] ?? null, - 'name' => $this['name'], - 'rate' => $this['rate'], - 'tags' => $this['tags'], - 'is_online' => $this['is_online'], - 'cache_key' => $this['cache_key'], - 'last_check_at' => $this['last_check_at'] - ]; - } -} diff --git a/Xboard/app/Http/Resources/OrderResource.php b/Xboard/app/Http/Resources/OrderResource.php deleted file mode 100644 index ae3e6e4..0000000 --- a/Xboard/app/Http/Resources/OrderResource.php +++ /dev/null @@ -1,28 +0,0 @@ - - */ - public function toArray(Request $request): array - { - return [ - ...parent::toArray($request), - 'period' => PlanService::getLegacyPeriod((string)$this->period), - 'plan' => $this->whenLoaded('plan', fn() => PlanResource::make($this->plan)), - ]; - } -} diff --git a/Xboard/app/Http/Resources/PlanResource.php b/Xboard/app/Http/Resources/PlanResource.php deleted file mode 100644 index b78583a..0000000 --- a/Xboard/app/Http/Resources/PlanResource.php +++ /dev/null @@ -1,122 +0,0 @@ - - */ - public function toArray(Request $request): array - { - return [ - 'id' => $this->resource['id'], - 'group_id' => $this->resource['group_id'], - 'name' => $this->resource['name'], - 'tags' => $this->resource['tags'], - 'content' => $this->formatContent(), - ...$this->getPeriodPrices(), - 'capacity_limit' => $this->getFormattedCapacityLimit(), - 'transfer_enable' => $this->resource['transfer_enable'], - 'speed_limit' => $this->resource['speed_limit'], - 'device_limit' => $this->resource['device_limit'], - 'show' => (bool) $this->resource['show'], - 'sell' => (bool) $this->resource['sell'], - 'renew' => (bool) $this->resource['renew'], - 'reset_traffic_method' => $this->resource['reset_traffic_method'], - 'sort' => $this->resource['sort'], - 'created_at' => $this->resource['created_at'], - 'updated_at' => $this->resource['updated_at'] - ]; - } - - /** - * Get transformed period prices using Plan mapping - * - * @return array - */ - protected function getPeriodPrices(): array - { - return collect(Plan::LEGACY_PERIOD_MAPPING) - ->mapWithKeys(function (string $newPeriod, string $legacyPeriod): array { - $price = $this->resource['prices'][$newPeriod] ?? null; - return [ - $legacyPeriod => $price !== null - ? (float) $price * self::PRICE_MULTIPLIER - : null - ]; - }) - ->all(); - } - - /** - * Get formatted capacity limit value - * - * @return int|string|null - */ - protected function getFormattedCapacityLimit(): int|string|null - { - $limit = $this->resource['capacity_limit']; - - return match (true) { - $limit === null => null, - $limit <= 0 => __('Sold out'), - default => (int) $limit, - }; - } - - /** - * Format content with template variables - * - * @return string - */ - protected function formatContent(): string - { - $content = $this->resource['content'] ?? ''; - - $replacements = [ - '{{transfer}}' => $this->resource['transfer_enable'], - '{{speed}}' => $this->resource['speed_limit'] === NULL ? __('No Limit') : $this->resource['speed_limit'], - '{{devices}}' => $this->resource['device_limit'] === NULL ? __('No Limit') : $this->resource['device_limit'], - '{{reset_method}}' => $this->getResetMethodText(), - ]; - - return str_replace( - array_keys($replacements), - array_values($replacements), - $content - ); - } - - /** - * Get reset method text - * - * @return string - */ - protected function getResetMethodText(): string - { - $method = $this->resource['reset_traffic_method']; - - if ($method === Plan::RESET_TRAFFIC_FOLLOW_SYSTEM) { - $method = admin_setting('reset_traffic_method', Plan::RESET_TRAFFIC_MONTHLY); - } - return match ($method) { - Plan::RESET_TRAFFIC_FIRST_DAY_MONTH => __('First Day of Month'), - Plan::RESET_TRAFFIC_MONTHLY => __('Monthly'), - Plan::RESET_TRAFFIC_NEVER => __('Never'), - Plan::RESET_TRAFFIC_FIRST_DAY_YEAR => __('First Day of Year'), - Plan::RESET_TRAFFIC_YEARLY => __('Yearly'), - default => __('Monthly') - }; - } -} \ No newline at end of file diff --git a/Xboard/app/Http/Resources/TicketResource.php b/Xboard/app/Http/Resources/TicketResource.php deleted file mode 100644 index 9b9a773..0000000 --- a/Xboard/app/Http/Resources/TicketResource.php +++ /dev/null @@ -1,31 +0,0 @@ - - */ - public function toArray(Request $request): array - { - $data = [ - "id" => $this['id'], - "level" => $this['level'], - "reply_status" => $this['reply_status'], - "status" => $this['status'], - "subject" => $this['subject'], - "message" => array_key_exists('message',$this->additional) ? MessageResource::collection($this['message']) : null, - "created_at" => $this['created_at'], - "updated_at" => $this['updated_at'] - ]; - if(!config('hidden_features.enable_exposed_user_count_fix')) $data['user_id']= $this['user_id']; - return $data; - - } -} diff --git a/Xboard/app/Http/Resources/TrafficLogResource.php b/Xboard/app/Http/Resources/TrafficLogResource.php deleted file mode 100644 index 798ea7e..0000000 --- a/Xboard/app/Http/Resources/TrafficLogResource.php +++ /dev/null @@ -1,26 +0,0 @@ - - */ - public function toArray(Request $request): array - { - $data = [ - "d" => $this['d'], - "u" => $this['u'], - "record_at" => $this['record_at'], - "server_rate" => $this['server_rate'], - ]; - if(!config('hidden_features.enable_exposed_user_count_fix')) $data['user_id']= $this['user_id']; - return $data; - } -} diff --git a/Xboard/app/Http/Routes/V1/ClientRoute.php b/Xboard/app/Http/Routes/V1/ClientRoute.php deleted file mode 100644 index ad13989..0000000 --- a/Xboard/app/Http/Routes/V1/ClientRoute.php +++ /dev/null @@ -1,23 +0,0 @@ -group([ - 'prefix' => 'client', - 'middleware' => 'client' - ], function ($router) { - // Client - $router->get('/subscribe', [ClientController::class, 'subscribe'])->name('client.subscribe.legacy'); - // App - $router->get('/app/getConfig', [AppController::class, 'getConfig']); - $router->get('/app/getVersion', [AppController::class, 'getVersion']); - }); - } -} diff --git a/Xboard/app/Http/Routes/V1/GuestRoute.php b/Xboard/app/Http/Routes/V1/GuestRoute.php deleted file mode 100644 index 3c4f571..0000000 --- a/Xboard/app/Http/Routes/V1/GuestRoute.php +++ /dev/null @@ -1,27 +0,0 @@ -group([ - 'prefix' => 'guest' - ], function ($router) { - // Plan - $router->get('/plan/fetch', [PlanController::class, 'fetch']); - // Telegram - $router->post('/telegram/webhook', [TelegramController::class, 'webhook']); - // Payment - $router->match(['get', 'post'], '/payment/notify/{method}/{uuid}', [PaymentController::class, 'notify']); - // Comm - $router->get('/comm/config', [CommController::class, 'config']); - }); - } -} diff --git a/Xboard/app/Http/Routes/V1/PassportRoute.php b/Xboard/app/Http/Routes/V1/PassportRoute.php deleted file mode 100644 index 3134b96..0000000 --- a/Xboard/app/Http/Routes/V1/PassportRoute.php +++ /dev/null @@ -1,27 +0,0 @@ -group([ - 'prefix' => 'passport' - ], function ($router) { - // Auth - $router->post('/auth/register', [AuthController::class, 'register']); - $router->post('/auth/login', [AuthController::class, 'login']); - $router->get('/auth/token2Login', [AuthController::class, 'token2Login']); - $router->post('/auth/forget', [AuthController::class, 'forget']); - $router->post('/auth/getQuickLoginUrl', [AuthController::class, 'getQuickLoginUrl']); - $router->post('/auth/loginWithMailLink', [AuthController::class, 'loginWithMailLink']); - // Comm - $router->post('/comm/sendEmailVerify', [CommController::class, 'sendEmailVerify']); - $router->post('/comm/pv', [CommController::class, 'pv']); - }); - } -} diff --git a/Xboard/app/Http/Routes/V1/ServerRoute.php b/Xboard/app/Http/Routes/V1/ServerRoute.php deleted file mode 100644 index 42f7b22..0000000 --- a/Xboard/app/Http/Routes/V1/ServerRoute.php +++ /dev/null @@ -1,45 +0,0 @@ -group([ - 'prefix' => 'server', - ], function ($router) { - $router->group([ - 'prefix' => 'UniProxy', - 'middleware' => 'server' - ], function ($route) { - $route->get('config', [UniProxyController::class, 'config']); - $route->get('user', [UniProxyController::class, 'user']); - $route->post('push', [UniProxyController::class, 'push']); - $route->post('alive', [UniProxyController::class, 'alive']); - $route->get('alivelist', [UniProxyController::class, 'alivelist']); - $route->post('status', [UniProxyController::class, 'status']); - }); - $router->group([ - 'prefix' => 'ShadowsocksTidalab', - 'middleware' => 'server:shadowsocks' - ], function ($route) { - $route->get('user', [ShadowsocksTidalabController::class, 'user']); - $route->post('submit', [ShadowsocksTidalabController::class, 'submit']); - }); - $router->group([ - 'prefix' => 'TrojanTidalab', - 'middleware' => 'server:trojan' - ], function ($route) { - $route->get('config', [TrojanTidalabController::class, 'config']); - $route->get('user', [TrojanTidalabController::class, 'user']); - $route->post('submit', [TrojanTidalabController::class, 'submit']); - }); - }); - } -} diff --git a/Xboard/app/Http/Routes/V1/UserRoute.php b/Xboard/app/Http/Routes/V1/UserRoute.php deleted file mode 100644 index 92c3605..0000000 --- a/Xboard/app/Http/Routes/V1/UserRoute.php +++ /dev/null @@ -1,83 +0,0 @@ -group([ - 'prefix' => 'user', - 'middleware' => 'user' - ], function ($router) { - // User - $router->get('/resetSecurity', [UserController::class, 'resetSecurity']); - $router->get('/info', [UserController::class, 'info']); - $router->post('/changePassword', [UserController::class, 'changePassword']); - $router->post('/update', [UserController::class, 'update']); - $router->get('/getSubscribe', [UserController::class, 'getSubscribe']); - $router->get('/getStat', [UserController::class, 'getStat']); - $router->get('/checkLogin', [UserController::class, 'checkLogin']); - $router->post('/transfer', [UserController::class, 'transfer']); - $router->post('/getQuickLoginUrl', [UserController::class, 'getQuickLoginUrl']); - $router->get('/getActiveSession', [UserController::class, 'getActiveSession']); - $router->post('/removeActiveSession', [UserController::class, 'removeActiveSession']); - // Order - $router->post('/order/save', [OrderController::class, 'save']); - $router->post('/order/checkout', [OrderController::class, 'checkout']); - $router->get('/order/check', [OrderController::class, 'check']); - $router->get('/order/detail', [OrderController::class, 'detail']); - $router->get('/order/fetch', [OrderController::class, 'fetch']); - $router->get('/order/getPaymentMethod', [OrderController::class, 'getPaymentMethod']); - $router->post('/order/cancel', [OrderController::class, 'cancel']); - // Plan - $router->get('/plan/fetch', [PlanController::class, 'fetch']); - // Invite - $router->get('/invite/save', [InviteController::class, 'save']); - $router->get('/invite/fetch', [InviteController::class, 'fetch']); - $router->get('/invite/details', [InviteController::class, 'details']); - // Notice - $router->get('/notice/fetch', [NoticeController::class, 'fetch']); - // Ticket - $router->post('/ticket/reply', [TicketController::class, 'reply']); - $router->post('/ticket/close', [TicketController::class, 'close']); - $router->post('/ticket/save', [TicketController::class, 'save']); - $router->get('/ticket/fetch', [TicketController::class, 'fetch']); - $router->post('/ticket/withdraw', [TicketController::class, 'withdraw']); - // Server - $router->get('/server/fetch', [ServerController::class, 'fetch']); - // Coupon - $router->post('/coupon/check', [CouponController::class, 'check']); - // Gift Card - $router->post('/gift-card/check', [GiftCardController::class, 'check']); - $router->post('/gift-card/redeem', [GiftCardController::class, 'redeem']); - $router->get('/gift-card/history', [GiftCardController::class, 'history']); - $router->get('/gift-card/detail', [GiftCardController::class, 'detail']); - $router->get('/gift-card/types', [GiftCardController::class, 'types']); - // Telegram - $router->get('/telegram/getBotInfo', [TelegramController::class, 'getBotInfo']); - // Comm - $router->get('/comm/config', [CommController::class, 'config']); - $router->Post('/comm/getStripePublicKey', [CommController::class, 'getStripePublicKey']); - // Knowledge - $router->get('/knowledge/fetch', [KnowledgeController::class, 'fetch']); - $router->get('/knowledge/getCategory', [KnowledgeController::class, 'getCategory']); - // Stat - $router->get('/stat/getTrafficLog', [StatController::class, 'getTrafficLog']); - }); - } -} diff --git a/Xboard/app/Http/Routes/V2/AdminRoute.php b/Xboard/app/Http/Routes/V2/AdminRoute.php deleted file mode 100644 index 3d8c910..0000000 --- a/Xboard/app/Http/Routes/V2/AdminRoute.php +++ /dev/null @@ -1,276 +0,0 @@ -group([ - 'prefix' => admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key')))), - 'middleware' => ['admin', 'log'], - ], function ($router) { - // Config - $router->group([ - 'prefix' => 'config' - ], function ($router) { - $router->get('/fetch', [ConfigController::class, 'fetch']); - $router->post('/save', [ConfigController::class, 'save']); - $router->get('/getEmailTemplate', [ConfigController::class, 'getEmailTemplate']); - $router->get('/getThemeTemplate', [ConfigController::class, 'getThemeTemplate']); - $router->post('/setTelegramWebhook', [ConfigController::class, 'setTelegramWebhook']); - $router->post('/testSendMail', [ConfigController::class, 'testSendMail']); - }); - - // Plan - $router->group([ - 'prefix' => 'plan' - ], function ($router) { - $router->get('/fetch', [PlanController::class, 'fetch']); - $router->post('/save', [PlanController::class, 'save']); - $router->post('/drop', [PlanController::class, 'drop']); - $router->post('/update', [PlanController::class, 'update']); - $router->post('/sort', [PlanController::class, 'sort']); - }); - - // Server - $router->group([ - 'prefix' => 'server/group' - ], function ($router) { - $router->get('/fetch', [GroupController::class, 'fetch']); - $router->post('/save', [GroupController::class, 'save']); - $router->post('/drop', [GroupController::class, 'drop']); - }); - $router->group([ - 'prefix' => 'server/route' - ], function ($router) { - $router->get('/fetch', [RouteController::class, 'fetch']); - $router->post('/save', [RouteController::class, 'save']); - $router->post('/drop', [RouteController::class, 'drop']); - }); - $router->group([ - 'prefix' => 'server/manage' - ], function ($router) { - $router->get('/getNodes', [ManageController::class, 'getNodes']); - $router->post('/sort', [ManageController::class, 'sort']); - }); - - // 节点更新接口 - $router->group([ - 'prefix' => 'server/manage' - ], function ($router) { - $router->post('/update', [ManageController::class, 'update']); - $router->post('/save', [ManageController::class, 'save']); - $router->post('/drop', [ManageController::class, 'drop']); - $router->post('/copy', [ManageController::class, 'copy']); - $router->post('/sort', [ManageController::class, 'sort']); - $router->post('/batchDelete', [ManageController::class, 'batchDelete']); - $router->post('/resetTraffic', [ManageController::class, 'resetTraffic']); - $router->post('/batchResetTraffic', [ManageController::class, 'batchResetTraffic']); - }); - - // Order - $router->group([ - 'prefix' => 'order' - ], function ($router) { - $router->any('/fetch', [OrderController::class, 'fetch']); - $router->post('/update', [OrderController::class, 'update']); - $router->post('/assign', [OrderController::class, 'assign']); - $router->post('/paid', [OrderController::class, 'paid']); - $router->post('/cancel', [OrderController::class, 'cancel']); - $router->post('/detail', [OrderController::class, 'detail']); - }); - - // User - $router->group([ - 'prefix' => 'user' - ], function ($router) { - $router->any('/fetch', [UserController::class, 'fetch']); - $router->post('/update', [UserController::class, 'update']); - $router->get('/getUserInfoById', [UserController::class, 'getUserInfoById']); - $router->post('/generate', [UserController::class, 'generate']); - $router->post('/dumpCSV', [UserController::class, 'dumpCSV']); - $router->post('/sendMail', [UserController::class, 'sendMail']); - $router->post('/ban', [UserController::class, 'ban']); - $router->post('/resetSecret', [UserController::class, 'resetSecret']); - $router->post('/setInviteUser', [UserController::class, 'setInviteUser']); - $router->post('/destroy', [UserController::class, 'destroy']); - }); - - // Stat - $router->group([ - 'prefix' => 'stat' - ], function ($router) { - $router->get('/getOverride', [StatController::class, 'getOverride']); - $router->get('/getStats', [StatController::class, 'getStats']); - $router->get('/getServerLastRank', [StatController::class, 'getServerLastRank']); - $router->get('/getServerYesterdayRank', [StatController::class, 'getServerYesterdayRank']); - $router->get('/getOrder', [StatController::class, 'getOrder']); - $router->any('/getStatUser', [StatController::class, 'getStatUser']); - $router->get('/getRanking', [StatController::class, 'getRanking']); - $router->get('/getStatRecord', [StatController::class, 'getStatRecord']); - $router->get('/getTrafficRank', [StatController::class, 'getTrafficRank']); - }); - - // Notice - $router->group([ - 'prefix' => 'notice' - ], function ($router) { - $router->get('/fetch', [NoticeController::class, 'fetch']); - $router->post('/save', [NoticeController::class, 'save']); - $router->post('/update', [NoticeController::class, 'update']); - $router->post('/drop', [NoticeController::class, 'drop']); - $router->post('/show', [NoticeController::class, 'show']); - $router->post('/sort', [NoticeController::class, 'sort']); - }); - - // Ticket - $router->group([ - 'prefix' => 'ticket' - ], function ($router) { - $router->any('/fetch', [TicketController::class, 'fetch']); - $router->post('/reply', [TicketController::class, 'reply']); - $router->post('/close', [TicketController::class, 'close']); - }); - - // Coupon - $router->group([ - 'prefix' => 'coupon' - ], function ($router) { - $router->any('/fetch', [CouponController::class, 'fetch']); - $router->post('/generate', [CouponController::class, 'generate']); - $router->post('/drop', [CouponController::class, 'drop']); - $router->post('/show', [CouponController::class, 'show']); - $router->post('/update', [CouponController::class, 'update']); - }); - - // Gift Card - $router->group([ - 'prefix' => 'gift-card' - ], function ($router) { - // Template management - $router->any('/templates', [GiftCardController::class, 'templates']); - $router->post('/create-template', [GiftCardController::class, 'createTemplate']); - $router->post('/update-template', [GiftCardController::class, 'updateTemplate']); - $router->post('/delete-template', [GiftCardController::class, 'deleteTemplate']); - - // Code management - $router->post('/generate-codes', [GiftCardController::class, 'generateCodes']); - $router->any('/codes', [GiftCardController::class, 'codes']); - $router->post('/toggle-code', [GiftCardController::class, 'toggleCode']); - $router->get('/export-codes', [GiftCardController::class, 'exportCodes']); - $router->post('/update-code', [GiftCardController::class, 'updateCode']); - $router->post('/delete-code', [GiftCardController::class, 'deleteCode']); - - // Usage records - $router->any('/usages', [GiftCardController::class, 'usages']); - - // Statistics - $router->any('/statistics', [GiftCardController::class, 'statistics']); - $router->get('/types', [GiftCardController::class, 'types']); - }); - - // Knowledge - $router->group([ - 'prefix' => 'knowledge' - ], function ($router) { - $router->get('/fetch', [KnowledgeController::class, 'fetch']); - $router->get('/getCategory', [KnowledgeController::class, 'getCategory']); - $router->post('/save', [KnowledgeController::class, 'save']); - $router->post('/show', [KnowledgeController::class, 'show']); - $router->post('/drop', [KnowledgeController::class, 'drop']); - $router->post('/sort', [KnowledgeController::class, 'sort']); - }); - - // Payment - $router->group([ - 'prefix' => 'payment' - ], function ($router) { - $router->get('/fetch', [PaymentController::class, 'fetch']); - $router->get('/getPaymentMethods', [PaymentController::class, 'getPaymentMethods']); - $router->post('/getPaymentForm', [PaymentController::class, 'getPaymentForm']); - $router->post('/save', [PaymentController::class, 'save']); - $router->post('/drop', [PaymentController::class, 'drop']); - $router->post('/show', [PaymentController::class, 'show']); - $router->post('/sort', [PaymentController::class, 'sort']); - }); - - // System - $router->group([ - 'prefix' => 'system' - ], function ($router) { - $router->get('/getSystemStatus', [SystemController::class, 'getSystemStatus']); - $router->get('/getQueueStats', [SystemController::class, 'getQueueStats']); - $router->get('/getQueueWorkload', [SystemController::class, 'getQueueWorkload']); - $router->get('/getQueueMasters', '\\Laravel\\Horizon\\Http\\Controllers\\MasterSupervisorController@index'); - $router->get('/getHorizonFailedJobs', [SystemController::class, 'getHorizonFailedJobs']); - $router->any('/getAuditLog', [SystemController::class, 'getAuditLog']); - }); - - // Update - // $router->group([ - // 'prefix' => 'update' - // ], function ($router) { - // $router->get('/check', [UpdateController::class, 'checkUpdate']); - // $router->post('/execute', [UpdateController::class, 'executeUpdate']); - // }); - - // Theme - $router->group([ - 'prefix' => 'theme' - ], function ($router) { - $router->get('/getThemes', [ThemeController::class, 'getThemes']); - $router->post('/upload', [ThemeController::class, 'upload']); - $router->post('/delete', [ThemeController::class, 'delete']); - $router->post('/saveThemeConfig', [ThemeController::class, 'saveThemeConfig']); - $router->post('/getThemeConfig', [ThemeController::class, 'getThemeConfig']); - }); - - // Plugin - $router->group([ - 'prefix' => 'plugin' - ], function ($router) { - $router->get('/types', [\App\Http\Controllers\V2\Admin\PluginController::class, 'types']); - $router->get('/getPlugins', [\App\Http\Controllers\V2\Admin\PluginController::class, 'index']); - $router->post('/upload', [\App\Http\Controllers\V2\Admin\PluginController::class, 'upload']); - $router->post('/delete', [\App\Http\Controllers\V2\Admin\PluginController::class, 'delete']); - $router->post('install', [\App\Http\Controllers\V2\Admin\PluginController::class, 'install']); - $router->post('uninstall', [\App\Http\Controllers\V2\Admin\PluginController::class, 'uninstall']); - $router->post('enable', [\App\Http\Controllers\V2\Admin\PluginController::class, 'enable']); - $router->post('disable', [\App\Http\Controllers\V2\Admin\PluginController::class, 'disable']); - $router->get('config', [\App\Http\Controllers\V2\Admin\PluginController::class, 'getConfig']); - $router->post('config', [\App\Http\Controllers\V2\Admin\PluginController::class, 'updateConfig']); - $router->post('upgrade', [\App\Http\Controllers\V2\Admin\PluginController::class, 'upgrade']); - }); - - // 流量重置管理 - $router->group([ - 'prefix' => 'traffic-reset' - ], function ($router) { - $router->get('logs', [TrafficResetController::class, 'logs']); - $router->get('stats', [TrafficResetController::class, 'stats']); - $router->get('user/{userId}/history', [TrafficResetController::class, 'userHistory']); - $router->post('reset-user', [TrafficResetController::class, 'resetUser']); - }); - }); - - } -} diff --git a/Xboard/app/Http/Routes/V2/ClientRoute.php b/Xboard/app/Http/Routes/V2/ClientRoute.php deleted file mode 100644 index 693a40d..0000000 --- a/Xboard/app/Http/Routes/V2/ClientRoute.php +++ /dev/null @@ -1,20 +0,0 @@ -group([ - 'prefix' => 'client', - 'middleware' => 'client' - ], function ($router) { - // App - $router->get('/app/getConfig', [AppController::class, 'getConfig']); - $router->get('/app/getVersion', [AppController::class, 'getVersion']); - }); - } -} diff --git a/Xboard/app/Http/Routes/V2/PassportRoute.php b/Xboard/app/Http/Routes/V2/PassportRoute.php deleted file mode 100644 index ed91d81..0000000 --- a/Xboard/app/Http/Routes/V2/PassportRoute.php +++ /dev/null @@ -1,27 +0,0 @@ -group([ - 'prefix' => 'passport' - ], function ($router) { - // Auth - $router->post('/auth/register', [AuthController::class, 'register']); - $router->post('/auth/login', [AuthController::class, 'login']); - $router->get ('/auth/token2Login', [AuthController::class, 'token2Login']); - $router->post('/auth/forget', [AuthController::class, 'forget']); - $router->post('/auth/getQuickLoginUrl', [AuthController::class, 'getQuickLoginUrl']); - $router->post('/auth/loginWithMailLink', [AuthController::class, 'loginWithMailLink']); - // Comm - $router->post('/comm/sendEmailVerify', [CommController::class, 'sendEmailVerify']); - $router->post('/comm/pv', [CommController::class, 'pv']); - }); - } -} diff --git a/Xboard/app/Http/Routes/V2/ServerRoute.php b/Xboard/app/Http/Routes/V2/ServerRoute.php deleted file mode 100644 index 9742d31..0000000 --- a/Xboard/app/Http/Routes/V2/ServerRoute.php +++ /dev/null @@ -1,29 +0,0 @@ -group([ - 'prefix' => 'server', - 'middleware' => 'server' - ], function ($route) { - $route->post('handshake', [ServerController::class, 'handshake']); - $route->post('report', [ServerController::class, 'report']); - $route->get('config', [UniProxyController::class, 'config']); - $route->get('user', [UniProxyController::class, 'user']); - $route->post('push', [UniProxyController::class, 'push']); - $route->post('alive', [UniProxyController::class, 'alive']); - $route->get('alivelist', [UniProxyController::class, 'alivelist']); - $route->post('status', [UniProxyController::class, 'status']); - }); - } -} diff --git a/Xboard/app/Http/Routes/V2/UserRoute.php b/Xboard/app/Http/Routes/V2/UserRoute.php deleted file mode 100644 index 38bc12f..0000000 --- a/Xboard/app/Http/Routes/V2/UserRoute.php +++ /dev/null @@ -1,20 +0,0 @@ -group([ - 'prefix' => 'user', - 'middleware' => 'user' - ], function ($router) { - // User - $router->get('/resetSecurity', [UserController::class, 'resetSecurity']); - $router->get('/info', [UserController::class, 'info']); - }); - } -} diff --git a/Xboard/app/Jobs/NodeUserSyncJob.php b/Xboard/app/Jobs/NodeUserSyncJob.php deleted file mode 100644 index f49e108..0000000 --- a/Xboard/app/Jobs/NodeUserSyncJob.php +++ /dev/null @@ -1,45 +0,0 @@ -onQueue('node_sync'); - } - - public function handle(): void - { - $user = User::find($this->userId); - - if ($this->action === 'updated' || $this->action === 'created') { - if ($this->oldGroupId) { - NodeSyncService::notifyUserRemovedFromGroup($this->userId, $this->oldGroupId); - } - if ($user) { - NodeSyncService::notifyUserChanged($user); - } - } elseif ($this->action === 'deleted') { - if ($this->oldGroupId) { - NodeSyncService::notifyUserRemovedFromGroup($this->userId, $this->oldGroupId); - } - } - } -} diff --git a/Xboard/app/Jobs/OrderHandleJob.php b/Xboard/app/Jobs/OrderHandleJob.php deleted file mode 100644 index 960f659..0000000 --- a/Xboard/app/Jobs/OrderHandleJob.php +++ /dev/null @@ -1,56 +0,0 @@ -onQueue('order_handle'); - $this->tradeNo = $tradeNo; - } - - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - $order = Order::where('trade_no', $this->tradeNo) - ->lockForUpdate() - ->first(); - if (!$order) return; - $orderService = new OrderService($order); - switch ($order->status) { - // cancel - case Order::STATUS_PENDING: - if ($order->created_at <= (time() - 3600 * 2)) { - $orderService->cancel(); - } - break; - case Order::STATUS_PROCESSING: - $orderService->open(); - break; - } - } -} diff --git a/Xboard/app/Jobs/SendEmailJob.php b/Xboard/app/Jobs/SendEmailJob.php deleted file mode 100644 index d998f41..0000000 --- a/Xboard/app/Jobs/SendEmailJob.php +++ /dev/null @@ -1,42 +0,0 @@ -onQueue($queue); - $this->params = $params; - } - - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - $mailLog = MailService::sendEmail($this->params); - if ($mailLog['error']) { - $this->release(); //发送失败将触发重试 - } - } -} diff --git a/Xboard/app/Jobs/SendTelegramJob.php b/Xboard/app/Jobs/SendTelegramJob.php deleted file mode 100644 index 8d30344..0000000 --- a/Xboard/app/Jobs/SendTelegramJob.php +++ /dev/null @@ -1,43 +0,0 @@ -onQueue('send_telegram'); - $this->telegramId = $telegramId; - $this->text = $text; - } - - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - $telegramService = new TelegramService(); - $telegramService->sendMessage($this->telegramId, $this->text, 'markdown'); - } -} diff --git a/Xboard/app/Jobs/StatServerJob.php b/Xboard/app/Jobs/StatServerJob.php deleted file mode 100644 index c055ca5..0000000 --- a/Xboard/app/Jobs/StatServerJob.php +++ /dev/null @@ -1,174 +0,0 @@ -onQueue('stat'); - $this->data = $data; - $this->server = $server; - $this->protocol = $protocol; - $this->recordType = $recordType; - } - - public function handle(): void - { - $recordAt = $this->recordType === 'm' - ? strtotime(date('Y-m-01')) - : strtotime(date('Y-m-d')); - - $u = $d = 0; - foreach ($this->data as $traffic) { - $u += $traffic[0]; - $d += $traffic[1]; - } - - try { - $this->processServerStat($u, $d, $recordAt); - $this->updateServerTraffic($u, $d); - } catch (\Exception $e) { - Log::error('StatServerJob failed for server ' . $this->server['id'] . ': ' . $e->getMessage()); - throw $e; - } - } - - protected function updateServerTraffic(int $u, int $d): void - { - DB::table('v2_server') - ->where('id', $this->server['id']) - ->incrementEach( - ['u' => $u, 'd' => $d], - ['updated_at' => Carbon::now()] - ); - } - - protected function processServerStat(int $u, int $d, int $recordAt): void - { - $driver = config('database.default'); - if ($driver === 'sqlite') { - $this->processServerStatForSqlite($u, $d, $recordAt); - } elseif ($driver === 'pgsql') { - $this->processServerStatForPostgres($u, $d, $recordAt); - } else { - $this->processServerStatForOtherDatabases($u, $d, $recordAt); - } - } - - protected function processServerStatForSqlite(int $u, int $d, int $recordAt): void - { - DB::transaction(function () use ($u, $d, $recordAt) { - $existingRecord = StatServer::where([ - 'record_at' => $recordAt, - 'server_id' => $this->server['id'], - 'server_type' => $this->protocol, - 'record_type' => $this->recordType, - ])->first(); - - if ($existingRecord) { - $existingRecord->update([ - 'u' => $existingRecord->u + $u, - 'd' => $existingRecord->d + $d, - 'updated_at' => time(), - ]); - } else { - StatServer::create([ - 'record_at' => $recordAt, - 'server_id' => $this->server['id'], - 'server_type' => $this->protocol, - 'record_type' => $this->recordType, - 'u' => $u, - 'd' => $d, - 'created_at' => time(), - 'updated_at' => time(), - ]); - } - }, 3); - } - - protected function processServerStatForOtherDatabases(int $u, int $d, int $recordAt): void - { - StatServer::upsert( - [ - 'record_at' => $recordAt, - 'server_id' => $this->server['id'], - 'server_type' => $this->protocol, - 'record_type' => $this->recordType, - 'u' => $u, - 'd' => $d, - 'created_at' => time(), - 'updated_at' => time(), - ], - ['server_id', 'server_type', 'record_at', 'record_type'], - [ - 'u' => DB::raw("u + VALUES(u)"), - 'd' => DB::raw("d + VALUES(d)"), - 'updated_at' => time(), - ] - ); - } - - /** - * PostgreSQL upsert with arithmetic increments using ON CONFLICT ... DO UPDATE - */ - protected function processServerStatForPostgres(int $u, int $d, int $recordAt): void - { - $table = (new StatServer())->getTable(); - $now = time(); - - // Use parameter binding to avoid SQL injection and keep maintainability - $sql = "INSERT INTO {$table} (record_at, server_id, server_type, record_type, u, d, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT (server_id, server_type, record_at) - DO UPDATE SET - u = {$table}.u + EXCLUDED.u, - d = {$table}.d + EXCLUDED.d, - updated_at = EXCLUDED.updated_at"; - - DB::statement($sql, [ - $recordAt, - $this->server['id'], - $this->protocol, - $this->recordType, - $u, - $d, - $now, - $now, - ]); - } -} diff --git a/Xboard/app/Jobs/StatUserJob.php b/Xboard/app/Jobs/StatUserJob.php deleted file mode 100644 index 620443c..0000000 --- a/Xboard/app/Jobs/StatUserJob.php +++ /dev/null @@ -1,158 +0,0 @@ -onQueue('stat'); - $this->data = $data; - $this->server = $server; - $this->protocol = $protocol; - $this->recordType = $recordType; - } - - public function handle(): void - { - $recordAt = $this->recordType === 'm' - ? strtotime(date('Y-m-01')) - : strtotime(date('Y-m-d')); - - foreach ($this->data as $uid => $v) { - try { - $this->processUserStat($uid, $v, $recordAt); - } catch (\Exception $e) { - Log::error('StatUserJob failed for user ' . $uid . ': ' . $e->getMessage()); - throw $e; - } - } - } - - protected function processUserStat(int $uid, array $v, int $recordAt): void - { - $driver = config('database.default'); - if ($driver === 'sqlite') { - $this->processUserStatForSqlite($uid, $v, $recordAt); - } elseif ($driver === 'pgsql') { - $this->processUserStatForPostgres($uid, $v, $recordAt); - } else { - $this->processUserStatForOtherDatabases($uid, $v, $recordAt); - } - } - - protected function processUserStatForSqlite(int $uid, array $v, int $recordAt): void - { - DB::transaction(function () use ($uid, $v, $recordAt) { - $existingRecord = StatUser::where([ - 'user_id' => $uid, - 'server_rate' => $this->server['rate'], - 'record_at' => $recordAt, - 'record_type' => $this->recordType, - ])->first(); - - if ($existingRecord) { - $existingRecord->update([ - 'u' => $existingRecord->u + intval($v[0] * $this->server['rate']), - 'd' => $existingRecord->d + intval($v[1] * $this->server['rate']), - 'updated_at' => time(), - ]); - } else { - StatUser::create([ - 'user_id' => $uid, - 'server_rate' => $this->server['rate'], - 'record_at' => $recordAt, - 'record_type' => $this->recordType, - 'u' => intval($v[0] * $this->server['rate']), - 'd' => intval($v[1] * $this->server['rate']), - 'created_at' => time(), - 'updated_at' => time(), - ]); - } - }, 3); - } - - protected function processUserStatForOtherDatabases(int $uid, array $v, int $recordAt): void - { - StatUser::upsert( - [ - 'user_id' => $uid, - 'server_rate' => $this->server['rate'], - 'record_at' => $recordAt, - 'record_type' => $this->recordType, - 'u' => intval($v[0] * $this->server['rate']), - 'd' => intval($v[1] * $this->server['rate']), - 'created_at' => time(), - 'updated_at' => time(), - ], - ['user_id', 'server_rate', 'record_at', 'record_type'], - [ - 'u' => DB::raw("u + VALUES(u)"), - 'd' => DB::raw("d + VALUES(d)"), - 'updated_at' => time(), - ] - ); - } - - /** - * PostgreSQL upsert with arithmetic increments using ON CONFLICT ... DO UPDATE - */ - protected function processUserStatForPostgres(int $uid, array $v, int $recordAt): void - { - $table = (new StatUser())->getTable(); - $now = time(); - $u = intval($v[0] * $this->server['rate']); - $d = intval($v[1] * $this->server['rate']); - - $sql = "INSERT INTO {$table} (user_id, server_rate, record_at, record_type, u, d, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT (user_id, server_rate, record_at) - DO UPDATE SET - u = {$table}.u + EXCLUDED.u, - d = {$table}.d + EXCLUDED.d, - updated_at = EXCLUDED.updated_at"; - - DB::statement($sql, [ - $uid, - $this->server['rate'], - $recordAt, - $this->recordType, - $u, - $d, - $now, - $now, - ]); - } -} \ No newline at end of file diff --git a/Xboard/app/Jobs/TrafficFetchJob.php b/Xboard/app/Jobs/TrafficFetchJob.php deleted file mode 100644 index 1e398aa..0000000 --- a/Xboard/app/Jobs/TrafficFetchJob.php +++ /dev/null @@ -1,51 +0,0 @@ -onQueue('traffic_fetch'); - $this->server = $server; - $this->data = $data; - $this->protocol = $protocol; - $this->timestamp = $timestamp; - } - - public function handle(): void - { - $userIds = array_keys($this->data); - - foreach ($this->data as $uid => $v) { - User::where('id', $uid) - ->incrementEach( - [ - 'u' => $v[0] * $this->server['rate'], - 'd' => $v[1] * $this->server['rate'], - ], - ['t' => time()] - ); - } - - if (!empty($userIds)) { - Redis::sadd('traffic:pending_check', ...$userIds); - } - } -} diff --git a/Xboard/app/Models/AdminAuditLog.php b/Xboard/app/Models/AdminAuditLog.php deleted file mode 100644 index 9e9404c..0000000 --- a/Xboard/app/Models/AdminAuditLog.php +++ /dev/null @@ -1,21 +0,0 @@ - 'timestamp', - 'updated_at' => 'timestamp', - ]; - - public function admin() - { - return $this->belongsTo(User::class, 'admin_id'); - } -} diff --git a/Xboard/app/Models/CommissionLog.php b/Xboard/app/Models/CommissionLog.php deleted file mode 100644 index 744c5d5..0000000 --- a/Xboard/app/Models/CommissionLog.php +++ /dev/null @@ -1,16 +0,0 @@ - 'timestamp', - 'updated_at' => 'timestamp' - ]; -} diff --git a/Xboard/app/Models/Coupon.php b/Xboard/app/Models/Coupon.php deleted file mode 100644 index e6271b8..0000000 --- a/Xboard/app/Models/Coupon.php +++ /dev/null @@ -1,28 +0,0 @@ - 'timestamp', - 'updated_at' => 'timestamp', - 'limit_plan_ids' => 'array', - 'limit_period' => 'array', - 'show' => 'boolean', - ]; - - public function getLimitPeriodAttribute($value) - { - return collect(json_decode((string) $value, true))->map(function ($item) { - return PlanService::getPeriodKey($item); - })->toArray(); - } - -} diff --git a/Xboard/app/Models/GiftCardCode.php b/Xboard/app/Models/GiftCardCode.php deleted file mode 100644 index dd09e8e..0000000 --- a/Xboard/app/Models/GiftCardCode.php +++ /dev/null @@ -1,260 +0,0 @@ - 'timestamp', - 'updated_at' => 'timestamp', - 'used_at' => 'timestamp', - 'expires_at' => 'timestamp', - 'actual_rewards' => 'array', - 'metadata' => 'array' - ]; - - /** - * 获取状态映射 - */ - public static function getStatusMap(): array - { - return [ - self::STATUS_UNUSED => '未使用', - self::STATUS_USED => '已使用', - self::STATUS_EXPIRED => '已过期', - self::STATUS_DISABLED => '已禁用', - ]; - } - - /** - * 获取状态名称 - */ - public function getStatusNameAttribute(): string - { - return self::getStatusMap()[$this->status] ?? '未知状态'; - } - - /** - * 关联礼品卡模板 - */ - public function template(): BelongsTo - { - return $this->belongsTo(GiftCardTemplate::class, 'template_id'); - } - - /** - * 关联使用用户 - */ - public function user(): BelongsTo - { - return $this->belongsTo(User::class, 'user_id'); - } - - /** - * 关联使用记录 - */ - public function usages(): HasMany - { - return $this->hasMany(GiftCardUsage::class, 'code_id'); - } - - /** - * 检查是否可用 - */ - public function isAvailable(): bool - { - // 检查状态 - if (in_array($this->status, [self::STATUS_EXPIRED, self::STATUS_DISABLED])) { - return false; - } - - // 检查是否过期 - if ($this->expires_at && $this->expires_at < time()) { - return false; - } - - // 检查使用次数 - if ($this->usage_count >= $this->max_usage) { - return false; - } - - return true; - } - - /** - * 检查是否已过期 - */ - public function isExpired(): bool - { - return $this->expires_at && $this->expires_at < time(); - } - - /** - * 标记为已使用 - */ - public function markAsUsed(User $user): bool - { - $this->status = self::STATUS_USED; - $this->user_id = $user->id; - $this->used_at = time(); - $this->usage_count += 1; - - return $this->save(); - } - - /** - * 标记为已过期 - */ - public function markAsExpired(): bool - { - $this->status = self::STATUS_EXPIRED; - return $this->save(); - } - - /** - * 标记为已禁用 - */ - public function markAsDisabled(): bool - { - $this->status = self::STATUS_DISABLED; - return $this->save(); - } - - /** - * 生成兑换码 - */ - public static function generateCode(string $prefix = 'GC'): string - { - do { - $safePrefix = (string) $prefix; - $code = $safePrefix . strtoupper(substr(md5(uniqid($safePrefix . mt_rand(), true)), 0, 12)); - } while (self::where('code', $code)->exists()); - - return $code; - } - - /** - * 批量生成兑换码 - */ - public static function batchGenerate(int $templateId, int $count, array $options = []): string - { - $batchId = uniqid('batch_'); - $prefix = $options['prefix'] ?? 'GC'; - $expiresAt = $options['expires_at'] ?? null; - $maxUsage = $options['max_usage'] ?? 1; - - $codes = []; - for ($i = 0; $i < $count; $i++) { - $codes[] = [ - 'template_id' => $templateId, - 'code' => self::generateCode($prefix), - 'batch_id' => $batchId, - 'status' => self::STATUS_UNUSED, - 'expires_at' => $expiresAt, - 'max_usage' => $maxUsage, - 'created_at' => time(), - 'updated_at' => time(), - ]; - } - - self::insert($codes); - - return $batchId; - } - - /** - * 设置实际奖励(用于盲盒等) - */ - public function setActualRewards(array $rewards): bool - { - $this->actual_rewards = $rewards; - return $this->save(); - } - - /** - * 获取实际奖励 - */ - public function getActualRewards(): array - { - return $this->actual_rewards ?? $this->template->rewards ?? []; - } - - /** - * 检查兑换码格式 - */ - public static function validateCodeFormat(string $code): bool - { - // 基本格式验证:字母数字组合,长度8-32 - return preg_match('/^[A-Z0-9]{8,32}$/', $code); - } - - /** - * 根据批次ID获取兑换码 - */ - public static function getByBatchId(string $batchId) - { - return self::where('batch_id', $batchId)->get(); - } - - /** - * 清理过期兑换码 - */ - public static function cleanupExpired(): int - { - $count = self::where('status', self::STATUS_UNUSED) - ->where('expires_at', '<', time()) - ->count(); - - self::where('status', self::STATUS_UNUSED) - ->where('expires_at', '<', time()) - ->update(['status' => self::STATUS_EXPIRED]); - - return $count; - } -} \ No newline at end of file diff --git a/Xboard/app/Models/GiftCardTemplate.php b/Xboard/app/Models/GiftCardTemplate.php deleted file mode 100644 index 87a04de..0000000 --- a/Xboard/app/Models/GiftCardTemplate.php +++ /dev/null @@ -1,254 +0,0 @@ - 'timestamp', - 'updated_at' => 'timestamp', - 'conditions' => 'array', - 'rewards' => 'array', - 'limits' => 'array', - 'special_config' => 'array', - 'status' => 'boolean' - ]; - - /** - * 获取卡片类型映射 - */ - public static function getTypeMap(): array - { - return [ - self::TYPE_GENERAL => '通用礼品卡', - self::TYPE_PLAN => '套餐礼品卡', - self::TYPE_MYSTERY => '盲盒礼品卡', - ]; - } - - /** - * 获取类型名称 - */ - public function getTypeNameAttribute(): string - { - return self::getTypeMap()[$this->type] ?? '未知类型'; - } - - /** - * 关联兑换码 - */ - public function codes(): HasMany - { - return $this->hasMany(GiftCardCode::class, 'template_id'); - } - - /** - * 关联使用记录 - */ - public function usages(): HasMany - { - return $this->hasMany(GiftCardUsage::class, 'template_id'); - } - - /** - * 关联统计数据 - */ - public function stats(): HasMany - { - return $this->hasMany(GiftCardUsage::class, 'template_id'); - } - - /** - * 检查是否可用 - */ - public function isAvailable(): bool - { - return $this->status; - } - - /** - * 检查用户是否满足使用条件 - */ - public function checkUserConditions(User $user): bool - { - switch ($this->type) { - case self::TYPE_GENERAL: - $rewards = $this->rewards ?? []; - if (isset($rewards['transfer_enable']) || isset($rewards['expire_days']) || isset($rewards['reset_package'])) { - if (!$user->plan_id) { - return false; - } - } - break; - case self::TYPE_PLAN: - if ($user->isActive()) { - return false; - } - break; - } - - $conditions = $this->conditions ?? []; - - // 检查新用户条件 - if (isset($conditions['new_user_only']) && $conditions['new_user_only']) { - $maxDays = $conditions['new_user_max_days'] ?? 7; - if ($user->created_at < (time() - ($maxDays * 86400))) { - return false; - } - } - - // 检查付费用户条件 - if (isset($conditions['paid_user_only']) && $conditions['paid_user_only']) { - $paidOrderExists = $user->orders()->where('status', Order::STATUS_COMPLETED)->exists(); - if (!$paidOrderExists) { - return false; - } - } - - // 检查允许的套餐 - if (isset($conditions['allowed_plans']) && $user->plan_id) { - if (!in_array($user->plan_id, $conditions['allowed_plans'])) { - return false; - } - } - - // 检查是否需要邀请人 - if (isset($conditions['require_invite']) && $conditions['require_invite']) { - if (!$user->invite_user_id) { - return false; - } - } - - return true; - } - - /** - * 计算实际奖励 - */ - public function calculateActualRewards(User $user): array - { - $baseRewards = $this->rewards; - $actualRewards = $baseRewards; - - // 处理盲盒随机奖励 - if ($this->type === self::TYPE_MYSTERY && isset($this->rewards['random_rewards'])) { - $randomRewards = $this->rewards['random_rewards']; - $totalWeight = array_sum(array_column($randomRewards, 'weight')); - $random = mt_rand(1, $totalWeight); - $currentWeight = 0; - - foreach ($randomRewards as $reward) { - $currentWeight += $reward['weight']; - if ($random <= $currentWeight) { - $actualRewards = array_merge($actualRewards, $reward); - unset($actualRewards['weight']); - break; - } - } - } - - // 处理节日等特殊奖励(通用逻辑) - if (isset($this->special_config['festival_bonus'])) { - $now = time(); - $festivalConfig = $this->special_config; - - if (isset($festivalConfig['start_time']) && isset($festivalConfig['end_time'])) { - if ($now >= $festivalConfig['start_time'] && $now <= $festivalConfig['end_time']) { - $bonus = data_get($festivalConfig, 'festival_bonus', 1.0); - if ($bonus > 1.0) { - foreach ($actualRewards as $key => &$value) { - if (is_numeric($value)) { - $value = intval($value * $bonus); - } - } - unset($value); // 解除引用 - } - } - } - } - - return $actualRewards; - } - - /** - * 检查使用频率限制 - */ - public function checkUsageLimit(User $user): bool - { - $limits = $this->limits ?? []; - - // 检查每用户最大使用次数 - if (isset($limits['max_use_per_user'])) { - $usedCount = $this->usages() - ->where('user_id', $user->id) - ->count(); - if ($usedCount >= $limits['max_use_per_user']) { - return false; - } - } - - // 检查冷却时间 - if (isset($limits['cooldown_hours'])) { - $lastUsage = $this->usages() - ->where('user_id', $user->id) - ->orderBy('created_at', 'desc') - ->first(); - - if ($lastUsage && isset($lastUsage->created_at)) { - $cooldownTime = $lastUsage->created_at + ($limits['cooldown_hours'] * 3600); - if (time() < $cooldownTime) { - return false; - } - } - } - - return true; - } -} \ No newline at end of file diff --git a/Xboard/app/Models/GiftCardUsage.php b/Xboard/app/Models/GiftCardUsage.php deleted file mode 100644 index 69337b9..0000000 --- a/Xboard/app/Models/GiftCardUsage.php +++ /dev/null @@ -1,112 +0,0 @@ - 'timestamp', - 'rewards_given' => 'array', - 'invite_rewards' => 'array', - 'multiplier_applied' => 'float' - ]; - - /** - * 关联兑换码 - */ - public function code(): BelongsTo - { - return $this->belongsTo(GiftCardCode::class, 'code_id'); - } - - /** - * 关联模板 - */ - public function template(): BelongsTo - { - return $this->belongsTo(GiftCardTemplate::class, 'template_id'); - } - - /** - * 关联使用用户 - */ - public function user(): BelongsTo - { - return $this->belongsTo(User::class, 'user_id'); - } - - /** - * 关联邀请人 - */ - public function inviteUser(): BelongsTo - { - return $this->belongsTo(User::class, 'invite_user_id'); - } - - /** - * 创建使用记录 - */ - public static function createRecord( - GiftCardCode $code, - User $user, - array $rewards, - array $options = [] - ): self { - return self::create([ - 'code_id' => $code->id, - 'template_id' => $code->template_id, - 'user_id' => $user->id, - 'invite_user_id' => $user->invite_user_id, - 'rewards_given' => $rewards, - 'invite_rewards' => $options['invite_rewards'] ?? null, - 'user_level_at_use' => $user->plan ? $user->plan->sort : null, - 'plan_id_at_use' => $user->plan_id, - 'multiplier_applied' => $options['multiplier'] ?? 1.0, - // 'ip_address' => $options['ip_address'] ?? null, - 'user_agent' => $options['user_agent'] ?? null, - 'notes' => $options['notes'] ?? null, - 'created_at' => time(), - ]); - } -} \ No newline at end of file diff --git a/Xboard/app/Models/InviteCode.php b/Xboard/app/Models/InviteCode.php deleted file mode 100644 index 7d2716b..0000000 --- a/Xboard/app/Models/InviteCode.php +++ /dev/null @@ -1,19 +0,0 @@ - 'timestamp', - 'updated_at' => 'timestamp', - 'status' => 'boolean', - ]; - - const STATUS_UNUSED = 0; - const STATUS_USED = 1; -} diff --git a/Xboard/app/Models/Knowledge.php b/Xboard/app/Models/Knowledge.php deleted file mode 100644 index 91e7736..0000000 --- a/Xboard/app/Models/Knowledge.php +++ /dev/null @@ -1,17 +0,0 @@ - 'boolean', - 'created_at' => 'timestamp', - 'updated_at' => 'timestamp', - ]; -} diff --git a/Xboard/app/Models/MailLog.php b/Xboard/app/Models/MailLog.php deleted file mode 100644 index 0aef380..0000000 --- a/Xboard/app/Models/MailLog.php +++ /dev/null @@ -1,16 +0,0 @@ - 'timestamp', - 'updated_at' => 'timestamp' - ]; -} diff --git a/Xboard/app/Models/Notice.php b/Xboard/app/Models/Notice.php deleted file mode 100644 index 3ae0cb0..0000000 --- a/Xboard/app/Models/Notice.php +++ /dev/null @@ -1,18 +0,0 @@ - 'timestamp', - 'updated_at' => 'timestamp', - 'tags' => 'array', - 'show' => 'boolean', - ]; -} diff --git a/Xboard/app/Models/Order.php b/Xboard/app/Models/Order.php deleted file mode 100644 index dda0581..0000000 --- a/Xboard/app/Models/Order.php +++ /dev/null @@ -1,120 +0,0 @@ - $commission_log - */ -class Order extends Model -{ - protected $table = 'v2_order'; - protected $dateFormat = 'U'; - protected $guarded = ['id']; - protected $casts = [ - 'created_at' => 'timestamp', - 'updated_at' => 'timestamp', - 'surplus_order_ids' => 'array', - 'handling_amount' => 'integer' - ]; - - const STATUS_PENDING = 0; // 待支付 - const STATUS_PROCESSING = 1; // 开通中 - const STATUS_CANCELLED = 2; // 已取消 - const STATUS_COMPLETED = 3; // 已完成 - const STATUS_DISCOUNTED = 4; // 已折抵 - - public static $statusMap = [ - self::STATUS_PENDING => '待支付', - self::STATUS_PROCESSING => '开通中', - self::STATUS_CANCELLED => '已取消', - self::STATUS_COMPLETED => '已完成', - self::STATUS_DISCOUNTED => '已折抵', - ]; - - const TYPE_NEW_PURCHASE = 1; // 新购 - const TYPE_RENEWAL = 2; // 续费 - const TYPE_UPGRADE = 3; // 升级 - const TYPE_RESET_TRAFFIC = 4; //流量重置包 - public static $typeMap = [ - self::TYPE_NEW_PURCHASE => '新购', - self::TYPE_RENEWAL => '续费', - self::TYPE_UPGRADE => '升级', - self::TYPE_RESET_TRAFFIC => '流量重置', - ]; - - /** - * 获取与订单关联的支付方式 - */ - public function payment(): BelongsTo - { - return $this->belongsTo(Payment::class, 'payment_id', 'id'); - } - - /** - * 获取与订单关联的用户 - */ - public function user(): BelongsTo - { - return $this->belongsTo(User::class, 'user_id', 'id'); - } - - /** - * 获取邀请人 - */ - public function invite_user(): BelongsTo - { - return $this->belongsTo(User::class, 'invite_user_id', 'id'); - } - - /** - * 获取与订单关联的套餐 - */ - public function plan(): BelongsTo - { - return $this->belongsTo(Plan::class, 'plan_id', 'id'); - } - - /** - * 获取与订单关联的佣金记录 - */ - public function commission_log(): HasMany - { - return $this->hasMany(CommissionLog::class, 'trade_no', 'trade_no'); - } -} diff --git a/Xboard/app/Models/Payment.php b/Xboard/app/Models/Payment.php deleted file mode 100644 index fec8b00..0000000 --- a/Xboard/app/Models/Payment.php +++ /dev/null @@ -1,18 +0,0 @@ - 'timestamp', - 'updated_at' => 'timestamp', - 'config' => 'array', - 'enable' => 'boolean' - ]; -} diff --git a/Xboard/app/Models/Plan.php b/Xboard/app/Models/Plan.php deleted file mode 100644 index 13d66f2..0000000 --- a/Xboard/app/Models/Plan.php +++ /dev/null @@ -1,353 +0,0 @@ - $order 关联的订单 - */ -class Plan extends Model -{ - use HasFactory; - - protected $table = 'v2_plan'; - protected $dateFormat = 'U'; - - // 定义流量重置方式 - public const RESET_TRAFFIC_FOLLOW_SYSTEM = null; // 跟随系统设置 - public const RESET_TRAFFIC_FIRST_DAY_MONTH = 0; // 每月1号 - public const RESET_TRAFFIC_MONTHLY = 1; // 按月重置 - public const RESET_TRAFFIC_NEVER = 2; // 不重置 - public const RESET_TRAFFIC_FIRST_DAY_YEAR = 3; // 每年1月1日 - public const RESET_TRAFFIC_YEARLY = 4; // 按年重置 - - // 定义价格类型 - public const PRICE_TYPE_RESET_TRAFFIC = 'reset_traffic'; // 重置流量价格 - - // 定义可用的订阅周期 - public const PERIOD_MONTHLY = 'monthly'; - public const PERIOD_QUARTERLY = 'quarterly'; - public const PERIOD_HALF_YEARLY = 'half_yearly'; - public const PERIOD_YEARLY = 'yearly'; - public const PERIOD_TWO_YEARLY = 'two_yearly'; - public const PERIOD_THREE_YEARLY = 'three_yearly'; - public const PERIOD_ONETIME = 'onetime'; - public const PERIOD_RESET_TRAFFIC = 'reset_traffic'; - - // 定义旧版周期映射 - public const LEGACY_PERIOD_MAPPING = [ - 'month_price' => self::PERIOD_MONTHLY, - 'quarter_price' => self::PERIOD_QUARTERLY, - 'half_year_price' => self::PERIOD_HALF_YEARLY, - 'year_price' => self::PERIOD_YEARLY, - 'two_year_price' => self::PERIOD_TWO_YEARLY, - 'three_year_price' => self::PERIOD_THREE_YEARLY, - 'onetime_price' => self::PERIOD_ONETIME, - 'reset_price' => self::PERIOD_RESET_TRAFFIC - ]; - - protected $fillable = [ - 'group_id', - 'transfer_enable', - 'name', - 'speed_limit', - 'show', - 'sort', - 'renew', - 'content', - 'prices', - 'reset_traffic_method', - 'capacity_limit', - 'sell', - 'device_limit', - 'tags' - ]; - - protected $casts = [ - 'show' => 'boolean', - 'renew' => 'boolean', - 'created_at' => 'timestamp', - 'updated_at' => 'timestamp', - 'group_id' => 'integer', - 'prices' => 'array', - 'tags' => 'array', - 'reset_traffic_method' => 'integer', - ]; - - /** - * 获取所有可用的流量重置方式 - * - * @return array - */ - public static function getResetTrafficMethods(): array - { - return [ - self::RESET_TRAFFIC_FOLLOW_SYSTEM => '跟随系统设置', - self::RESET_TRAFFIC_FIRST_DAY_MONTH => '每月1号', - self::RESET_TRAFFIC_MONTHLY => '按月重置', - self::RESET_TRAFFIC_NEVER => '不重置', - self::RESET_TRAFFIC_FIRST_DAY_YEAR => '每年1月1日', - self::RESET_TRAFFIC_YEARLY => '按年重置', - ]; - } - - /** - * 获取所有可用的订阅周期 - * - * @return array - */ - public static function getAvailablePeriods(): array - { - return [ - self::PERIOD_MONTHLY => [ - 'name' => '月付', - 'days' => 30, - 'value' => 1 - ], - self::PERIOD_QUARTERLY => [ - 'name' => '季付', - 'days' => 90, - 'value' => 3 - ], - self::PERIOD_HALF_YEARLY => [ - 'name' => '半年付', - 'days' => 180, - 'value' => 6 - ], - self::PERIOD_YEARLY => [ - 'name' => '年付', - 'days' => 365, - 'value' => 12 - ], - self::PERIOD_TWO_YEARLY => [ - 'name' => '两年付', - 'days' => 730, - 'value' => 24 - ], - self::PERIOD_THREE_YEARLY => [ - 'name' => '三年付', - 'days' => 1095, - 'value' => 36 - ], - self::PERIOD_ONETIME => [ - 'name' => '一次性', - 'days' => -1, - 'value' => -1 - ], - self::PERIOD_RESET_TRAFFIC => [ - 'name' => '重置流量', - 'days' => -1, - 'value' => -1 - ], - ]; - } - - /** - * 获取指定周期的价格 - * - * @param string $period - * @return int|null - */ - public function getPriceByPeriod(string $period): ?int - { - return $this->prices[$period] ?? null; - } - - /** - * 获取所有已设置价格的周期 - * - * @return array - */ - public function getActivePeriods(): array - { - return array_filter( - self::getAvailablePeriods(), - fn($period) => isset($this->prices[$period]) - && $this->prices[$period] > 0, - ARRAY_FILTER_USE_KEY - ); - } - - /** - * 设置指定周期的价格 - * - * @param string $period - * @param int $price - * @return void - * @throws InvalidArgumentException - */ - public function setPeriodPrice(string $period, int $price): void - { - if (!array_key_exists($period, self::getAvailablePeriods())) { - throw new InvalidArgumentException("Invalid period: {$period}"); - } - - $prices = $this->prices ?? []; - $prices[$period] = $price; - $this->prices = $prices; - } - - /** - * 移除指定周期的价格 - * - * @param string $period - * @return void - */ - public function removePeriodPrice(string $period): void - { - $prices = $this->prices ?? []; - unset($prices[$period]); - $this->prices = $prices; - } - - /** - * 获取所有价格及其对应的周期信息 - * - * @return array - */ - public function getPriceList(): array - { - $prices = $this->prices ?? []; - $periods = self::getAvailablePeriods(); - - $priceList = []; - foreach ($prices as $period => $price) { - if (isset($periods[$period]) && $price > 0) { - $priceList[$period] = [ - 'period' => $periods[$period], - 'price' => $price, - 'average_price' => $periods[$period]['value'] > 0 - ? round($price / $periods[$period]['value'], 2) - : $price - ]; - } - } - - return $priceList; - } - - /** - * 检查是否可以重置流量 - * - * @return bool - */ - public function canResetTraffic(): bool - { - return $this->reset_traffic_method !== self::RESET_TRAFFIC_NEVER - && $this->getResetTrafficPrice() > 0; - } - - /** - * 获取重置流量的价格 - * - * @return int - */ - public function getResetTrafficPrice(): int - { - return $this->prices[self::PRICE_TYPE_RESET_TRAFFIC] ?? 0; - } - - /** - * 计算指定周期的有效天数 - * - * @param string $period - * @return int -1表示永久有效 - * @throws InvalidArgumentException - */ - public static function getPeriodDays(string $period): int - { - $periods = self::getAvailablePeriods(); - if (!isset($periods[$period])) { - throw new InvalidArgumentException("Invalid period: {$period}"); - } - - return $periods[$period]['days']; - } - - /** - * 检查周期是否有效 - * - * @param string $period - * @return bool - */ - public static function isValidPeriod(string $period): bool - { - return array_key_exists($period, self::getAvailablePeriods()); - } - - public function users(): HasMany - { - return $this->hasMany(User::class); - } - - public function group(): HasOne - { - return $this->hasOne(ServerGroup::class, 'id', 'group_id'); - } - - public function orders(): HasMany - { - return $this->hasMany(Order::class); - } - - /** - * 设置流量重置方式 - * - * @param int $method - * @return void - * @throws InvalidArgumentException - */ - public function setResetTrafficMethod(int $method): void - { - if (!array_key_exists($method, self::getResetTrafficMethods())) { - throw new InvalidArgumentException("Invalid reset traffic method: {$method}"); - } - - $this->reset_traffic_method = $method; - } - - /** - * 设置重置流量价格 - * - * @param int $price - * @return void - */ - public function setResetTrafficPrice(int $price): void - { - $prices = $this->prices ?? []; - $prices[self::PRICE_TYPE_RESET_TRAFFIC] = max(0, $price); - $this->prices = $prices; - } - - public function order(): HasMany - { - return $this->hasMany(Order::class); - } -} \ No newline at end of file diff --git a/Xboard/app/Models/Plugin.php b/Xboard/app/Models/Plugin.php deleted file mode 100644 index 0425558..0000000 --- a/Xboard/app/Models/Plugin.php +++ /dev/null @@ -1,77 +0,0 @@ - 'boolean', - ]; - - public function scopeByType(Builder $query, string $type): Builder - { - return $query->where('type', $type); - } - - public function isFeaturePlugin(): bool - { - return $this->type === self::TYPE_FEATURE; - } - - public function isPaymentPlugin(): bool - { - return $this->type === self::TYPE_PAYMENT; - } - - public function isProtected(): bool - { - return in_array($this->code, self::PROTECTED_PLUGINS); - } - - public function canBeDeleted(): bool - { - return !$this->isProtected(); - } - -} diff --git a/Xboard/app/Models/Server.php b/Xboard/app/Models/Server.php deleted file mode 100644 index 19e2b5e..0000000 --- a/Xboard/app/Models/Server.php +++ /dev/null @@ -1,563 +0,0 @@ - $stats 节点统计 - * - * @property-read int|null $last_check_at 最后检查时间(Unix时间戳) - * @property-read int|null $last_push_at 最后推送时间(Unix时间戳) - * @property-read int $online 在线用户数 - * @property-read int $online_conn 在线连接数 - * @property-read array|null $metrics 节点指标指标 - * @property-read int $is_online 是否在线(1在线 0离线) - * @property-read string $available_status 可用状态描述 - * @property-read string $cache_key 缓存键 - * @property string|null $ports 端口范围 - * @property string|null $password 密码 - * @property int|null $u 上行流量 - * @property int|null $d 下行流量 - * @property int|null $total 总流量 - * @property-read array|null $load_status 负载状态(包含CPU、内存、交换区、磁盘信息) - * - * @property int $transfer_enable 流量上限,0或者null表示不限制 - * @property int $u 当前上传流量 - * @property int $d 当前下载流量 - */ -class Server extends Model -{ - public const TYPE_HYSTERIA = 'hysteria'; - public const TYPE_VLESS = 'vless'; - public const TYPE_TROJAN = 'trojan'; - public const TYPE_VMESS = 'vmess'; - public const TYPE_TUIC = 'tuic'; - public const TYPE_SHADOWSOCKS = 'shadowsocks'; - public const TYPE_ANYTLS = 'anytls'; - public const TYPE_SOCKS = 'socks'; - public const TYPE_NAIVE = 'naive'; - public const TYPE_HTTP = 'http'; - public const TYPE_MIERU = 'mieru'; - public const STATUS_OFFLINE = 0; - public const STATUS_ONLINE_NO_PUSH = 1; - public const STATUS_ONLINE = 2; - - public const CHECK_INTERVAL = 300; // 5 minutes in seconds - - private const CIPHER_CONFIGURATIONS = [ - '2022-blake3-aes-128-gcm' => [ - 'serverKeySize' => 16, - 'userKeySize' => 16, - ], - '2022-blake3-aes-256-gcm' => [ - 'serverKeySize' => 32, - 'userKeySize' => 32, - ], - '2022-blake3-chacha20-poly1305' => [ - 'serverKeySize' => 32, - 'userKeySize' => 32, - ] - ]; - - public const TYPE_ALIASES = [ - 'v2ray' => self::TYPE_VMESS, - 'hysteria2' => self::TYPE_HYSTERIA, - ]; - - public const VALID_TYPES = [ - self::TYPE_HYSTERIA, - self::TYPE_VLESS, - self::TYPE_TROJAN, - self::TYPE_VMESS, - self::TYPE_TUIC, - self::TYPE_SHADOWSOCKS, - self::TYPE_ANYTLS, - self::TYPE_SOCKS, - self::TYPE_NAIVE, - self::TYPE_HTTP, - self::TYPE_MIERU, - ]; - - protected $table = 'v2_server'; - - protected $guarded = ['id']; - protected $casts = [ - 'group_ids' => 'array', - 'route_ids' => 'array', - 'tags' => 'array', - 'protocol_settings' => 'array', - 'custom_outbounds' => 'array', - 'custom_routes' => 'array', - 'cert_config' => 'array', - 'last_check_at' => 'integer', - 'last_push_at' => 'integer', - 'show' => 'boolean', - 'created_at' => 'timestamp', - 'updated_at' => 'timestamp', - 'rate_time_ranges' => 'array', - 'rate_time_enable' => 'boolean', - 'transfer_enable' => 'integer', - 'u' => 'integer', - 'd' => 'integer', - ]; - - private const MULTIPLEX_CONFIGURATION = [ - 'multiplex' => [ - 'type' => 'object', - 'fields' => [ - 'enabled' => ['type' => 'boolean', 'default' => false], - 'protocol' => ['type' => 'string', 'default' => 'yamux'], - 'max_connections' => ['type' => 'integer', 'default' => null], - // 'min_streams' => ['type' => 'integer', 'default' => null], - // 'max_streams' => ['type' => 'integer', 'default' => null], - 'padding' => ['type' => 'boolean', 'default' => false], - 'brutal' => [ - 'type' => 'object', - 'fields' => [ - 'enabled' => ['type' => 'boolean', 'default' => false], - 'up_mbps' => ['type' => 'integer', 'default' => null], - 'down_mbps' => ['type' => 'integer', 'default' => null], - ] - ] - ] - ] - ]; - - private const REALITY_CONFIGURATION = [ - 'reality_settings' => [ - 'type' => 'object', - 'fields' => [ - 'server_name' => ['type' => 'string', 'default' => null], - 'server_port' => ['type' => 'string', 'default' => null], - 'public_key' => ['type' => 'string', 'default' => null], - 'private_key' => ['type' => 'string', 'default' => null], - 'short_id' => ['type' => 'string', 'default' => null], - 'allow_insecure' => ['type' => 'boolean', 'default' => false], - ] - ] - ]; - - private const UTLS_CONFIGURATION = [ - 'utls' => [ - 'type' => 'object', - 'fields' => [ - 'enabled' => ['type' => 'boolean', 'default' => false], - 'fingerprint' => ['type' => 'string', 'default' => 'chrome'], - ] - ] - ]; - - private const PROTOCOL_CONFIGURATIONS = [ - self::TYPE_TROJAN => [ - 'tls' => ['type' => 'integer', 'default' => 1], - 'network' => ['type' => 'string', 'default' => null], - 'network_settings' => ['type' => 'array', 'default' => null], - 'server_name' => ['type' => 'string', 'default' => null], - 'allow_insecure' => ['type' => 'boolean', 'default' => false], - ...self::REALITY_CONFIGURATION, - ...self::MULTIPLEX_CONFIGURATION, - ...self::UTLS_CONFIGURATION - ], - self::TYPE_VMESS => [ - 'tls' => ['type' => 'integer', 'default' => 0], - 'network' => ['type' => 'string', 'default' => null], - 'rules' => ['type' => 'array', 'default' => null], - 'network_settings' => ['type' => 'array', 'default' => null], - 'tls_settings' => ['type' => 'array', 'default' => null], - ...self::MULTIPLEX_CONFIGURATION, - ...self::UTLS_CONFIGURATION - ], - self::TYPE_VLESS => [ - 'tls' => ['type' => 'integer', 'default' => 0], - 'tls_settings' => ['type' => 'array', 'default' => null], - 'flow' => ['type' => 'string', 'default' => null], - 'encryption' => [ - 'type' => 'object', - 'default' => null, - 'fields' => [ - 'enabled' => ['type' => 'boolean', 'default' => false], - 'encryption' => ['type' => 'string', 'default' => null], // 客户端公钥 - 'decryption' => ['type' => 'string', 'default' => null], // 服务端私钥 - ] - ], - 'network' => ['type' => 'string', 'default' => null], - 'network_settings' => ['type' => 'array', 'default' => null], - ...self::REALITY_CONFIGURATION, - ...self::MULTIPLEX_CONFIGURATION, - ...self::UTLS_CONFIGURATION - ], - self::TYPE_SHADOWSOCKS => [ - 'cipher' => ['type' => 'string', 'default' => null], - 'obfs' => ['type' => 'string', 'default' => null], - 'obfs_settings' => ['type' => 'array', 'default' => null], - 'plugin' => ['type' => 'string', 'default' => null], - 'plugin_opts' => ['type' => 'string', 'default' => null] - ], - self::TYPE_HYSTERIA => [ - 'version' => ['type' => 'integer', 'default' => 2], - 'bandwidth' => [ - 'type' => 'object', - 'fields' => [ - 'up' => ['type' => 'integer', 'default' => null], - 'down' => ['type' => 'integer', 'default' => null] - ] - ], - 'obfs' => [ - 'type' => 'object', - 'fields' => [ - 'open' => ['type' => 'boolean', 'default' => false], - 'type' => ['type' => 'string', 'default' => 'salamander'], - 'password' => ['type' => 'string', 'default' => null] - ] - ], - 'tls' => [ - 'type' => 'object', - 'fields' => [ - 'server_name' => ['type' => 'string', 'default' => null], - 'allow_insecure' => ['type' => 'boolean', 'default' => false] - ] - ], - 'hop_interval' => ['type' => 'integer', 'default' => null] - ], - self::TYPE_TUIC => [ - 'version' => ['type' => 'integer', 'default' => 5], - 'congestion_control' => ['type' => 'string', 'default' => 'cubic'], - 'alpn' => ['type' => 'array', 'default' => ['h3']], - 'udp_relay_mode' => ['type' => 'string', 'default' => 'native'], - 'tls' => [ - 'type' => 'object', - 'fields' => [ - 'server_name' => ['type' => 'string', 'default' => null], - 'allow_insecure' => ['type' => 'boolean', 'default' => false] - ] - ] - ], - self::TYPE_ANYTLS => [ - 'padding_scheme' => [ - 'type' => 'array', - 'default' => [ - "stop=8", - "0=30-30", - "1=100-400", - "2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000", - "3=9-9,500-1000", - "4=500-1000", - "5=500-1000", - "6=500-1000", - "7=500-1000" - ] - ], - 'tls' => [ - 'type' => 'object', - 'fields' => [ - 'server_name' => ['type' => 'string', 'default' => null], - 'allow_insecure' => ['type' => 'boolean', 'default' => false] - ] - ] - ], - self::TYPE_SOCKS => [ - 'tls' => ['type' => 'integer', 'default' => 0], - 'tls_settings' => [ - 'type' => 'object', - 'fields' => [ - 'allow_insecure' => ['type' => 'boolean', 'default' => false] - ] - ] - ], - self::TYPE_NAIVE => [ - 'tls' => ['type' => 'integer', 'default' => 0], - 'tls_settings' => ['type' => 'array', 'default' => null] - ], - self::TYPE_HTTP => [ - 'tls' => ['type' => 'integer', 'default' => 0], - 'tls_settings' => [ - 'type' => 'object', - 'fields' => [ - 'allow_insecure' => ['type' => 'boolean', 'default' => false], - 'server_name' => ['type' => 'string', 'default' => null] - ] - ] - ], - self::TYPE_MIERU => [ - 'transport' => ['type' => 'string', 'default' => 'TCP'], - 'traffic_pattern' => ['type' => 'string', 'default' => ''], - ...self::MULTIPLEX_CONFIGURATION, - ] - ]; - - private function castValueWithConfig($value, array $config) - { - if ($value === null && $config['type'] !== 'object') { - return $config['default'] ?? null; - } - - return match ($config['type']) { - 'integer' => (int) $value, - 'boolean' => (bool) $value, - 'string' => (string) $value, - 'array' => (array) $value, - 'object' => is_array($value) ? - $this->castSettingsWithConfig($value, $config['fields']) : - $config['default'] ?? null, - default => $value - }; - } - - private function castSettingsWithConfig(array $settings, array $configs): array - { - $result = []; - foreach ($configs as $key => $config) { - $value = $settings[$key] ?? null; - $result[$key] = $this->castValueWithConfig($value, $config); - } - return $result; - } - - public function getProtocolSettingsAttribute($value) - { - $settings = json_decode($value, true) ?? []; - $configs = self::PROTOCOL_CONFIGURATIONS[$this->type] ?? []; - return $this->castSettingsWithConfig($settings, $configs); - } - - public function setProtocolSettingsAttribute($value) - { - if (is_string($value)) { - $value = json_decode($value, true); - } - - $configs = self::PROTOCOL_CONFIGURATIONS[$this->type] ?? []; - $castedSettings = $this->castSettingsWithConfig($value ?? [], $configs); - - $this->attributes['protocol_settings'] = json_encode($castedSettings); - } - - public function generateServerPassword(User $user): string - { - if ($this->type !== self::TYPE_SHADOWSOCKS) { - return $user->uuid; - } - - - $cipher = data_get($this, 'protocol_settings.cipher'); - if (!$cipher || !isset(self::CIPHER_CONFIGURATIONS[$cipher])) { - return $user->uuid; - } - - $config = self::CIPHER_CONFIGURATIONS[$cipher]; - // Use parent's created_at if this is a child node - $serverCreatedAt = $this->parent_id ? $this->parent->created_at : $this->created_at; - $serverKey = Helper::getServerKey($serverCreatedAt, $config['serverKeySize']); - $userKey = Helper::uuidToBase64($user->uuid, $config['userKeySize']); - return "{$serverKey}:{$userKey}"; - } - - public static function normalizeType(?string $type): string | null - { - return $type ? strtolower(self::TYPE_ALIASES[$type] ?? $type) : null; - } - - public static function isValidType(?string $type): bool - { - return $type ? in_array(self::normalizeType($type), self::VALID_TYPES, true) : true; - } - - public function getAvailableStatusAttribute(): int - { - $now = time(); - if (!$this->last_check_at || ($now - self::CHECK_INTERVAL) >= $this->last_check_at) { - return self::STATUS_OFFLINE; - } - if (!$this->last_push_at || ($now - self::CHECK_INTERVAL) >= $this->last_push_at) { - return self::STATUS_ONLINE_NO_PUSH; - } - return self::STATUS_ONLINE; - } - - public function parent(): BelongsTo - { - return $this->belongsTo(self::class, 'parent_id', 'id'); - } - - public function stats(): HasMany - { - return $this->hasMany(StatServer::class, 'server_id', 'id'); - } - - public function groups() - { - return ServerGroup::whereIn('id', $this->group_ids)->get(); - } - - public function routes() - { - return ServerRoute::whereIn('id', $this->route_ids)->get(); - } - - /** - * 最后检查时间访问器 - */ - protected function lastCheckAt(): Attribute - { - return Attribute::make( - get: function () { - $type = strtoupper($this->type); - $serverId = $this->parent_id ?: $this->id; - return Cache::get(CacheKey::get("SERVER_{$type}_LAST_CHECK_AT", $serverId)); - } - ); - } - - /** - * 最后推送时间访问器 - */ - protected function lastPushAt(): Attribute - { - return Attribute::make( - get: function () { - $type = strtoupper($this->type); - $serverId = $this->parent_id ?: $this->id; - return Cache::get(CacheKey::get("SERVER_{$type}_LAST_PUSH_AT", $serverId)); - } - ); - } - - /** - * 在线用户数访问器 - */ - protected function online(): Attribute - { - return Attribute::make( - get: function () { - $type = strtoupper($this->type); - $serverId = $this->parent_id ?: $this->id; - return Cache::get(CacheKey::get("SERVER_{$type}_ONLINE_USER", $serverId)) ?? 0; - } - ); - } - - /** - * 是否在线访问器 - */ - protected function isOnline(): Attribute - { - return Attribute::make( - get: function () { - return (time() - 300 > $this->last_check_at) ? 0 : 1; - } - ); - } - - /** - * 缓存键访问器 - */ - protected function cacheKey(): Attribute - { - return Attribute::make( - get: function () { - return "{$this->type}-{$this->id}-{$this->updated_at}-{$this->is_online}"; - } - ); - } - - /** - * 服务器密钥访问器 - */ - protected function serverKey(): Attribute - { - return Attribute::make( - get: function () { - if ($this->type === self::TYPE_SHADOWSOCKS) { - return Helper::getServerKey($this->created_at, 16); - } - return null; - } - ); - } - - /** - * 指标指标访问器 - */ - protected function metrics(): Attribute - { - return Attribute::make( - get: function () { - $type = strtoupper($this->type); - $serverId = $this->parent_id ?: $this->id; - return Cache::get(CacheKey::get("SERVER_{$type}_METRICS", $serverId)); - } - ); - } - - /** - * 在线连接数访问器 - */ - protected function onlineConn(): Attribute - { - return Attribute::make( - get: function () { - return $this->metrics['active_connections'] ?? 0; - } - ); - } - - /** - * 负载状态访问器 - */ - protected function loadStatus(): Attribute - { - return Attribute::make( - get: function () { - $type = strtoupper($this->type); - $serverId = $this->parent_id ?: $this->id; - return Cache::get(CacheKey::get("SERVER_{$type}_LOAD_STATUS", $serverId)); - } - ); - } - - public function getCurrentRate(): float - { - if (!$this->rate_time_enable) { - return (float) $this->rate; - } - - $now = now()->format('H:i'); - $ranges = $this->rate_time_ranges ?? []; - $matchedRange = collect($ranges) - ->first(fn($range) => $now >= $range['start'] && $now <= $range['end']); - - return $matchedRange ? (float) $matchedRange['rate'] : (float) $this->rate; - } -} diff --git a/Xboard/app/Models/ServerGroup.php b/Xboard/app/Models/ServerGroup.php deleted file mode 100644 index 57f3514..0000000 --- a/Xboard/app/Models/ServerGroup.php +++ /dev/null @@ -1,46 +0,0 @@ - 'timestamp', - 'updated_at' => 'timestamp' - ]; - - public function users(): HasMany - { - return $this->hasMany(User::class, 'group_id', 'id'); - } - - public function servers() - { - return Server::whereJsonContains('group_ids', (string) $this->id)->get(); - } - - /** - * 获取服务器数量 - */ - protected function serverCount(): Attribute - { - return Attribute::make( - get: fn () => Server::whereJsonContains('group_ids', (string) $this->id)->count(), - ); - } -} diff --git a/Xboard/app/Models/ServerLog.php b/Xboard/app/Models/ServerLog.php deleted file mode 100644 index ef3590c..0000000 --- a/Xboard/app/Models/ServerLog.php +++ /dev/null @@ -1,16 +0,0 @@ - 'timestamp', - 'updated_at' => 'timestamp' - ]; -} diff --git a/Xboard/app/Models/ServerRoute.php b/Xboard/app/Models/ServerRoute.php deleted file mode 100644 index 024a7e4..0000000 --- a/Xboard/app/Models/ServerRoute.php +++ /dev/null @@ -1,17 +0,0 @@ - 'timestamp', - 'updated_at' => 'timestamp', - 'match' => 'array' - ]; -} diff --git a/Xboard/app/Models/ServerStat.php b/Xboard/app/Models/ServerStat.php deleted file mode 100644 index 5006ded..0000000 --- a/Xboard/app/Models/ServerStat.php +++ /dev/null @@ -1,16 +0,0 @@ - 'timestamp', - 'updated_at' => 'timestamp' - ]; -} diff --git a/Xboard/app/Models/Setting.php b/Xboard/app/Models/Setting.php deleted file mode 100644 index b24a471..0000000 --- a/Xboard/app/Models/Setting.php +++ /dev/null @@ -1,68 +0,0 @@ - 'string', - 'value' => 'string', - ]; - - /** - * 获取实际内容值 - */ - public function getContentValue() - { - $rawValue = $this->attributes['value'] ?? null; - - if ($rawValue === null) { - return null; - } - - // 如果已经是数组,直接返回 - if (is_array($rawValue)) { - return $rawValue; - } - - // 如果是数字字符串,返回原值 - if (is_numeric($rawValue) && !preg_match('/[^\d.]/', $rawValue)) { - return $rawValue; - } - - // 尝试解析 JSON - if (is_string($rawValue)) { - $decodedValue = json_decode($rawValue, true); - if (json_last_error() === JSON_ERROR_NONE) { - return $decodedValue; - } - } - - return $rawValue; - } - - /** - * 兼容性:保持原有的 value 访问器 - */ - public function getValueAttribute($value) - { - return $this->getContentValue(); - } - - /** - * 创建或更新设置项 - */ - public static function createOrUpdate(string $name, $value): self - { - $processedValue = is_array($value) ? json_encode($value) : $value; - - return self::updateOrCreate( - ['name' => $name], - ['value' => $processedValue] - ); - } -} diff --git a/Xboard/app/Models/Stat.php b/Xboard/app/Models/Stat.php deleted file mode 100644 index b0ed9ec..0000000 --- a/Xboard/app/Models/Stat.php +++ /dev/null @@ -1,16 +0,0 @@ - 'timestamp', - 'updated_at' => 'timestamp' - ]; -} diff --git a/Xboard/app/Models/StatServer.php b/Xboard/app/Models/StatServer.php deleted file mode 100644 index efa1fc6..0000000 --- a/Xboard/app/Models/StatServer.php +++ /dev/null @@ -1,33 +0,0 @@ - 'timestamp', - 'updated_at' => 'timestamp' - ]; - - public function server() - { - return $this->belongsTo(Server::class, 'server_id'); - } -} diff --git a/Xboard/app/Models/StatUser.php b/Xboard/app/Models/StatUser.php deleted file mode 100644 index a956bd7..0000000 --- a/Xboard/app/Models/StatUser.php +++ /dev/null @@ -1,28 +0,0 @@ - 'timestamp', - 'updated_at' => 'timestamp' - ]; -} diff --git a/Xboard/app/Models/SubscribeTemplate.php b/Xboard/app/Models/SubscribeTemplate.php deleted file mode 100644 index fec76e0..0000000 --- a/Xboard/app/Models/SubscribeTemplate.php +++ /dev/null @@ -1,46 +0,0 @@ - 'string', - 'content' => 'string', - ]; - - private static string $cachePrefix = 'subscribe_template:'; - - public static function getContent(string $name): ?string - { - $cacheKey = self::$cachePrefix . $name; - - return Cache::store('redis')->remember($cacheKey, 3600, function () use ($name) { - return self::where('name', $name)->value('content'); - }); - } - - public static function setContent(string $name, ?string $content): void - { - self::updateOrCreate( - ['name' => $name], - ['content' => $content] - ); - Cache::store('redis')->forget(self::$cachePrefix . $name); - } - - public static function getAllContents(): array - { - return self::pluck('content', 'name')->toArray(); - } - - public static function flushCache(string $name): void - { - Cache::store('redis')->forget(self::$cachePrefix . $name); - } -} diff --git a/Xboard/app/Models/Ticket.php b/Xboard/app/Models/Ticket.php deleted file mode 100644 index 86ba9b6..0000000 --- a/Xboard/app/Models/Ticket.php +++ /dev/null @@ -1,60 +0,0 @@ - $messages 关联的工单消息 - */ -class Ticket extends Model -{ - protected $table = 'v2_ticket'; - protected $dateFormat = 'U'; - protected $guarded = ['id']; - protected $casts = [ - 'created_at' => 'timestamp', - 'updated_at' => 'timestamp' - ]; - - const STATUS_OPENING = 0; - const STATUS_CLOSED = 1; - public static $statusMap = [ - self::STATUS_OPENING => '开启', - self::STATUS_CLOSED => '关闭' - ]; - - public function user(): BelongsTo - { - return $this->belongsTo(User::class, 'user_id', 'id'); - } - - /** - * 关联的工单消息 - */ - public function messages(): HasMany - { - return $this->hasMany(TicketMessage::class, 'ticket_id', 'id'); - } - - // 即将删除 - public function message(): HasMany - { - return $this->hasMany(TicketMessage::class, 'ticket_id', 'id'); - } -} diff --git a/Xboard/app/Models/TicketMessage.php b/Xboard/app/Models/TicketMessage.php deleted file mode 100644 index c9d45a8..0000000 --- a/Xboard/app/Models/TicketMessage.php +++ /dev/null @@ -1,56 +0,0 @@ - 'timestamp', - 'updated_at' => 'timestamp' - ]; - - protected $appends = ['is_from_user', 'is_from_admin']; - - /** - * 关联的工单 - */ - public function ticket(): BelongsTo - { - return $this->belongsTo(Ticket::class, 'ticket_id', 'id'); - } - - /** - * 判断消息是否由工单发起人发送 - */ - public function getIsFromUserAttribute(): bool - { - return $this->ticket->user_id === $this->user_id; - } - - /** - * 判断消息是否由管理员发送 - */ - public function getIsFromAdminAttribute(): bool - { - return $this->ticket->user_id !== $this->user_id; - } -} diff --git a/Xboard/app/Models/TrafficResetLog.php b/Xboard/app/Models/TrafficResetLog.php deleted file mode 100644 index 3b05a84..0000000 --- a/Xboard/app/Models/TrafficResetLog.php +++ /dev/null @@ -1,149 +0,0 @@ - 'datetime', - 'metadata' => 'array', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - ]; - - // 重置类型常量 - public const TYPE_MONTHLY = 'monthly'; - public const TYPE_FIRST_DAY_MONTH = 'first_day_month'; - public const TYPE_YEARLY = 'yearly'; - public const TYPE_FIRST_DAY_YEAR = 'first_day_year'; - public const TYPE_MANUAL = 'manual'; - public const TYPE_PURCHASE = 'purchase'; - - // 触发来源常量 - public const SOURCE_AUTO = 'auto'; - public const SOURCE_MANUAL = 'manual'; - public const SOURCE_API = 'api'; - public const SOURCE_CRON = 'cron'; - public const SOURCE_USER_ACCESS = 'user_access'; - public const SOURCE_ORDER = 'order'; - public const SOURCE_GIFT_CARD = 'gift_card'; - - /** - * 获取重置类型的多语言名称 - */ - public static function getResetTypeNames(): array - { - return [ - self::TYPE_MONTHLY => __('traffic_reset.reset_type.monthly'), - self::TYPE_FIRST_DAY_MONTH => __('traffic_reset.reset_type.first_day_month'), - self::TYPE_YEARLY => __('traffic_reset.reset_type.yearly'), - self::TYPE_FIRST_DAY_YEAR => __('traffic_reset.reset_type.first_day_year'), - self::TYPE_MANUAL => __('traffic_reset.reset_type.manual'), - self::TYPE_PURCHASE => __('traffic_reset.reset_type.purchase'), - ]; - } - - /** - * 获取触发来源的多语言名称 - */ - public static function getSourceNames(): array - { - return [ - self::SOURCE_AUTO => __('traffic_reset.source.auto'), - self::SOURCE_MANUAL => __('traffic_reset.source.manual'), - self::SOURCE_API => __('traffic_reset.source.api'), - self::SOURCE_CRON => __('traffic_reset.source.cron'), - self::SOURCE_USER_ACCESS => __('traffic_reset.source.user_access'), - ]; - } - - /** - * 关联用户 - */ - public function user(): BelongsTo - { - return $this->belongsTo(User::class, 'user_id', 'id'); - } - - /** - * 获取重置类型名称 - */ - public function getResetTypeName(): string - { - return self::getResetTypeNames()[$this->reset_type] ?? $this->reset_type; - } - - /** - * 获取触发来源名称 - */ - public function getSourceName(): string - { - return self::getSourceNames()[$this->trigger_source] ?? $this->trigger_source; - } - - /** - * 获取重置的流量差值 - */ - public function getTrafficDiff(): array - { - return [ - 'upload_diff' => $this->new_upload - $this->old_upload, - 'download_diff' => $this->new_download - $this->old_download, - 'total_diff' => $this->new_total - $this->old_total, - ]; - } - - /** - * 格式化流量大小 - */ - public function formatTraffic(int $bytes): string - { - $units = ['B', 'KB', 'MB', 'GB', 'TB']; - $bytes = max($bytes, 0); - $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); - $pow = min($pow, count($units) - 1); - - $bytes /= (1 << (10 * $pow)); - - return round($bytes, 2) . ' ' . $units[$pow]; - } -} \ No newline at end of file diff --git a/Xboard/app/Models/User.php b/Xboard/app/Models/User.php deleted file mode 100644 index a826e4a..0000000 --- a/Xboard/app/Models/User.php +++ /dev/null @@ -1,215 +0,0 @@ - $codes 邀请码列表 - * @property-read \Illuminate\Database\Eloquent\Collection $orders 订单列表 - * @property-read \Illuminate\Database\Eloquent\Collection $stat 统计信息 - * @property-read \Illuminate\Database\Eloquent\Collection $tickets 工单列表 - * @property-read \Illuminate\Database\Eloquent\Collection $trafficResetLogs 流量重置记录 - * @property-read User|null $parent 父账户 - * @property-read string $subscribe_url 订阅链接(动态生成) - */ -class User extends Authenticatable -{ - use HasApiTokens; - protected $table = 'v2_user'; - protected $dateFormat = 'U'; - protected $guarded = ['id']; - protected $casts = [ - 'created_at' => 'timestamp', - 'updated_at' => 'timestamp', - 'banned' => 'boolean', - 'is_admin' => 'boolean', - 'is_staff' => 'boolean', - 'remind_expire' => 'boolean', - 'remind_traffic' => 'boolean', - 'commission_auto_check' => 'boolean', - 'commission_rate' => 'float', - 'next_reset_at' => 'timestamp', - 'last_reset_at' => 'timestamp', - ]; - protected $hidden = ['password']; - - public const COMMISSION_TYPE_SYSTEM = 0; - public const COMMISSION_TYPE_PERIOD = 1; - public const COMMISSION_TYPE_ONETIME = 2; - protected function email(): Attribute - { - return Attribute::make( - set: fn (string $value) => strtolower(trim($value)), - ); - } - - /** - * 按邮箱查询(大小写不敏感,兼容所有数据库) - */ - public function scopeByEmail(Builder $query, string $email): Builder - { - return $query->where('email', strtolower(trim($email))); - } - - // 获取邀请人信息 - public function invite_user(): BelongsTo - { - return $this->belongsTo(self::class, 'invite_user_id', 'id'); - } - - /** - * 获取用户订阅计划 - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function plan(): BelongsTo - { - return $this->belongsTo(Plan::class, 'plan_id', 'id'); - } - - public function group(): BelongsTo - { - return $this->belongsTo(ServerGroup::class, 'group_id', 'id'); - } - - // 获取用户邀请码列表 - public function codes(): HasMany - { - return $this->hasMany(InviteCode::class, 'user_id', 'id'); - } - - public function orders(): HasMany - { - return $this->hasMany(Order::class, 'user_id', 'id'); - } - - public function stat(): HasMany - { - return $this->hasMany(StatUser::class, 'user_id', 'id'); - } - - // 关联工单列表 - public function tickets(): HasMany - { - return $this->hasMany(Ticket::class, 'user_id', 'id'); - } - - public function parent(): BelongsTo - { - return $this->belongsTo(self::class, 'parent_id', 'id'); - } - - /** - * 关联流量重置记录 - */ - public function trafficResetLogs(): HasMany - { - return $this->hasMany(TrafficResetLog::class, 'user_id', 'id'); - } - - /** - * 检查用户是否处于活跃状态 - */ - public function isActive(): bool - { - return !$this->banned && - ($this->expired_at === null || $this->expired_at > time()) && - $this->plan_id !== null; - } - - /** - * 检查用户是否可用节点流量且充足 - */ - public function isAvailable(): bool - { - return $this->isActive() && $this->getRemainingTraffic() > 0; - } - - /** - * 检查是否需要重置流量 - */ - public function shouldResetTraffic(): bool - { - return $this->isActive() && - $this->next_reset_at !== null && - $this->next_reset_at <= time(); - } - - /** - * 获取总使用流量 - */ - public function getTotalUsedTraffic(): int - { - return ($this->u ?? 0) + ($this->d ?? 0); - } - - /** - * 获取剩余流量 - */ - public function getRemainingTraffic(): int - { - $used = $this->getTotalUsedTraffic(); - $total = $this->transfer_enable ?? 0; - return max(0, $total - $used); - } - - /** - * 获取流量使用百分比 - */ - public function getTrafficUsagePercentage(): float - { - $total = $this->transfer_enable ?? 0; - if ($total <= 0) { - return 0; - } - - $used = $this->getTotalUsedTraffic(); - return min(100, ($used / $total) * 100); - } -} diff --git a/Xboard/app/Observers/PlanObserver.php b/Xboard/app/Observers/PlanObserver.php deleted file mode 100644 index 19dcef4..0000000 --- a/Xboard/app/Observers/PlanObserver.php +++ /dev/null @@ -1,35 +0,0 @@ -isDirty('reset_traffic_method')) { - return; - } - $trafficResetService = app(TrafficResetService::class); - User::where('plan_id', $plan->id) - ->where('banned', 0) - ->where(function ($query) { - $query->where('expired_at', '>', time()) - ->orWhereNull('expired_at'); - }) - ->lazyById(500) - ->each(function (User $user) use ($trafficResetService) { - $nextResetTime = $trafficResetService->calculateNextResetTime($user); - $user->update([ - 'next_reset_at' => $nextResetTime?->timestamp, - ]); - }); - } -} - diff --git a/Xboard/app/Observers/ServerObserver.php b/Xboard/app/Observers/ServerObserver.php deleted file mode 100644 index 0f1ce9d..0000000 --- a/Xboard/app/Observers/ServerObserver.php +++ /dev/null @@ -1,37 +0,0 @@ -isDirty([ - 'group_ids', - ]) - ) { - NodeSyncService::notifyUsersUpdatedByGroup($server->id); - } else if ( - $server->isDirty([ - 'server_port', - 'protocol_settings', - 'type', - 'route_ids', - 'custom_outbounds', - 'custom_routes', - 'cert_config', - ]) - ) { - NodeSyncService::notifyConfigUpdated($server->id); - } - } - - public function deleted(Server $server): void - { - NodeSyncService::notifyConfigUpdated($server->id); - } -} diff --git a/Xboard/app/Observers/ServerRouteObserver.php b/Xboard/app/Observers/ServerRouteObserver.php deleted file mode 100644 index f8457d7..0000000 --- a/Xboard/app/Observers/ServerRouteObserver.php +++ /dev/null @@ -1,31 +0,0 @@ -notifyAffectedNodes($route->id); - } - - public function deleted(ServerRoute $route): void - { - $this->notifyAffectedNodes($route->id); - } - - private function notifyAffectedNodes(int $routeId): void - { - $servers = Server::where('show', 1)->get()->filter( - fn ($s) => in_array($routeId, $s->route_ids ?? []) - ); - - foreach ($servers as $server) { - NodeSyncService::notifyConfigUpdated($server->id); - } - } -} diff --git a/Xboard/app/Observers/UserObserver.php b/Xboard/app/Observers/UserObserver.php deleted file mode 100644 index ffa74d0..0000000 --- a/Xboard/app/Observers/UserObserver.php +++ /dev/null @@ -1,53 +0,0 @@ -isDirty(['plan_id', 'expired_at'])) { - $this->recalculateNextResetAt($user); - } - - if ($user->isDirty(['group_id', 'uuid', 'speed_limit', 'device_limit', 'banned', 'expired_at', 'transfer_enable', 'u', 'd', 'plan_id'])) { - $oldGroupId = $user->isDirty('group_id') ? $user->getOriginal('group_id') : null; - NodeUserSyncJob::dispatch($user->id, 'updated', $oldGroupId); - } - } - - public function created(User $user): void - { - $this->recalculateNextResetAt($user); - NodeUserSyncJob::dispatch($user->id, 'created'); - } - - public function deleted(User $user): void - { - if ($user->group_id) { - NodeUserSyncJob::dispatch($user->id, 'deleted', $user->group_id); - } - } - - /** - * 根据当前用户状态重新计算 next_reset_at - */ - private function recalculateNextResetAt(User $user): void - { - $user->refresh(); - User::withoutEvents(function () use ($user) { - $nextResetTime = $this->trafficResetService->calculateNextResetTime($user); - $user->next_reset_at = $nextResetTime?->timestamp; - $user->save(); - }); - } -} \ No newline at end of file diff --git a/Xboard/app/Protocols/Clash.php b/Xboard/app/Protocols/Clash.php deleted file mode 100644 index f84d99f..0000000 --- a/Xboard/app/Protocols/Clash.php +++ /dev/null @@ -1,332 +0,0 @@ -servers; - $user = $this->user; - $appName = admin_setting('app_name', 'XBoard'); - - // 优先从数据库配置中获取模板 - $template = subscribe_template('clash'); - - $config = Yaml::parse($template); - $proxy = []; - $proxies = []; - - foreach ($servers as $item) { - - if ( - $item['type'] === Server::TYPE_SHADOWSOCKS - && in_array(data_get($item['protocol_settings'], 'cipher'), [ - 'aes-128-gcm', - 'aes-192-gcm', - 'aes-256-gcm', - 'chacha20-ietf-poly1305' - ]) - ) { - array_push($proxy, self::buildShadowsocks($item['password'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_VMESS) { - array_push($proxy, self::buildVmess($item['password'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_TROJAN) { - array_push($proxy, self::buildTrojan($item['password'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_SOCKS) { - array_push($proxy, self::buildSocks5($item['password'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_HTTP) { - array_push($proxy, self::buildHttp($item['password'], $item)); - array_push($proxies, $item['name']); - } - } - - $config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy); - foreach ($config['proxy-groups'] as $k => $v) { - if (!is_array($config['proxy-groups'][$k]['proxies'])) - $config['proxy-groups'][$k]['proxies'] = []; - $isFilter = false; - foreach ($config['proxy-groups'][$k]['proxies'] as $src) { - foreach ($proxies as $dst) { - if (!$this->isRegex($src)) - continue; - $isFilter = true; - $config['proxy-groups'][$k]['proxies'] = array_values(array_diff($config['proxy-groups'][$k]['proxies'], [$src])); - if ($this->isMatch($src, $dst)) { - array_push($config['proxy-groups'][$k]['proxies'], $dst); - } - } - if ($isFilter) - continue; - } - if ($isFilter) - continue; - $config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies); - } - - $config['proxy-groups'] = array_filter($config['proxy-groups'], function ($group) { - return $group['proxies']; - }); - $config['proxy-groups'] = array_values($config['proxy-groups']); - - $config = $this->buildRules($config); - - - $yaml = Yaml::dump($config, 2, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE); - $yaml = str_replace('$app_name', admin_setting('app_name', 'XBoard'), $yaml); - return response($yaml) - ->header('content-type', 'text/yaml') - ->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}") - ->header('profile-update-interval', '24') - ->header('content-disposition', 'attachment;filename*=UTF-8\'\'' . rawurlencode($appName)) - ->header('profile-web-page-url', admin_setting('app_url')); - } - - /** - * Build the rules for Clash. - */ - public function buildRules($config) - { - // Force the current subscription domain to be a direct rule - $subsDomain = request()->header('Host'); - if ($subsDomain) { - array_unshift($config['rules'], "DOMAIN,{$subsDomain},DIRECT"); - } - - return $config; - } - - public static function buildShadowsocks($uuid, $server) - { - $protocol_settings = $server['protocol_settings']; - $array = []; - $array['name'] = $server['name']; - $array['type'] = 'ss'; - $array['server'] = $server['host']; - $array['port'] = $server['port']; - $array['cipher'] = data_get($protocol_settings, 'cipher'); - $array['password'] = $uuid; - $array['udp'] = true; - if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) { - $plugin = data_get($protocol_settings, 'plugin'); - $pluginOpts = data_get($protocol_settings, 'plugin_opts', ''); - $array['plugin'] = $plugin; - - // 解析插件选项 - $parsedOpts = collect(explode(';', $pluginOpts)) - ->filter() - ->mapWithKeys(function ($pair) { - if (!str_contains($pair, '=')) { - return []; - } - [$key, $value] = explode('=', $pair, 2); - return [trim($key) => trim($value)]; - }) - ->all(); - - // 根据插件类型进行字段映射 - switch ($plugin) { - case 'obfs': - $array['plugin-opts'] = [ - 'mode' => $parsedOpts['obfs'] ?? data_get($protocol_settings, 'obfs', 'http'), - 'host' => $parsedOpts['obfs-host'] ?? data_get($protocol_settings, 'obfs_settings.host', ''), - ]; - - if (isset($parsedOpts['path'])) { - $array['plugin-opts']['path'] = $parsedOpts['path']; - } - break; - case 'v2ray-plugin': - $array['plugin-opts'] = [ - 'mode' => $parsedOpts['mode'] ?? 'websocket', - 'tls' => isset($parsedOpts['tls']) && $parsedOpts['tls'] == 'true', - 'host' => $parsedOpts['host'] ?? '', - 'path' => $parsedOpts['path'] ?? '/', - ]; - break; - default: - // 对于其他插件,直接使用解析出的键值对 - $array['plugin-opts'] = $parsedOpts; - } - } - return $array; - } - - public static function buildVmess($uuid, $server) - { - $protocol_settings = $server['protocol_settings']; - $array = []; - $array['name'] = $server['name']; - $array['type'] = 'vmess'; - $array['server'] = $server['host']; - $array['port'] = $server['port']; - $array['uuid'] = $uuid; - $array['alterId'] = 0; - $array['cipher'] = 'auto'; - $array['udp'] = true; - - if (data_get($protocol_settings, 'tls')) { - $array['tls'] = true; - $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure'); - if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { - $array['servername'] = $serverName; - } - } - - switch (data_get($protocol_settings, 'network')) { - case 'tcp': - $headerType = data_get($protocol_settings, 'network_settings.header.type', 'none'); - $array['network'] = ($headerType === 'http') ? 'http' : 'tcp'; - if ($headerType === 'http') { - if ($httpOpts = array_filter([ - 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'), - 'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/']) - ])) { - $array['http-opts'] = $httpOpts; - } - } - break; - case 'ws': - $array['network'] = 'ws'; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $array['ws-opts']['path'] = $path; - if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) - $array['ws-opts']['headers'] = ['Host' => $host]; - break; - case 'grpc': - $array['network'] = 'grpc'; - if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName')) - $array['grpc-opts']['grpc-service-name'] = $serviceName; - break; - default: - break; - } - return $array; - } - - public static function buildTrojan($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $array = []; - $array['name'] = $server['name']; - $array['type'] = 'trojan'; - $array['server'] = $server['host']; - $array['port'] = $server['port']; - $array['password'] = $password; - $array['udp'] = true; - if ($serverName = data_get($protocol_settings, 'server_name')) { - $array['sni'] = $serverName; - } - $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'allow_insecure'); - - switch (data_get($protocol_settings, 'network')) { - case 'tcp': - $array['network'] = 'tcp'; - break; - case 'ws': - $array['network'] = 'ws'; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $array['ws-opts']['path'] = $path; - if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) - $array['ws-opts']['headers'] = ['Host' => $host]; - break; - case 'grpc': - $array['network'] = 'grpc'; - if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName')) - $array['grpc-opts']['grpc-service-name'] = $serviceName; - break; - default: - $array['network'] = 'tcp'; - break; - } - return $array; - } - - public static function buildSocks5($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $array = []; - $array['name'] = $server['name']; - $array['type'] = 'socks5'; - $array['server'] = $server['host']; - $array['port'] = $server['port']; - $array['udp'] = true; - - $array['username'] = $password; - $array['password'] = $password; - - // TLS 配置 - if (data_get($protocol_settings, 'tls')) { - $array['tls'] = true; - $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false); - } - - return $array; - } - - public static function buildHttp($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $array = []; - $array['name'] = $server['name']; - $array['type'] = 'http'; - $array['server'] = $server['host']; - $array['port'] = $server['port']; - - $array['username'] = $password; - $array['password'] = $password; - - // TLS 配置 - if (data_get($protocol_settings, 'tls')) { - $array['tls'] = true; - $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false); - } - - return $array; - } - - private function isMatch($exp, $str) - { - try { - return preg_match($exp, $str) === 1; - } catch (\Exception $e) { - return false; - } - } - - private function isRegex($exp) - { - if (empty($exp)) { - return false; - } - try { - return preg_match($exp, '') !== false; - } catch (\Exception $e) { - return false; - } - } -} diff --git a/Xboard/app/Protocols/ClashMeta.php b/Xboard/app/Protocols/ClashMeta.php deleted file mode 100644 index feda7d0..0000000 --- a/Xboard/app/Protocols/ClashMeta.php +++ /dev/null @@ -1,708 +0,0 @@ - [ - 'whitelist' => [ - 'tcp' => '0.0.0', - 'ws' => '0.0.0', - 'grpc' => '0.0.0', - 'http' => '0.0.0', - 'h2' => '0.0.0', - 'httpupgrade' => '0.0.0', - ], - 'strict' => true, - ], - 'nekobox.hysteria.protocol_settings.version' => [ - 1 => '0.0.0', - 2 => '1.2.7', - ], - 'clashmetaforandroid.hysteria.protocol_settings.version' => [ - 2 => '2.9.0', - ], - 'nekoray.hysteria.protocol_settings.version' => [ - 2 => '3.24', - ], - 'verge.hysteria.protocol_settings.version' => [ - 2 => '1.3.8', - ], - 'ClashX Meta.hysteria.protocol_settings.version' => [ - 2 => '1.3.5', - ], - 'flclash.hysteria.protocol_settings.version' => [ - 2 => '0.8.0', - ], - ]; - - public function handle() - { - $servers = $this->servers; - $user = $this->user; - $appName = admin_setting('app_name', 'XBoard'); - - $template = subscribe_template('clashmeta'); - - $config = Yaml::parse($template); - $proxy = []; - $proxies = []; - - foreach ($servers as $item) { - if ($item['type'] === Server::TYPE_SHADOWSOCKS) { - array_push($proxy, self::buildShadowsocks($item['password'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_VMESS) { - array_push($proxy, self::buildVmess($item['password'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_TROJAN) { - array_push($proxy, self::buildTrojan($item['password'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_VLESS) { - array_push($proxy, self::buildVless($item['password'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_HYSTERIA) { - array_push($proxy, self::buildHysteria($item['password'], $item, $user)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_TUIC) { - array_push($proxy, self::buildTuic($item['password'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_ANYTLS) { - array_push($proxy, self::buildAnyTLS($item['password'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_SOCKS) { - array_push($proxy, self::buildSocks5($item['password'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_HTTP) { - array_push($proxy, self::buildHttp($item['password'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_MIERU) { - array_push($proxy, self::buildMieru($item['password'], $item)); - array_push($proxies, $item['name']); - } - } - - $config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy); - foreach ($config['proxy-groups'] as $k => $v) { - if (!is_array($config['proxy-groups'][$k]['proxies'])) - $config['proxy-groups'][$k]['proxies'] = []; - $isFilter = false; - foreach ($config['proxy-groups'][$k]['proxies'] as $src) { - foreach ($proxies as $dst) { - if (!$this->isRegex($src)) - continue; - $isFilter = true; - $config['proxy-groups'][$k]['proxies'] = array_values(array_diff($config['proxy-groups'][$k]['proxies'], [$src])); - if ($this->isMatch($src, $dst)) { - array_push($config['proxy-groups'][$k]['proxies'], $dst); - } - } - if ($isFilter) - continue; - } - if ($isFilter) - continue; - $config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies); - } - $config['proxy-groups'] = array_filter($config['proxy-groups'], function ($group) { - return $group['proxies']; - }); - $config['proxy-groups'] = array_values($config['proxy-groups']); - $config = $this->buildRules($config); - - $yaml = Yaml::dump($config, 2, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE); - $yaml = str_replace('$app_name', admin_setting('app_name', 'XBoard'), $yaml); - return response($yaml) - ->header('content-type', 'text/yaml') - ->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}") - ->header('profile-update-interval', '24') - ->header('content-disposition', 'attachment;filename*=UTF-8\'\'' . rawurlencode($appName)); - } - - /** - * Build the rules for Clash. - */ - public function buildRules($config) - { - // Force the current subscription domain to be a direct rule - $subsDomain = request()->header('Host'); - if ($subsDomain) { - array_unshift($config['rules'], "DOMAIN,{$subsDomain},DIRECT"); - } - // // Force the nodes ip to be a direct rule - // collect($this->servers)->pluck('host')->map(function ($host) { - // $host = trim($host); - // return filter_var($host, FILTER_VALIDATE_IP) ? [$host] : Helper::getIpByDomainName($host); - // })->flatten()->unique()->each(function ($nodeIP) use (&$config) { - // array_unshift($config['rules'], "IP-CIDR,{$nodeIP}/32,DIRECT,no-resolve"); - // }); - - return $config; - } - - public static function buildShadowsocks($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $array = []; - $array['name'] = $server['name']; - $array['type'] = 'ss'; - $array['server'] = $server['host']; - $array['port'] = $server['port']; - $array['cipher'] = data_get($server['protocol_settings'], 'cipher'); - $array['password'] = data_get($server, 'password', $password); - $array['udp'] = true; - if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) { - $plugin = data_get($protocol_settings, 'plugin'); - $pluginOpts = data_get($protocol_settings, 'plugin_opts', ''); - $array['plugin'] = $plugin; - - // 解析插件选项 - $parsedOpts = collect(explode(';', $pluginOpts)) - ->filter() - ->mapWithKeys(function ($pair) { - if (!str_contains($pair, '=')) { - return [trim($pair) => true]; - } - [$key, $value] = explode('=', $pair, 2); - return [trim($key) => trim($value)]; - }) - ->all(); - - // 根据插件类型进行字段映射 - switch ($plugin) { - case 'obfs': - case 'obfs-local': - $array['plugin'] = 'obfs'; - $array['plugin-opts'] = array_filter([ - 'mode' => $parsedOpts['obfs'] ?? ($parsedOpts['mode'] ?? 'http'), - 'host' => $parsedOpts['obfs-host'] ?? ($parsedOpts['host'] ?? 'www.bing.com'), - ]); - break; - - case 'v2ray-plugin': - $array['plugin-opts'] = array_filter([ - 'mode' => $parsedOpts['mode'] ?? 'websocket', - 'tls' => isset($parsedOpts['tls']) || isset($parsedOpts['server']), - 'host' => $parsedOpts['host'] ?? null, - 'path' => $parsedOpts['path'] ?? '/', - 'mux' => isset($parsedOpts['mux']) ? true : null, - 'headers' => isset($parsedOpts['host']) ? ['Host' => $parsedOpts['host']] : null - ], fn($v) => $v !== null); - break; - - case 'shadow-tls': - $array['plugin-opts'] = array_filter([ - 'host' => $parsedOpts['host'] ?? null, - 'password' => $parsedOpts['password'] ?? null, - 'version' => isset($parsedOpts['version']) ? (int) $parsedOpts['version'] : 2 - ], fn($v) => $v !== null); - break; - - case 'restls': - $array['plugin-opts'] = array_filter([ - 'host' => $parsedOpts['host'] ?? null, - 'password' => $parsedOpts['password'] ?? null, - 'restls-script' => $parsedOpts['restls-script'] ?? '123' - ], fn($v) => $v !== null); - break; - - default: - $array['plugin-opts'] = $parsedOpts; - } - } - return $array; - } - - public static function buildVmess($uuid, $server) - { - $protocol_settings = data_get($server, 'protocol_settings', []); - $array = [ - 'name' => $server['name'], - 'type' => 'vmess', - 'server' => $server['host'], - 'port' => $server['port'], - 'uuid' => $uuid, - 'alterId' => 0, - 'cipher' => 'auto', - 'udp' => true - ]; - - if (data_get($protocol_settings, 'tls')) { - $array['tls'] = (bool) data_get($protocol_settings, 'tls'); - $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false); - $array['servername'] = data_get($protocol_settings, 'tls_settings.server_name'); - } - - self::appendUtls($array, $protocol_settings); - self::appendMultiplex($array, $protocol_settings); - - switch (data_get($protocol_settings, 'network')) { - case 'tcp': - $headerType = data_get($protocol_settings, 'network_settings.header.type', 'none'); - $array['network'] = ($headerType === 'http') ? 'http' : 'tcp'; - if ($headerType === 'http') { - if ( - $httpOpts = array_filter([ - 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'), - 'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/']) - ]) - ) { - $array['http-opts'] = $httpOpts; - } - } - break; - case 'ws': - $array['network'] = 'ws'; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $array['ws-opts']['path'] = $path; - if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) - $array['ws-opts']['headers'] = ['Host' => $host]; - break; - case 'grpc': - $array['network'] = 'grpc'; - if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName')) - $array['grpc-opts']['grpc-service-name'] = $serviceName; - break; - case 'h2': - $array['network'] = 'h2'; - $array['h2-opts'] = []; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $array['h2-opts']['path'] = $path; - if ($host = data_get($protocol_settings, 'network_settings.host')) - $array['h2-opts']['host'] = is_array($host) ? $host : [$host]; - break; - case 'httpupgrade': - $array['network'] = 'ws'; - $array['ws-opts'] = ['v2ray-http-upgrade' => true]; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $array['ws-opts']['path'] = $path; - if ($host = data_get($protocol_settings, 'network_settings.host')) - $array['ws-opts']['headers'] = ['Host' => $host]; - break; - default: - break; - } - - return $array; - } - - public static function buildVless($password, $server) - { - $protocol_settings = data_get($server, 'protocol_settings', []); - $array = [ - 'name' => $server['name'], - 'type' => 'vless', - 'server' => $server['host'], - 'port' => $server['port'], - 'uuid' => $password, - 'alterId' => 0, - 'cipher' => 'auto', - 'udp' => true, - 'flow' => data_get($protocol_settings, 'flow'), - 'encryption' => match (data_get($protocol_settings, 'encryption.enabled')) { - true => data_get($protocol_settings, 'encryption.encryption', 'none'), - default => 'none' - }, - 'tls' => false - ]; - - switch (data_get($protocol_settings, 'tls')) { - case 1: - $array['tls'] = true; - $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false); - if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { - $array['servername'] = $serverName; - } - self::appendUtls($array, $protocol_settings); - break; - case 2: - $array['tls'] = true; - $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'reality_settings.allow_insecure', false); - $array['servername'] = data_get($protocol_settings, 'reality_settings.server_name'); - $array['reality-opts'] = [ - 'public-key' => data_get($protocol_settings, 'reality_settings.public_key'), - 'short-id' => data_get($protocol_settings, 'reality_settings.short_id') - ]; - self::appendUtls($array, $protocol_settings); - break; - default: - break; - } - - switch (data_get($protocol_settings, 'network')) { - case 'tcp': - $array['network'] = 'tcp'; - $headerType = data_get($protocol_settings, 'network_settings.header.type', 'none'); - if ($headerType === 'http') { - $array['network'] = 'http'; - if ( - $httpOpts = array_filter([ - 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'), - 'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/']) - ]) - ) { - $array['http-opts'] = $httpOpts; - } - } - break; - case 'ws': - $array['network'] = 'ws'; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $array['ws-opts']['path'] = $path; - if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) - $array['ws-opts']['headers'] = ['Host' => $host]; - break; - case 'grpc': - $array['network'] = 'grpc'; - if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName')) - $array['grpc-opts']['grpc-service-name'] = $serviceName; - break; - case 'h2': - $array['network'] = 'h2'; - $array['h2-opts'] = []; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $array['h2-opts']['path'] = $path; - if ($host = data_get($protocol_settings, 'network_settings.host')) - $array['h2-opts']['host'] = is_array($host) ? $host : [$host]; - break; - case 'httpupgrade': - $array['network'] = 'ws'; - $array['ws-opts'] = ['v2ray-http-upgrade' => true]; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $array['ws-opts']['path'] = $path; - if ($host = data_get($protocol_settings, 'network_settings.host')) - $array['ws-opts']['headers'] = ['Host' => $host]; - break; - default: - break; - } - - self::appendMultiplex($array, $protocol_settings); - - return $array; - } - - public static function buildTrojan($password, $server) - { - $protocol_settings = data_get($server, 'protocol_settings', []); - $array = [ - 'name' => $server['name'], - 'type' => 'trojan', - 'server' => $server['host'], - 'port' => $server['port'], - 'password' => $password, - 'udp' => true, - ]; - - $tlsMode = (int) data_get($protocol_settings, 'tls', 1); - switch ($tlsMode) { - case 2: // Reality - $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'reality_settings.allow_insecure', false); - if ($serverName = data_get($protocol_settings, 'reality_settings.server_name')) { - $array['sni'] = $serverName; - } - $array['reality-opts'] = [ - 'public-key' => data_get($protocol_settings, 'reality_settings.public_key'), - 'short-id' => data_get($protocol_settings, 'reality_settings.short_id'), - ]; - break; - default: // Standard TLS - $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'allow_insecure', false); - if ($serverName = data_get($protocol_settings, 'server_name')) { - $array['sni'] = $serverName; - } - break; - } - - self::appendUtls($array, $protocol_settings); - self::appendMultiplex($array, $protocol_settings); - - switch (data_get($protocol_settings, 'network')) { - case 'tcp': - $array['network'] = 'tcp'; - break; - case 'ws': - $array['network'] = 'ws'; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $array['ws-opts']['path'] = $path; - if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) - $array['ws-opts']['headers'] = ['Host' => $host]; - break; - case 'grpc': - $array['network'] = 'grpc'; - if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName')) - $array['grpc-opts']['grpc-service-name'] = $serviceName; - break; - case 'h2': - $array['network'] = 'h2'; - $array['h2-opts'] = []; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $array['h2-opts']['path'] = $path; - if ($host = data_get($protocol_settings, 'network_settings.host')) - $array['h2-opts']['host'] = is_array($host) ? $host : [$host]; - break; - case 'httpupgrade': - $array['network'] = 'ws'; - $array['ws-opts'] = ['v2ray-http-upgrade' => true]; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $array['ws-opts']['path'] = $path; - if ($host = data_get($protocol_settings, 'network_settings.host')) - $array['ws-opts']['headers'] = ['Host' => $host]; - break; - default: - $array['network'] = 'tcp'; - break; - } - - return $array; - } - - public static function buildHysteria($password, $server, $user) - { - $protocol_settings = data_get($server, 'protocol_settings', []); - $array = [ - 'name' => $server['name'], - 'server' => $server['host'], - 'port' => $server['port'], - 'sni' => data_get($protocol_settings, 'tls.server_name'), - 'up' => data_get($protocol_settings, 'bandwidth.up'), - 'down' => data_get($protocol_settings, 'bandwidth.down'), - 'skip-cert-verify' => (bool) data_get($protocol_settings, 'tls.allow_insecure', false), - ]; - if (isset($server['ports'])) { - $array['ports'] = $server['ports']; - } - if ($hopInterval = data_get($protocol_settings, 'hop_interval')) { - $array['hop-interval'] = (int) $hopInterval; - } - switch (data_get($protocol_settings, 'version')) { - case 1: - $array['type'] = 'hysteria'; - $array['auth_str'] = $password; - $array['protocol'] = 'udp'; // 支持 udp/wechat-video/faketcp - if (data_get($protocol_settings, 'obfs.open')) { - $array['obfs'] = data_get($protocol_settings, 'obfs.password'); - } - $array['fast-open'] = true; - $array['disable_mtu_discovery'] = true; - break; - case 2: - $array['type'] = 'hysteria2'; - $array['password'] = $password; - if (data_get($protocol_settings, 'obfs.open')) { - $array['obfs'] = data_get($protocol_settings, 'obfs.type'); - $array['obfs-password'] = data_get($protocol_settings, 'obfs.password'); - } - break; - } - - return $array; - } - - public static function buildTuic($password, $server) - { - $protocol_settings = data_get($server, 'protocol_settings', []); - $array = [ - 'name' => $server['name'], - 'type' => 'tuic', - 'server' => $server['host'], - 'port' => $server['port'], - 'udp' => true, - ]; - - if (data_get($protocol_settings, 'version') === 4) { - $array['token'] = $password; - } else { - $array['uuid'] = $password; - $array['password'] = $password; - } - - $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls.allow_insecure', false); - if ($serverName = data_get($protocol_settings, 'tls.server_name')) { - $array['sni'] = $serverName; - } - - if ($alpn = data_get($protocol_settings, 'alpn')) { - $array['alpn'] = $alpn; - } - - $array['congestion-controller'] = data_get($protocol_settings, 'congestion_control', 'cubic'); - $array['udp-relay-mode'] = data_get($protocol_settings, 'udp_relay_mode', 'native'); - - return $array; - } - - public static function buildAnyTLS($password, $server) - { - - $protocol_settings = data_get($server, 'protocol_settings', []); - $array = [ - 'name' => $server['name'], - 'type' => 'anytls', - 'server' => $server['host'], - 'port' => $server['port'], - 'password' => $password, - 'udp' => true, - ]; - - if ($serverName = data_get($protocol_settings, 'tls.server_name')) { - $array['sni'] = $serverName; - } - if ($allowInsecure = data_get($protocol_settings, 'tls.allow_insecure')) { - $array['skip-cert-verify'] = (bool) $allowInsecure; - } - - return $array; - } - - public static function buildMieru($password, $server) - { - $protocol_settings = data_get($server, 'protocol_settings', []); - $array = [ - 'name' => $server['name'], - 'type' => 'mieru', - 'server' => $server['host'], - 'port' => $server['port'], - 'username' => $password, - 'password' => $password, - 'transport' => strtoupper(data_get($protocol_settings, 'transport', 'TCP')) - ]; - - // 如果配置了端口范围 - if (isset($server['ports'])) { - $array['port-range'] = $server['ports']; - } - - return $array; - } - - public static function buildSocks5($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $array = []; - $array['name'] = $server['name']; - $array['type'] = 'socks5'; - $array['server'] = $server['host']; - $array['port'] = $server['port']; - $array['udp'] = true; - - $array['username'] = $password; - $array['password'] = $password; - - // TLS 配置 - if (data_get($protocol_settings, 'tls')) { - $array['tls'] = true; - $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false); - } - - return $array; - } - - public static function buildHttp($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $array = []; - $array['name'] = $server['name']; - $array['type'] = 'http'; - $array['server'] = $server['host']; - $array['port'] = $server['port']; - - $array['username'] = $password; - $array['password'] = $password; - - // TLS 配置 - if (data_get($protocol_settings, 'tls')) { - $array['tls'] = true; - $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false); - } - - return $array; - } - - private function isMatch($exp, $str) - { - try { - return preg_match($exp, $str) === 1; - } catch (\Exception $e) { - return false; - } - } - - private function isRegex($exp) - { - if (empty($exp)) { - return false; - } - try { - return preg_match($exp, '') !== false; - } catch (\Exception $e) { - return false; - } - } - - protected static function appendMultiplex(&$array, $protocol_settings) - { - if ($multiplex = data_get($protocol_settings, 'multiplex')) { - if (data_get($multiplex, 'enabled')) { - $array['smux'] = array_filter([ - 'enabled' => true, - 'protocol' => data_get($multiplex, 'protocol', 'yamux'), - 'max-connections' => data_get($multiplex, 'max_connections'), - // 'min-streams' => data_get($multiplex, 'min_streams'), - // 'max-streams' => data_get($multiplex, 'max_streams'), - 'padding' => data_get($multiplex, 'padding') ? true : null, - ]); - - if (data_get($multiplex, 'brutal.enabled')) { - $array['smux']['brutal-opts'] = [ - 'enabled' => true, - 'up' => data_get($multiplex, 'brutal.up_mbps'), - 'down' => data_get($multiplex, 'brutal.down_mbps'), - ]; - } - } - } - } - - protected static function appendUtls(&$array, $protocol_settings) - { - if ($utls = data_get($protocol_settings, 'utls')) { - if (data_get($utls, 'enabled')) { - $array['client-fingerprint'] = Helper::getTlsFingerprint($utls); - } - } - } -} \ No newline at end of file diff --git a/Xboard/app/Protocols/General.php b/Xboard/app/Protocols/General.php deleted file mode 100644 index 5071027..0000000 --- a/Xboard/app/Protocols/General.php +++ /dev/null @@ -1,448 +0,0 @@ - [2 => '1.9.5'], - 'v2rayn.hysteria.protocol_settings.version' => [2 => '6.31'], - ]; - - public function handle() - { - $servers = $this->servers; - $user = $this->user; - $uri = ''; - - foreach ($servers as $item) { - $uri .= match ($item['type']) { - Server::TYPE_VMESS => self::buildVmess($item['password'], $item), - Server::TYPE_VLESS => self::buildVless($item['password'], $item), - Server::TYPE_SHADOWSOCKS => self::buildShadowsocks($item['password'], $item), - Server::TYPE_TROJAN => self::buildTrojan($item['password'], $item), - Server::TYPE_HYSTERIA => self::buildHysteria($item['password'], $item), - Server::TYPE_ANYTLS => self::buildAnyTLS($item['password'], $item), - Server::TYPE_SOCKS => self::buildSocks($item['password'], $item), - Server::TYPE_TUIC => self::buildTuic($item['password'], $item), - Server::TYPE_HTTP => self::buildHttp($item['password'], $item), - default => '', - }; - } - return response(base64_encode($uri)) - ->header('content-type', 'text/plain') - ->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}"); - } - - public static function buildShadowsocks($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $name = rawurlencode($server['name']); - $password = data_get($server, 'password', $password); - $str = str_replace( - ['+', '/', '='], - ['-', '_', ''], - base64_encode(data_get($protocol_settings, 'cipher') . ":{$password}") - ); - $addr = Helper::wrapIPv6($server['host']); - $plugin = data_get($protocol_settings, 'plugin'); - $plugin_opts = data_get($protocol_settings, 'plugin_opts'); - $url = "ss://{$str}@{$addr}:{$server['port']}"; - if ($plugin && $plugin_opts) { - $url .= '/?' . 'plugin=' . rawurlencode($plugin . ';' . $plugin_opts); - } - $url .= "#{$name}\r\n"; - return $url; - } - - public static function buildVmess($uuid, $server) - { - $protocol_settings = $server['protocol_settings']; - $config = [ - "v" => "2", - "ps" => $server['name'], - "add" => $server['host'], - "port" => (string) $server['port'], - "id" => $uuid, - "aid" => '0', - "net" => data_get($server, 'protocol_settings.network'), - "type" => "none", - "host" => "", - "path" => "", - "tls" => data_get($protocol_settings, 'tls') ? "tls" : "", - ]; - if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { - $config['sni'] = $serverName; - } - if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) { - $config['fp'] = $fp; - } - - switch (data_get($protocol_settings, 'network')) { - case 'tcp': - if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') { - $config['type'] = data_get($protocol_settings, 'network_settings.header.type', 'http'); - $config['path'] = Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])); - $config['host'] = - data_get($protocol_settings, 'network_settings.header.request.headers.Host') - ? Arr::random(data_get($protocol_settings, 'network_settings.header.request.headers.Host', ['/']), ) - : null; - } - break; - case 'ws': - $config['type'] = 'ws'; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $config['path'] = $path; - if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) - $config['host'] = $host; - break; - case 'grpc': - $config['type'] = 'grpc'; - if ($path = data_get($protocol_settings, 'network_settings.serviceName')) - $config['path'] = $path; - break; - case 'h2': - $config['net'] = 'h2'; - $config['type'] = 'h2'; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $config['path'] = $path; - if ($host = data_get($protocol_settings, 'network_settings.host')) - $config['host'] = is_array($host) ? implode(',', $host) : $host; - break; - case 'httpupgrade': - $config['net'] = 'httpupgrade'; - $config['type'] = 'httpupgrade'; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $config['path'] = $path; - $config['host'] = data_get($protocol_settings, 'network_settings.host', $server['host']); - break; - default: - break; - } - return "vmess://" . base64_encode(json_encode($config)) . "\r\n"; - } - - public static function buildVless($uuid, $server) - { - $protocol_settings = $server['protocol_settings']; - $host = $server['host']; //节点地址 - $port = $server['port']; //节点端口 - $name = $server['name']; //节点名称 - - $config = [ - 'mode' => 'multi', //grpc传输模式 - 'security' => '', //传输层安全 tls/reality - 'encryption' => match (data_get($protocol_settings, 'encryption.enabled')) { - true => data_get($protocol_settings, 'encryption.encryption', 'none'), - default => 'none' - }, - 'type' => data_get($server, 'protocol_settings.network'), //传输协议 - 'flow' => data_get($protocol_settings, 'flow'), - ]; - // 处理TLS - switch (data_get($server, 'protocol_settings.tls')) { - case 1: - $config['security'] = "tls"; - if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) { - $config['fp'] = $fp; - } - if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { - $config['sni'] = $serverName; - } - if (data_get($protocol_settings, 'tls_settings.allow_insecure')) { - $config['allowInsecure'] = '1'; - } - break; - case 2: //reality - $config['security'] = "reality"; - $config['pbk'] = data_get($protocol_settings, 'reality_settings.public_key'); - $config['sid'] = data_get($protocol_settings, 'reality_settings.short_id'); - $config['sni'] = data_get($protocol_settings, 'reality_settings.server_name'); - $config['servername'] = data_get($protocol_settings, 'reality_settings.server_name'); - $config['spx'] = "/"; - if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) { - $config['fp'] = $fp; - } - break; - default: - break; - } - // 处理传输协议 - switch (data_get($server, 'protocol_settings.network')) { - case 'ws': - if ($path = data_get($protocol_settings, 'network_settings.path')) - $config['path'] = $path; - if ($wsHost = data_get($protocol_settings, 'network_settings.headers.Host')) - $config['host'] = $wsHost; - break; - case 'grpc': - if ($path = data_get($protocol_settings, 'network_settings.serviceName')) - $config['serviceName'] = $path; - break; - case 'h2': - $config['type'] = 'http'; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $config['path'] = $path; - if ($h2Host = data_get($protocol_settings, 'network_settings.host')) - $config['host'] = is_array($h2Host) ? implode(',', $h2Host) : $h2Host; - break; - case 'kcp': - if ($path = data_get($protocol_settings, 'network_settings.seed')) - $config['path'] = $path; - $config['type'] = data_get($protocol_settings, 'network_settings.header.type', 'none'); - break; - case 'httpupgrade': - if ($path = data_get($protocol_settings, 'network_settings.path')) - $config['path'] = $path; - $config['host'] = data_get($protocol_settings, 'network_settings.host', $server['host']); - break; - case 'xhttp': - $config['path'] = data_get($protocol_settings, 'network_settings.path'); - $config['host'] = data_get($protocol_settings, 'network_settings.host', $server['host']); - $config['mode'] = data_get($protocol_settings, 'network_settings.mode', 'auto'); - $config['extra'] = json_encode(data_get($protocol_settings, 'network_settings.extra')); - break; - } - - $user = $uuid . '@' . Helper::wrapIPv6($host) . ':' . $port; - $query = http_build_query($config); - $fragment = urlencode($name); - $link = sprintf("vless://%s?%s#%s\r\n", $user, $query, $fragment); - return $link; - } - - public static function buildTrojan($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $name = rawurlencode($server['name']); - $array = []; - $tlsMode = (int) data_get($protocol_settings, 'tls', 1); - - switch ($tlsMode) { - case 2: // Reality - $array['security'] = 'reality'; - $array['pbk'] = data_get($protocol_settings, 'reality_settings.public_key'); - $array['sid'] = data_get($protocol_settings, 'reality_settings.short_id'); - $array['sni'] = data_get($protocol_settings, 'reality_settings.server_name'); - if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) { - $array['fp'] = $fp; - } - break; - default: // Standard TLS - $array['allowInsecure'] = data_get($protocol_settings, 'allow_insecure', false); - if ($serverName = data_get($protocol_settings, 'server_name')) { - $array['peer'] = $serverName; - $array['sni'] = $serverName; - } - if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) { - $array['fp'] = $fp; - } - break; - } - - switch (data_get($server, 'protocol_settings.network')) { - case 'ws': - $array['type'] = 'ws'; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $array['path'] = $path; - if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) - $array['host'] = $host; - break; - case 'grpc': - // Follow V2rayN family standards - $array['type'] = 'grpc'; - if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName')) - $array['serviceName'] = $serviceName; - break; - case 'h2': - $array['type'] = 'http'; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $array['path'] = $path; - if ($host = data_get($protocol_settings, 'network_settings.host')) - $array['host'] = is_array($host) ? implode(',', $host) : $host; - break; - case 'httpupgrade': - $array['type'] = 'httpupgrade'; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $array['path'] = $path; - $array['host'] = data_get($protocol_settings, 'network_settings.host', $server['host']); - break; - default: - break; - } - $query = http_build_query($array); - $addr = Helper::wrapIPv6($server['host']); - - $uri = "trojan://{$password}@{$addr}:{$server['port']}?{$query}#{$name}"; - $uri .= "\r\n"; - return $uri; - } - - public static function buildHysteria($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $params = []; - $version = data_get($protocol_settings, 'version', 2); - - if ($serverName = data_get($protocol_settings, 'tls.server_name')) { - $params['sni'] = $serverName; - } - $params['insecure'] = data_get($protocol_settings, 'tls.allow_insecure') ? '1' : '0'; - - $name = rawurlencode($server['name']); - $addr = Helper::wrapIPv6($server['host']); - - if ($version === 2) { - if (data_get($protocol_settings, 'obfs.open')) { - $params['obfs'] = 'salamander'; - $params['obfs-password'] = data_get($protocol_settings, 'obfs.password'); - } - if (isset($server['ports'])) { - $params['mport'] = $server['ports']; - } - - $query = http_build_query($params); - $uri = "hysteria2://{$password}@{$addr}:{$server['port']}?{$query}#{$name}"; - } else { - $params['protocol'] = 'udp'; - $params['auth'] = $password; - if ($upMbps = data_get($protocol_settings, 'bandwidth.up')) - $params['upmbps'] = $upMbps; - if ($downMbps = data_get($protocol_settings, 'bandwidth.down')) - $params['downmbps'] = $downMbps; - if (data_get($protocol_settings, 'obfs.open') && ($obfsPassword = data_get($protocol_settings, 'obfs.password'))) { - $params['obfs'] = 'xplus'; - $params['obfsParam'] = $obfsPassword; - } - - $query = http_build_query($params); - $uri = "hysteria://{$addr}:{$server['port']}?{$query}#{$name}"; - } - $uri .= "\r\n"; - - return $uri; - } - - - public static function buildTuic($password, $server) - { - $protocol_settings = data_get($server, 'protocol_settings', []); - $name = rawurlencode($server['name']); - $addr = Helper::wrapIPv6($server['host']); - $port = $server['port']; - $uuid = $password; // v2rayN格式里,uuid和password都是密码部分 - $pass = $password; - - $queryParams = []; - - // 填充sni参数 - if ($sni = data_get($protocol_settings, 'tls.server_name')) { - $queryParams['sni'] = $sni; - } - - // alpn参数,支持多值时用逗号连接 - if ($alpn = data_get($protocol_settings, 'alpn')) { - if (is_array($alpn)) { - $queryParams['alpn'] = implode(',', $alpn); - } else { - $queryParams['alpn'] = $alpn; - } - } - - // congestion_controller参数,默认cubic - $congestion = data_get($protocol_settings, 'congestion_control', 'cubic'); - $queryParams['congestion_control'] = $congestion; - - // udp_relay_mode参数,默认native - $udpRelay = data_get($protocol_settings, 'udp_relay_mode', 'native'); - $queryParams['udp-relay-mode'] = $udpRelay; - - if (data_get($protocol_settings, 'tls.allow_insecure')) { - $queryParams['insecure'] = '1'; - } - - $query = http_build_query($queryParams); - - // 构造完整URI,格式: - // Tuic://uuid:password@host:port?sni=xxx&alpn=xxx&congestion_controller=xxx&udp_relay_mode=xxx#别名 - $uri = "tuic://{$uuid}:{$pass}@{$addr}:{$port}"; - - if (!empty($query)) { - $uri .= "?{$query}"; - } - - $uri .= "#{$name}\r\n"; - - return $uri; - } - - - - - - public static function buildAnyTLS($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $name = rawurlencode($server['name']); - $params = [ - 'sni' => data_get($protocol_settings, 'tls.server_name'), - 'insecure' => data_get($protocol_settings, 'tls.allow_insecure') - ]; - $query = http_build_query($params); - $addr = Helper::wrapIPv6($server['host']); - $uri = "anytls://{$password}@{$addr}:{$server['port']}?{$query}#{$name}"; - $uri .= "\r\n"; - return $uri; - } - - public static function buildSocks($password, $server) - { - $name = rawurlencode($server['name']); - $credentials = base64_encode("{$password}:{$password}"); - $addr = Helper::wrapIPv6($server['host']); - return "socks://{$credentials}@{$addr}:{$server['port']}#{$name}\r\n"; - } - - public static function buildHttp($password, $server) - { - $protocol_settings = data_get($server, 'protocol_settings', []); - $name = rawurlencode($server['name']); - $addr = Helper::wrapIPv6($server['host']); - $credentials = base64_encode("{$password}:{$password}"); - - $params = []; - if (data_get($protocol_settings, 'tls')) { - $params['security'] = 'tls'; - if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { - $params['sni'] = $serverName; - } - $params['allowInsecure'] = data_get($protocol_settings, 'tls_settings.allow_insecure') ? '1' : '0'; - } - - $uri = "http://{$credentials}@{$addr}:{$server['port']}"; - if (!empty($params)) { - $uri .= '?' . http_build_query($params); - } - $uri .= "#{$name}\r\n"; - return $uri; - } -} diff --git a/Xboard/app/Protocols/Loon.php b/Xboard/app/Protocols/Loon.php deleted file mode 100644 index 56df854..0000000 --- a/Xboard/app/Protocols/Loon.php +++ /dev/null @@ -1,357 +0,0 @@ - [2 => '637'], - 'loon.trojan.protocol_settings.tls' => [0 => '3.2.1', 1 => '3.2.1',2 => '999.9.9'], - ]; - - public function handle() - { - $servers = $this->servers; - $user = $this->user; - - $uri = ''; - - foreach ($servers as $item) { - if ( - $item['type'] === Server::TYPE_SHADOWSOCKS - ) { - $uri .= self::buildShadowsocks($item['password'], $item); - } - if ($item['type'] === Server::TYPE_VMESS) { - $uri .= self::buildVmess($item['password'], $item); - } - if ($item['type'] === Server::TYPE_TROJAN) { - $uri .= self::buildTrojan($item['password'], $item); - } - if ($item['type'] === Server::TYPE_HYSTERIA) { - $uri .= self::buildHysteria($item['password'], $item, $user); - } - if ($item['type'] === Server::TYPE_VLESS) { - $uri .= self::buildVless($item['password'], $item); - } - if ($item['type'] === Server::TYPE_ANYTLS) { - $uri .= self::buildAnyTLS($item['password'], $item); - } - } - return response($uri) - ->header('content-type', 'text/plain') - ->header('Subscription-Userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}"); - } - - - public static function buildShadowsocks($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $cipher = data_get($protocol_settings, 'cipher'); - - $config = [ - "{$server['name']}=Shadowsocks", - "{$server['host']}", - "{$server['port']}", - "{$cipher}", - "{$password}", - 'fast-open=false', - 'udp=true' - ]; - - if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) { - $plugin = data_get($protocol_settings, 'plugin'); - $pluginOpts = data_get($protocol_settings, 'plugin_opts', ''); - // 解析插件选项 - $parsedOpts = collect(explode(';', $pluginOpts)) - ->filter() - ->mapWithKeys(function ($pair) { - if (!str_contains($pair, '=')) { - return []; - } - [$key, $value] = explode('=', $pair, 2); - return [trim($key) => trim($value)]; - }) - ->all(); - switch ($plugin) { - case 'obfs': - $config[] = "obfs-name={$parsedOpts['obfs']}"; - if (isset($parsedOpts['obfs-host'])) { - $config[] = "obfs-host={$parsedOpts['obfs-host']}"; - } - if (isset($parsedOpts['path'])) { - $config[] = "obfs-uri={$parsedOpts['path']}"; - } - break; - } - } - - $config = array_filter($config); - $uri = implode(',', $config) . "\r\n"; - return $uri; - } - - public static function buildVmess($uuid, $server) - { - $protocol_settings = $server['protocol_settings']; - $config = [ - "{$server['name']}=vmess", - "{$server['host']}", - "{$server['port']}", - 'auto', - "{$uuid}", - 'fast-open=false', - 'udp=true', - "alterId=0" - ]; - - if (data_get($protocol_settings, 'tls')) { - $config[] = 'over-tls=true'; - if (data_get($protocol_settings, 'tls_settings')) { - $tls_settings = data_get($protocol_settings, 'tls_settings'); - $config[] = 'skip-cert-verify=' . (data_get($tls_settings, 'allow_insecure') ? 'true' : 'false'); - if (data_get($tls_settings, 'server_name')) - $config[] = "tls-name={$tls_settings['server_name']}"; - } - } - - switch (data_get($server['protocol_settings'], 'network')) { - case 'tcp': - $config[] = 'transport=tcp'; - $tcpSettings = data_get($protocol_settings, 'network_settings'); - if (data_get($tcpSettings, 'header.type')) - $config = str_replace('transport=tcp', "transport={$tcpSettings['header']['type']}", $config); - if (data_get($tcpSettings, key: 'header.request.path')) { - $paths = data_get($tcpSettings, key: 'header.request.path'); - $path = $paths[array_rand($paths)]; - $config[] = "path={$path}"; - } - if (data_get($tcpSettings, key: 'header.request.headers.Host')) { - $hosts = data_get($tcpSettings, key: 'header.request.headers.Host'); - $host = $hosts[array_rand($hosts)]; - $config[] = "host={$host}"; - } - break; - case 'ws': - $config[] = 'transport=ws'; - $wsSettings = data_get($protocol_settings, 'network_settings'); - if (data_get($wsSettings, key: 'path')) - $config[] = "path={$wsSettings['path']}"; - if (data_get($wsSettings, key: 'headers.Host')) - $config[] = "host={$wsSettings['headers']['Host']}"; - break; - case 'grpc': - $config[] = 'transport=grpc'; - if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName')) - $config[] = "grpc-service-name={$serviceName}"; - break; - case 'h2': - $config[] = 'transport=h2'; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $config[] = "path={$path}"; - if ($host = data_get($protocol_settings, 'network_settings.host')) - $config[] = "host=" . (is_array($host) ? $host[0] : $host); - break; - case 'httpupgrade': - $config[] = 'transport=httpupgrade'; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $config[] = "path={$path}"; - if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) - $config[] = "host={$host}"; - break; - } - - $uri = implode(',', $config); - $uri .= "\r\n"; - return $uri; - } - - public static function buildTrojan($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $config = [ - "{$server['name']}=trojan", - "{$server['host']}", - "{$server['port']}", - "{$password}", - ]; - - $tlsMode = (int) data_get($protocol_settings, 'tls', 1); - switch ($tlsMode) { - case 2: // Reality - if ($serverName = data_get($protocol_settings, 'reality_settings.server_name')) { - $config[] = "tls-name={$serverName}"; - } - if ($pubkey = data_get($protocol_settings, 'reality_settings.public_key')) { - $config[] = "public-key={$pubkey}"; - } - if ($shortid = data_get($protocol_settings, 'reality_settings.short_id')) { - $config[] = "short-id={$shortid}"; - } - $config[] = 'skip-cert-verify=' . (data_get($protocol_settings, 'reality_settings.allow_insecure', false) ? 'true' : 'false'); - break; - default: // Standard TLS - if ($serverName = data_get($protocol_settings, 'server_name')) { - $config[] = "tls-name={$serverName}"; - } - $config[] = 'skip-cert-verify=' . (data_get($protocol_settings, 'allow_insecure') ? 'true' : 'false'); - break; - } - - switch (data_get($protocol_settings, 'network', 'tcp')) { - case 'ws': - $config[] = 'transport=ws'; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $config[] = "path={$path}"; - if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) - $config[] = "host={$host}"; - break; - case 'grpc': - $config[] = 'transport=grpc'; - if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName')) - $config[] = "grpc-service-name={$serviceName}"; - break; - } - - $config = array_filter($config); - $uri = implode(',', $config); - $uri .= "\r\n"; - return $uri; - } - - public static function buildVless($password, $server) - { - $protocol_settings = data_get($server, 'protocol_settings', []); - - $config = [ - "{$server['name']}=VLESS", - "{$server['host']}", - "{$server['port']}", - "{$password}", - "alterId=0", - "udp=true" - ]; - - // flow - if ($flow = data_get($protocol_settings, 'flow')) { - $config[] = "flow={$flow}"; - } - - // TLS/Reality - switch (data_get($protocol_settings, 'tls')) { - case 1: - $config[] = "over-tls=true"; - $config[] = "skip-cert-verify=" . (data_get($protocol_settings, 'tls_settings.allow_insecure', false) ? "true" : "false"); - if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { - $config[] = "sni={$serverName}"; - } - break; - case 2: - $config[] = "over-tls=true"; - $config[] = "skip-cert-verify=" . (data_get($protocol_settings, 'reality_settings.allow_insecure', false) ? "true" : "false"); - if ($serverName = data_get($protocol_settings, 'reality_settings.server_name')) { - $config[] = "sni={$serverName}"; - } - if ($pubkey = data_get($protocol_settings, 'reality_settings.public_key')) { - $config[] = "public-key={$pubkey}"; - } - if ($shortid = data_get($protocol_settings, 'reality_settings.short_id')) { - $config[] = "short-id={$shortid}"; - } - break; - default: - $config[] = "over-tls=false"; - break; - } - - // network - switch (data_get($protocol_settings, 'network')) { - case 'ws': - $config[] = "transport=ws"; - if ($path = data_get($protocol_settings, 'network_settings.path')) { - $config[] = "path={$path}"; - } - if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) { - $config[] = "host={$host}"; - } - break; - case 'grpc': - $config[] = "transport=grpc"; - if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName')) { - $config[] = "grpc-service-name={$serviceName}"; - } - break; - default: - $config[] = "transport=tcp"; - break; - } - - $config = array_filter($config); - $uri = implode(',', $config) . "\r\n"; - return $uri; - } - - public static function buildHysteria($password, $server, $user) - { - $protocol_settings = $server['protocol_settings']; - if ($protocol_settings['version'] != 2) { - return; - } - $config = [ - "{$server['name']}=Hysteria2", - $server['host'], - $server['port'], - $password, - $protocol_settings['tls']['server_name'] ? "sni={$protocol_settings['tls']['server_name']}" : "(null)" - ]; - if (data_get($protocol_settings, 'tls.allow_insecure')) - $config[] = "skip-cert-verify=true"; - if ($down = data_get($protocol_settings, 'bandwidth.down')) { - $config[] = "download-bandwidth={$down}"; - } - $config[] = "udp=true"; - $config = array_filter($config); - $uri = implode(',', $config); - $uri .= "\r\n"; - return $uri; - } - - public static function buildAnyTLS($password, $server) - { - $protocol_settings = data_get($server, 'protocol_settings', []); - - $config = [ - "{$server['name']}=anytls", - "{$server['host']}", - "{$server['port']}", - "{$password}", - "udp=true" - ]; - - if ($serverName = data_get($protocol_settings, 'tls.server_name')) { - $config[] = "sni={$serverName}"; - } - // ✅ 跳过证书校验 - if (data_get($protocol_settings, 'tls.allow_insecure')) { - $config[] = 'skip-cert-verify=true'; - } - - $config = array_filter($config); - - return implode(',', $config) . "\r\n"; - } -} diff --git a/Xboard/app/Protocols/QuantumultX.php b/Xboard/app/Protocols/QuantumultX.php deleted file mode 100644 index 8488e0a..0000000 --- a/Xboard/app/Protocols/QuantumultX.php +++ /dev/null @@ -1,232 +0,0 @@ -servers; - $user = $this->user; - $uri = ''; - foreach ($servers as $item) { - $uri .= match ($item['type']) { - Server::TYPE_SHADOWSOCKS => self::buildShadowsocks($item['password'], $item), - Server::TYPE_VMESS => self::buildVmess($item['password'], $item), - Server::TYPE_VLESS => self::buildVless($item['password'], $item), - Server::TYPE_TROJAN => self::buildTrojan($item['password'], $item), - Server::TYPE_SOCKS => self::buildSocks5($item['password'], $item), - Server::TYPE_HTTP => self::buildHttp($item['password'], $item), - default => '' - }; - } - return response(base64_encode($uri)) - ->header('content-type', 'text/plain') - ->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}"); - } - - public static function buildShadowsocks($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $password = data_get($server, 'password', $password); - $addr = Helper::wrapIPv6($server['host']); - $config = [ - "shadowsocks={$addr}:{$server['port']}", - "method=" . data_get($protocol_settings, 'cipher'), - "password={$password}", - ]; - - if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) { - $plugin = data_get($protocol_settings, 'plugin'); - $pluginOpts = data_get($protocol_settings, 'plugin_opts', ''); - $parsedOpts = collect(explode(';', $pluginOpts)) - ->filter() - ->mapWithKeys(function ($pair) { - if (!str_contains($pair, '=')) { - return []; - } - [$key, $value] = explode('=', $pair, 2); - return [trim($key) => trim($value)]; - }) - ->all(); - if ($plugin === 'obfs') { - if (isset($parsedOpts['obfs'])) { - $config[] = "obfs={$parsedOpts['obfs']}"; - } - if (isset($parsedOpts['obfs-host'])) { - $config[] = "obfs-host={$parsedOpts['obfs-host']}"; - } - if (isset($parsedOpts['path'])) { - $config[] = "obfs-uri={$parsedOpts['path']}"; - } - } - } - - self::applyCommonSettings($config, $server); - - return implode(',', array_filter($config)) . "\r\n"; - } - - public static function buildVmess($uuid, $server) - { - $protocol_settings = $server['protocol_settings']; - $addr = Helper::wrapIPv6($server['host']); - $config = [ - "vmess={$addr}:{$server['port']}", - "method=" . data_get($protocol_settings, 'cipher', 'auto'), - "password={$uuid}", - ]; - - self::applyTransportSettings($config, $protocol_settings); - self::applyCommonSettings($config, $server); - - return implode(',', array_filter($config)) . "\r\n"; - } - - public static function buildVless($uuid, $server) - { - $protocol_settings = $server['protocol_settings']; - $addr = Helper::wrapIPv6($server['host']); - $config = [ - "vless={$addr}:{$server['port']}", - 'method=none', - "password={$uuid}", - ]; - - self::applyTransportSettings($config, $protocol_settings); - - if ($flow = data_get($protocol_settings, 'flow')) { - $config[] = "vless-flow={$flow}"; - } - - self::applyCommonSettings($config, $server); - - return implode(',', array_filter($config)) . "\r\n"; - } - - private static function applyTransportSettings(&$config, $settings, bool $nativeTls = false, ?array $tlsData = null) - { - $tlsMode = (int) data_get($settings, 'tls', 0); - $network = data_get($settings, 'network', 'tcp'); - $host = null; - $isWs = $network === 'ws'; - - switch ($network) { - case 'ws': - $config[] = $tlsMode ? 'obfs=wss' : 'obfs=ws'; - if ($path = data_get($settings, 'network_settings.path')) { - $config[] = "obfs-uri={$path}"; - } - $host = data_get($settings, 'network_settings.headers.Host'); - break; - case 'tcp': - $headerType = data_get($settings, 'network_settings.header.type', 'tcp'); - if ($headerType === 'http') { - $config[] = 'obfs=http'; - $paths = data_get($settings, 'network_settings.header.request.path', ['/']); - $config[] = 'obfs-uri=' . (is_array($paths) ? ($paths[0] ?? '/') : $paths); - $hostVal = data_get($settings, 'network_settings.header.request.headers.Host'); - $host = is_array($hostVal) ? ($hostVal[0] ?? null) : $hostVal; - } elseif ($tlsMode) { - $config[] = $nativeTls ? 'over-tls=true' : 'obfs=over-tls'; - } - break; - } - - switch ($tlsMode) { - case 2: // Reality - $host = $host ?? data_get($settings, 'reality_settings.server_name'); - if ($pubKey = data_get($settings, 'reality_settings.public_key')) { - $config[] = "reality-base64-pubkey={$pubKey}"; - } - if ($shortId = data_get($settings, 'reality_settings.short_id')) { - $config[] = "reality-hex-shortid={$shortId}"; - } - break; - case 1: // TLS - $resolved = $tlsData ?? (array) data_get($settings, 'tls_settings', []); - $allowInsecure = (bool) ($resolved['allow_insecure'] ?? false); - $config[] = 'tls-verification=' . ($allowInsecure ? 'false' : 'true'); - $host = $host ?? ($resolved['server_name'] ?? null); - break; - } - - if ($host) { - $config[] = ($nativeTls && !$isWs) ? "tls-host={$host}" : "obfs-host={$host}"; - } - } - - private static function applyCommonSettings(&$config, $server) - { - $config[] = 'fast-open=true'; - if ($server['type'] !== Server::TYPE_HTTP) { - $config[] = 'udp-relay=true'; - } - $config[] = "tag={$server['name']}"; - } - - public static function buildTrojan($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $addr = Helper::wrapIPv6($server['host']); - $config = [ - "trojan={$addr}:{$server['port']}", - "password={$password}", - ]; - - $tlsData = [ - 'allow_insecure' => data_get($protocol_settings, 'allow_insecure', false), - 'server_name' => data_get($protocol_settings, 'server_name'), - ]; - self::applyTransportSettings($config, $protocol_settings, true, $tlsData); - self::applyCommonSettings($config, $server); - - return implode(',', array_filter($config)) . "\r\n"; - } - - public static function buildSocks5($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $addr = Helper::wrapIPv6($server['host']); - $config = [ - "socks5={$addr}:{$server['port']}", - "username={$password}", - "password={$password}", - ]; - - self::applyTransportSettings($config, $protocol_settings, true); - self::applyCommonSettings($config, $server); - - return implode(',', array_filter($config)) . "\r\n"; - } - - public static function buildHttp($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $addr = Helper::wrapIPv6($server['host']); - $config = [ - "http={$addr}:{$server['port']}", - "username={$password}", - "password={$password}", - ]; - - self::applyTransportSettings($config, $protocol_settings, true); - self::applyCommonSettings($config, $server); - - return implode(',', array_filter($config)) . "\r\n"; - } -} diff --git a/Xboard/app/Protocols/Shadowrocket.php b/Xboard/app/Protocols/Shadowrocket.php deleted file mode 100644 index 37028e3..0000000 --- a/Xboard/app/Protocols/Shadowrocket.php +++ /dev/null @@ -1,415 +0,0 @@ - [2 => '1993'], - 'shadowrocket.anytls.base_version' => '2592', - ]; - - public function handle() - { - $servers = $this->servers; - $user = $this->user; - - $uri = ''; - //display remaining traffic and expire date - $upload = round($user['u'] / (1024 * 1024 * 1024), 2); - $download = round($user['d'] / (1024 * 1024 * 1024), 2); - $totalTraffic = round($user['transfer_enable'] / (1024 * 1024 * 1024), 2); - $expiredDate = $user['expired_at'] === null ? 'N/A' : date('Y-m-d', $user['expired_at']); - $uri .= "STATUS=🚀↑:{$upload}GB,↓:{$download}GB,TOT:{$totalTraffic}GB💡Expires:{$expiredDate}\r\n"; - foreach ($servers as $item) { - if ($item['type'] === Server::TYPE_SHADOWSOCKS) { - $uri .= self::buildShadowsocks($item['password'], $item); - } - if ($item['type'] === Server::TYPE_VMESS) { - $uri .= self::buildVmess($item['password'], $item); - } - if ($item['type'] === Server::TYPE_VLESS) { - $uri .= self::buildVless($item['password'], $item); - } - if ($item['type'] === Server::TYPE_TROJAN) { - $uri .= self::buildTrojan($item['password'], $item); - } - if ($item['type'] === Server::TYPE_HYSTERIA) { - $uri .= self::buildHysteria($item['password'], $item); - } - if ($item['type'] === Server::TYPE_TUIC) { - $uri .= self::buildTuic($item['password'], $item); - } - if ($item['type'] === Server::TYPE_ANYTLS) { - $uri .= self::buildAnyTLS($item['password'], $item); - } - if ($item['type'] === Server::TYPE_SOCKS) { - $uri .= self::buildSocks($item['password'], $item); - } - } - return response(base64_encode($uri)) - ->header('content-type', 'text/plain'); - } - - - public static function buildShadowsocks($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $name = rawurlencode($server['name']); - $password = data_get($server, 'password', $password); - $str = str_replace( - ['+', '/', '='], - ['-', '_', ''], - base64_encode(data_get($protocol_settings, 'cipher') . ":{$password}") - ); - $addr = Helper::wrapIPv6($server['host']); - - $uri = "ss://{$str}@{$addr}:{$server['port']}"; - $plugin = data_get($protocol_settings, 'plugin') == 'obfs' ? 'obfs-local' : data_get($protocol_settings, 'plugin'); - $plugin_opts = data_get($protocol_settings, 'plugin_opts'); - if ($plugin && $plugin_opts) { - $uri .= '/?' . 'plugin=' . $plugin . ';' . rawurlencode($plugin_opts); - } - return $uri . "#{$name}\r\n"; - } - - public static function buildVmess($uuid, $server) - { - $protocol_settings = $server['protocol_settings']; - $userinfo = base64_encode('auto:' . $uuid . '@' . Helper::wrapIPv6($server['host']) . ':' . $server['port']); - $config = [ - 'tfo' => 1, - 'remark' => $server['name'], - 'alterId' => 0 - ]; - if (data_get($protocol_settings, 'tls')) { - $config['tls'] = 1; - if (data_get($protocol_settings, 'tls_settings')) { - if (!!data_get($protocol_settings, 'tls_settings.allow_insecure')) - $config['allowInsecure'] = (int) data_get($protocol_settings, 'tls_settings.allow_insecure'); - if (!!data_get($protocol_settings, 'tls_settings.server_name')) - $config['peer'] = data_get($protocol_settings, 'tls_settings.server_name'); - } - } - - switch (data_get($protocol_settings, 'network')) { - case 'tcp': - if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') { - $config['obfs'] = data_get($protocol_settings, 'network_settings.header.type'); - $config['path'] = \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])); - $config['obfsParam'] = \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.headers.Host', ['www.example.com'])); - } - break; - case 'ws': - $config['obfs'] = "websocket"; - $config['path'] = data_get($protocol_settings, 'network_settings.path'); - if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) { - $config['obfsParam'] = $host; - } - break; - case 'grpc': - $config['obfs'] = "grpc"; - $config['path'] = data_get($protocol_settings, 'network_settings.serviceName'); - $config['host'] = data_get($protocol_settings, 'tls_settings.server_name') ?? $server['host']; - break; - case 'httpupgrade': - $config['obfs'] = "httpupgrade"; - if ($path = data_get($protocol_settings, 'network_settings.path')) { - $config['path'] = $path; - } - if ($host = data_get($protocol_settings, 'network_settings.host', $server['host'])) { - $config['obfsParam'] = $host; - } - break; - case 'h2': - $config['obfs'] = "h2"; - if ($path = data_get($protocol_settings, 'network_settings.path')) { - $config['path'] = $path; - } - if ($host = data_get($protocol_settings, 'network_settings.host')) { - $config['obfsParam'] = $host[0] ?? $server['host']; - $config['peer'] = $host [0] ?? $server['host']; - } - break; - } - $query = http_build_query($config, '', '&', PHP_QUERY_RFC3986); - $uri = "vmess://{$userinfo}?{$query}"; - $uri .= "\r\n"; - return $uri; - } - - public static function buildVless($uuid, $server) - { - $protocol_settings = $server['protocol_settings']; - $userinfo = base64_encode('auto:' . $uuid . '@' . Helper::wrapIPv6($server['host']) . ':' . $server['port']); - $config = [ - 'tfo' => 1, - 'remark' => $server['name'], - 'alterId' => 0 - ]; - - // 判断是否开启xtls - if (data_get($protocol_settings, 'flow')) { - $xtlsMap = [ - 'none' => 0, - 'xtls-rprx-direct' => 1, - 'xtls-rprx-vision' => 2 - ]; - if (array_key_exists(data_get($protocol_settings, 'flow'), $xtlsMap)) { - $config['tls'] = 1; - $config['xtls'] = $xtlsMap[data_get($protocol_settings, 'flow')]; - } - } - switch (data_get($protocol_settings, 'tls')) { - case 1: - $config['tls'] = 1; - $config['allowInsecure'] = (int) data_get($protocol_settings, 'tls_settings.allow_insecure'); - if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { - $config['peer'] = $serverName; - } - if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) { - $config['fp'] = $fp; - } - break; - case 2: - $config['tls'] = 1; - $config['sni'] = data_get($protocol_settings, 'reality_settings.server_name'); - $config['pbk'] = data_get($protocol_settings, 'reality_settings.public_key'); - $config['sid'] = data_get($protocol_settings, 'reality_settings.short_id'); - if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) { - $config['fp'] = $fp; - } - break; - default: - break; - } - switch (data_get($protocol_settings, 'network')) { - case 'tcp': - if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') { - $config['obfs'] = data_get($protocol_settings, 'network_settings.header.type'); - $config['path'] = \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])); - $config['obfsParam'] = \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.headers.Host', ['www.example.com'])); - } - break; - case 'ws': - $config['obfs'] = "websocket"; - if (data_get($protocol_settings, 'network_settings.path')) { - $config['path'] = data_get($protocol_settings, 'network_settings.path'); - } - - if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) { - $config['obfsParam'] = $host; - } - break; - case 'grpc': - $config['obfs'] = "grpc"; - $config['path'] = data_get($protocol_settings, 'network_settings.serviceName'); - $config['host'] = data_get($protocol_settings, 'tls_settings.server_name') ?? $server['host']; - break; - case 'kcp': - $config['obfs'] = "kcp"; - if ($seed = data_get($protocol_settings, 'network_settings.seed')) { - $config['path'] = $seed; - } - $config['type'] = data_get($protocol_settings, 'network_settings.header.type', 'none'); - break; - case 'h2': - $config['obfs'] = "h2"; - if ($path = data_get($protocol_settings, 'network_settings.path')) { - $config['path'] = $path; - } - if ($host = data_get($protocol_settings, 'network_settings.host', $server['host'])) { - $config['obfsParam'] = $host; - } - break; - case 'httpupgrade': - $config['obfs'] = "httpupgrade"; - if ($path = data_get($protocol_settings, 'network_settings.path')) { - $config['path'] = $path; - } - if ($host = data_get($protocol_settings, 'network_settings.host', $server['host'])) { - $config['obfsParam'] = $host; - } - break; - case 'xhttp': - $config['obfs'] = "xhttp"; - if ($path = data_get($protocol_settings, 'network_settings.path')) { - $config['path'] = $path; - } - if ($host = data_get($protocol_settings, 'network_settings.host', $server['host'])) { - $config['obfsParam'] = $host; - } - if ($mode = data_get($protocol_settings, 'network_settings.mode', 'auto')) { - $config['mode'] = $mode; - } - break; - } - - $query = http_build_query($config, '', '&', PHP_QUERY_RFC3986); - $uri = "vless" . "://{$userinfo}?{$query}"; - $uri .= "\r\n"; - return $uri; - } - - public static function buildTrojan($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $name = rawurlencode($server['name']); - $params = []; - $tlsMode = (int) data_get($protocol_settings, 'tls', 1); - - switch ($tlsMode) { - case 2: // Reality - $params['security'] = 'reality'; - $params['pbk'] = data_get($protocol_settings, 'reality_settings.public_key'); - $params['sid'] = data_get($protocol_settings, 'reality_settings.short_id'); - $params['sni'] = data_get($protocol_settings, 'reality_settings.server_name'); - break; - default: // Standard TLS - $params['allowInsecure'] = data_get($protocol_settings, 'allow_insecure'); - if ($serverName = data_get($protocol_settings, 'server_name')) { - $params['peer'] = $serverName; - } - break; - } - - switch (data_get($protocol_settings, 'network')) { - case 'grpc': - $params['obfs'] = 'grpc'; - $params['path'] = data_get($protocol_settings, 'network_settings.serviceName'); - break; - case 'ws': - $host = data_get($protocol_settings, 'network_settings.headers.Host'); - $path = data_get($protocol_settings, 'network_settings.path'); - $params['plugin'] = "obfs-local;obfs=websocket;obfs-host={$host};obfs-uri={$path}"; - break; - } - $query = http_build_query($params); - $addr = Helper::wrapIPv6($server['host']); - - $uri = "trojan://{$password}@{$addr}:{$server['port']}?{$query}&tfo=1#{$name}"; - $uri .= "\r\n"; - return $uri; - } - - public static function buildHysteria($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $uri = ''; // 初始化变量 - - switch (data_get($protocol_settings, 'version')) { - case 1: - $params = [ - "auth" => $password, - "upmbps" => data_get($protocol_settings, 'bandwidth.up'), - "downmbps" => data_get($protocol_settings, 'bandwidth.down'), - "protocol" => 'udp', - "fastopen" => 1, - ]; - if ($serverName = data_get($protocol_settings, 'tls.server_name')) { - $params['peer'] = $serverName; - } - if (data_get($protocol_settings, 'obfs.open')) { - $params["obfs"] = "xplus"; - $params["obfsParam"] = data_get($protocol_settings, 'obfs.password'); - } - $params['insecure'] = data_get($protocol_settings, 'tls.allow_insecure'); - if (isset($server['ports'])) - $params['mport'] = $server['ports']; - $query = http_build_query($params); - $addr = Helper::wrapIPv6($server['host']); - - $uri = "hysteria://{$addr}:{$server['port']}?{$query}#{$server['name']}"; - $uri .= "\r\n"; - break; - case 2: - $params = [ - "obfs" => 'none', - "fastopen" => 1 - ]; - if ($serverName = data_get($protocol_settings, 'tls.server_name')) { - $params['peer'] = $serverName; - } - if (data_get($protocol_settings, 'obfs.open')) { - $params['obfs'] = data_get($protocol_settings, 'obfs.type'); - $params['obfs-password'] = data_get($protocol_settings, 'obfs.password'); - } - $params['insecure'] = data_get($protocol_settings, 'tls.allow_insecure'); - if (isset($protocol_settings['hop_interval'])) { - $params['keepalive'] = data_get($protocol_settings, 'hop_interval'); - } - if (isset($server['ports'])) { - $params['mport'] = $server['ports']; - } - $query = http_build_query($params); - $addr = Helper::wrapIPv6($server['host']); - - $uri = "hysteria2://{$password}@{$addr}:{$server['port']}?{$query}#{$server['name']}"; - $uri .= "\r\n"; - break; - } - return $uri; - } - public static function buildTuic($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $name = rawurlencode($server['name']); - $params = [ - 'alpn' => data_get($protocol_settings, 'alpn'), - 'sni' => data_get($protocol_settings, 'tls.server_name'), - 'insecure' => data_get($protocol_settings, 'tls.allow_insecure') - ]; - if (data_get($protocol_settings, 'version') === 4) { - $params['token'] = $password; - } else { - $params['uuid'] = $password; - $params['password'] = $password; - } - $query = http_build_query($params); - $addr = Helper::wrapIPv6($server['host']); - $uri = "tuic://{$addr}:{$server['port']}?{$query}#{$name}"; - $uri .= "\r\n"; - return $uri; - } - - public static function buildAnyTLS($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $name = rawurlencode($server['name']); - $params = [ - 'sni' => data_get($protocol_settings, 'tls.server_name'), - 'insecure' => data_get($protocol_settings, 'tls.allow_insecure') - ]; - $query = http_build_query($params); - $addr = Helper::wrapIPv6($server['host']); - $uri = "anytls://{$password}@{$addr}:{$server['port']}?{$query}#{$name}"; - $uri .= "\r\n"; - return $uri; - } - - public static function buildSocks($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $name = rawurlencode($server['name']); - $addr = Helper::wrapIPv6($server['host']); - $uri = 'socks://' . base64_encode("{$password}:{$password}@{$addr}:{$server['port']}") . "?method=auto#{$name}"; - $uri .= "\r\n"; - return $uri; - } -} diff --git a/Xboard/app/Protocols/Shadowsocks.php b/Xboard/app/Protocols/Shadowsocks.php deleted file mode 100644 index 36f2d2a..0000000 --- a/Xboard/app/Protocols/Shadowsocks.php +++ /dev/null @@ -1,60 +0,0 @@ -servers; - $user = $this->user; - - $configs = []; - $subs = []; - $subs['servers'] = []; - $subs['bytes_used'] = ''; - $subs['bytes_remaining'] = ''; - - $bytesUsed = $user['u'] + $user['d']; - $bytesRemaining = $user['transfer_enable'] - $bytesUsed; - - foreach ($servers as $item) { - if ( - $item['type'] === 'shadowsocks' - && in_array(data_get($item, 'protocol_settings.cipher'), ['aes-128-gcm', 'aes-256-gcm', 'aes-192-gcm', 'chacha20-ietf-poly1305']) - ) { - array_push($configs, self::SIP008($item, $user)); - } - } - - $subs['version'] = 1; - $subs['bytes_used'] = $bytesUsed; - $subs['bytes_remaining'] = $bytesRemaining; - $subs['servers'] = array_merge($subs['servers'], $configs); - - return response()->json($subs) - ->header('content-type', 'application/json'); - } - - public static function SIP008($server, $user) - { - $config = [ - "id" => $server['id'], - "remarks" => $server['name'], - "server" => $server['host'], - "server_port" => $server['port'], - "password" => $server['password'], - "method" => data_get($server, 'protocol_settings.cipher') - ]; - return $config; - } -} diff --git a/Xboard/app/Protocols/SingBox.php b/Xboard/app/Protocols/SingBox.php deleted file mode 100644 index b412787..0000000 --- a/Xboard/app/Protocols/SingBox.php +++ /dev/null @@ -1,757 +0,0 @@ - [ - 'vless' => [ - 'base_version' => '1.5.0', - 'protocol_settings.flow' => [ - 'xtls-rprx-vision' => '1.5.0' - ], - 'protocol_settings.tls' => [ - '2' => '1.6.0' // Reality - ] - ], - 'hysteria' => [ - 'base_version' => '1.5.0', - 'protocol_settings.version' => [ - '2' => '1.5.0' // Hysteria 2 - ] - ], - 'tuic' => [ - 'base_version' => '1.5.0' - ], - 'ssh' => [ - 'base_version' => '1.8.0' - ], - 'juicity' => [ - 'base_version' => '1.7.0' - ], - 'wireguard' => [ - 'base_version' => '1.5.0' - ], - 'anytls' => [ - 'base_version' => '1.12.0' - ], - ] - ]; - - public function handle() - { - $appName = admin_setting('app_name', 'XBoard'); - $this->config = $this->loadConfig(); - $this->buildOutbounds(); - $this->buildRule(); - $this->adaptConfigForVersion(); - $user = $this->user; - - return response() - ->json($this->config) - ->header('profile-title', 'base64:' . base64_encode($appName)) - ->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}") - ->header('profile-update-interval', '24'); - } - - protected function loadConfig() - { - $jsonData = subscribe_template('singbox'); - - return is_array($jsonData) ? $jsonData : json_decode($jsonData, true); - } - - protected function buildOutbounds() - { - $outbounds = $this->config['outbounds']; - $proxies = []; - foreach ($this->servers as $item) { - $protocol_settings = $item['protocol_settings']; - if ($item['type'] === Server::TYPE_SHADOWSOCKS) { - $ssConfig = $this->buildShadowsocks($item['password'], $item); - $proxies[] = $ssConfig; - } - if ($item['type'] === Server::TYPE_TROJAN) { - $trojanConfig = $this->buildTrojan($this->user['uuid'], $item); - $proxies[] = $trojanConfig; - } - if ($item['type'] === Server::TYPE_VMESS) { - $vmessConfig = $this->buildVmess($this->user['uuid'], $item); - $proxies[] = $vmessConfig; - } - if ( - $item['type'] === Server::TYPE_VLESS - && in_array(data_get($protocol_settings, 'network'), ['tcp', 'ws', 'grpc', 'http', 'quic', 'httpupgrade']) - ) { - $vlessConfig = $this->buildVless($this->user['uuid'], $item); - $proxies[] = $vlessConfig; - } - if ($item['type'] === Server::TYPE_HYSTERIA) { - $hysteriaConfig = $this->buildHysteria($this->user['uuid'], $item); - $proxies[] = $hysteriaConfig; - } - if ($item['type'] === Server::TYPE_TUIC) { - $tuicConfig = $this->buildTuic($this->user['uuid'], $item); - $proxies[] = $tuicConfig; - } - if ($item['type'] === Server::TYPE_ANYTLS) { - $anytlsConfig = $this->buildAnyTLS($this->user['uuid'], $item); - $proxies[] = $anytlsConfig; - } - if ($item['type'] === Server::TYPE_SOCKS) { - $socksConfig = $this->buildSocks($this->user['uuid'], $item); - $proxies[] = $socksConfig; - } - if ($item['type'] === Server::TYPE_HTTP) { - $httpConfig = $this->buildHttp($this->user['uuid'], $item); - $proxies[] = $httpConfig; - } - } - foreach ($outbounds as &$outbound) { - if (in_array($outbound['type'], ['urltest', 'selector'])) { - array_push($outbound['outbounds'], ...array_column($proxies, 'tag')); - } - } - - $outbounds = array_merge($outbounds, $proxies); - $this->config['outbounds'] = $outbounds; - return $outbounds; - } - - /** - * Build rule - */ - protected function buildRule() - { - $rules = $this->config['route']['rules']; - $this->config['route']['rules'] = $rules; - } - - /** - * 根据客户端版本自适应配置格式 - * 模板基准格式: 1.13.0+ (最新) - */ - protected function adaptConfigForVersion(): void - { - $coreVersion = $this->getSingBoxCoreVersion(); - if (empty($coreVersion)) { - return; - } - - // >= 1.13.0: 移除已删除的 block/dns 出站 - if (version_compare($coreVersion, '1.13.0', '>=')) { - $this->upgradeSpecialOutboundsToActions(); - } - - // < 1.11.0: rule action 降级为旧出站; 恢复废弃字段 - if (version_compare($coreVersion, '1.11.0', '<')) { - $this->downgradeActionsToSpecialOutbounds(); - $this->restoreDeprecatedInboundFields(); - } - - // < 1.12.0: DNS type+server → 旧 address 格式 - if (version_compare($coreVersion, '1.12.0', '<')) { - $this->convertDnsServersToLegacy(); - } - - // < 1.10.0: tun address 数组 → inet4_address/inet6_address - if (version_compare($coreVersion, '1.10.0', '<')) { - $this->convertTunAddressToLegacy(); - } - } - - /** - * 获取核心版本 (Hiddify/SFM 等映射到内核版本) - */ - private function getSingBoxCoreVersion(): ?string - { - // 优先从 UA 提取核心版本 - if (!empty($this->userAgent)) { - if (preg_match('/sing-box\s+v?(\d+(?:\.\d+){0,2})/i', $this->userAgent, $matches)) { - return $matches[1]; - } - } - - if (empty($this->clientVersion)) { - return null; - } - - if ($this->clientName === 'sing-box') { - return $this->clientVersion; - } - - return '1.13.0'; - } - - /** - * sing-box >= 1.13.0: block/dns 出站升级为 action - */ - private function upgradeSpecialOutboundsToActions(): void - { - $removedTags = []; - $this->config['outbounds'] = array_values(array_filter( - $this->config['outbounds'] ?? [], - function ($outbound) use (&$removedTags) { - if (in_array($outbound['type'] ?? '', ['block', 'dns'])) { - $removedTags[$outbound['tag']] = $outbound['type']; - return false; - } - return true; - } - )); - - if (empty($removedTags)) { - return; - } - - if (isset($this->config['route']['rules'])) { - foreach ($this->config['route']['rules'] as &$rule) { - if (!isset($rule['outbound']) || !isset($removedTags[$rule['outbound']])) { - continue; - } - $type = $removedTags[$rule['outbound']]; - unset($rule['outbound']); - $rule['action'] = $type === 'dns' ? 'hijack-dns' : 'reject'; - } - unset($rule); - } - } - - /** - * sing-box < 1.11.0: rule action 降级为旧 block/dns 出站 - */ - private function downgradeActionsToSpecialOutbounds(): void - { - $needsDnsOutbound = false; - $needsBlockOutbound = false; - - if (isset($this->config['route']['rules'])) { - foreach ($this->config['route']['rules'] as &$rule) { - if (!isset($rule['action'])) { - continue; - } - switch ($rule['action']) { - case 'hijack-dns': - unset($rule['action']); - $rule['outbound'] = 'dns-out'; - $needsDnsOutbound = true; - break; - case 'reject': - unset($rule['action']); - $rule['outbound'] = 'block'; - $needsBlockOutbound = true; - break; - } - } - unset($rule); - } - - if ($needsBlockOutbound) { - $this->config['outbounds'][] = ['type' => 'block', 'tag' => 'block']; - } - if ($needsDnsOutbound) { - $this->config['outbounds'][] = ['type' => 'dns', 'tag' => 'dns-out']; - } - } - - /** - * sing-box < 1.11.0: 恢复废弃的入站字段 - */ - private function restoreDeprecatedInboundFields(): void - { - if (!isset($this->config['inbounds'])) { - return; - } - foreach ($this->config['inbounds'] as &$inbound) { - if ($inbound['type'] === 'tun') { - $inbound['endpoint_independent_nat'] = true; - } - if (!empty($inbound['sniff'])) { - $inbound['sniff_override_destination'] = true; - } - } - } - - /** - * sing-box < 1.12.0: 将新 DNS server type+server 格式转换为旧 address 格式 - */ - private function convertDnsServersToLegacy(): void - { - if (!isset($this->config['dns']['servers'])) { - return; - } - foreach ($this->config['dns']['servers'] as &$server) { - if (!isset($server['type'])) { - continue; - } - $type = $server['type']; - $host = $server['server'] ?? null; - switch ($type) { - case 'https': - $server['address'] = "https://{$host}/dns-query"; - break; - case 'tls': - $server['address'] = "tls://{$host}"; - break; - case 'tcp': - $server['address'] = "tcp://{$host}"; - break; - case 'quic': - $server['address'] = "quic://{$host}"; - break; - case 'udp': - $server['address'] = $host; - break; - case 'block': - $server['address'] = 'rcode://refused'; - break; - case 'rcode': - $server['address'] = 'rcode://' . ($server['rcode'] ?? 'success'); - unset($server['rcode']); - break; - default: - $server['address'] = $host; - break; - } - unset($server['type'], $server['server']); - } - unset($server); - } - - /** - * sing-box < 1.10.0: 将 tun address 数组转换为 inet4_address/inet6_address - */ - private function convertTunAddressToLegacy(): void - { - if (!isset($this->config['inbounds'])) { - return; - } - foreach ($this->config['inbounds'] as &$inbound) { - if ($inbound['type'] !== 'tun' || !isset($inbound['address'])) { - continue; - } - foreach ($inbound['address'] as $addr) { - if (str_contains($addr, ':')) { - $inbound['inet6_address'] = $addr; - } else { - $inbound['inet4_address'] = $addr; - } - } - unset($inbound['address']); - } - } - - protected function buildShadowsocks($password, $server) - { - $protocol_settings = data_get($server, 'protocol_settings'); - $array = []; - $array['tag'] = $server['name']; - $array['type'] = 'shadowsocks'; - $array['server'] = $server['host']; - $array['server_port'] = $server['port']; - $array['method'] = data_get($protocol_settings, 'cipher'); - $array['password'] = data_get($server, 'password', $password); - if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) { - $array['plugin'] = data_get($protocol_settings, 'plugin'); - $array['plugin_opts'] = data_get($protocol_settings, 'plugin_opts', ''); - } - - return $array; - } - - - protected function buildVmess($uuid, $server) - { - $protocol_settings = $server['protocol_settings']; - $array = [ - 'tag' => $server['name'], - 'type' => 'vmess', - 'server' => $server['host'], - 'server_port' => $server['port'], - 'uuid' => $uuid, - 'security' => 'auto', - 'alter_id' => 0, - ]; - - if ($protocol_settings['tls']) { - $array['tls'] = [ - 'enabled' => true, - 'insecure' => (bool) data_get($protocol_settings, 'tls_settings.allow_insecure'), - ]; - - $this->appendUtls($array['tls'], $protocol_settings); - - if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { - $array['tls']['server_name'] = $serverName; - } - } - - $this->appendMultiplex($array, $protocol_settings); - - if ($transport = $this->buildTransport($protocol_settings, $server)) { - $array['transport'] = $transport; - } - return $array; - } - - protected function buildVless($password, $server) - { - $protocol_settings = data_get($server, 'protocol_settings', []); - $array = [ - "type" => "vless", - "tag" => $server['name'], - "server" => $server['host'], - "server_port" => $server['port'], - "uuid" => $password, - "packet_encoding" => "xudp", - ]; - if ($flow = data_get($protocol_settings, 'flow')) { - $array['flow'] = $flow; - } - - if (data_get($protocol_settings, 'tls')) { - $tlsMode = (int) data_get($protocol_settings, 'tls', 0); - $tlsConfig = [ - 'enabled' => true, - 'insecure' => $tlsMode === 2 - ? (bool) data_get($protocol_settings, 'reality_settings.allow_insecure', false) - : (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false), - ]; - - $this->appendUtls($tlsConfig, $protocol_settings); - - switch ($tlsMode) { - case 1: - if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { - $tlsConfig['server_name'] = $serverName; - } - break; - case 2: - $tlsConfig['server_name'] = data_get($protocol_settings, 'reality_settings.server_name'); - $tlsConfig['reality'] = [ - 'enabled' => true, - 'public_key' => data_get($protocol_settings, 'reality_settings.public_key'), - 'short_id' => data_get($protocol_settings, 'reality_settings.short_id') - ]; - break; - } - - $array['tls'] = $tlsConfig; - } - - $this->appendMultiplex($array, $protocol_settings); - - if ($transport = $this->buildTransport($protocol_settings, $server)) { - $array['transport'] = $transport; - } - - return $array; - } - - protected function buildTrojan($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $array = [ - 'tag' => $server['name'], - 'type' => 'trojan', - 'server' => $server['host'], - 'server_port' => $server['port'], - 'password' => $password, - ]; - - $tlsMode = (int) data_get($protocol_settings, 'tls', 1); - $tlsConfig = ['enabled' => true]; - - switch ($tlsMode) { - case 2: // Reality - $tlsConfig['insecure'] = (bool) data_get($protocol_settings, 'reality_settings.allow_insecure', false); - $tlsConfig['server_name'] = data_get($protocol_settings, 'reality_settings.server_name'); - $tlsConfig['reality'] = [ - 'enabled' => true, - 'public_key' => data_get($protocol_settings, 'reality_settings.public_key'), - 'short_id' => data_get($protocol_settings, 'reality_settings.short_id'), - ]; - break; - default: // Standard TLS - $tlsConfig['insecure'] = (bool) data_get($protocol_settings, 'allow_insecure', false); - if ($serverName = data_get($protocol_settings, 'server_name')) { - $tlsConfig['server_name'] = $serverName; - } - break; - } - - $this->appendUtls($tlsConfig, $protocol_settings); - $array['tls'] = $tlsConfig; - - $this->appendMultiplex($array, $protocol_settings); - - if ($transport = $this->buildTransport($protocol_settings, $server)) { - $array['transport'] = $transport; - } - return $array; - } - - protected function buildHysteria($password, $server): array - { - $protocol_settings = $server['protocol_settings']; - $baseConfig = [ - 'server' => $server['host'], - 'server_port' => $server['port'], - 'tag' => $server['name'], - 'tls' => [ - 'enabled' => true, - 'insecure' => (bool) data_get($protocol_settings, 'tls.allow_insecure', false), - ] - ]; - // 支持 1.11.0 版本及以上 `server_ports` 和 `hop_interval` 配置 - if ($this->supportsFeature('sing-box', '1.11.0')) { - if (isset($server['ports'])) { - $baseConfig['server_ports'] = [str_replace('-', ':', $server['ports'])]; - } - if (isset($protocol_settings['hop_interval'])) { - $baseConfig['hop_interval'] = "{$protocol_settings['hop_interval']}s"; - } - } - - if ($serverName = data_get($protocol_settings, 'tls.server_name')) { - $baseConfig['tls']['server_name'] = $serverName; - } - $speedConfig = [ - 'up_mbps' => data_get($protocol_settings, 'bandwidth.up'), - 'down_mbps' => data_get($protocol_settings, 'bandwidth.down'), - ]; - $versionConfig = match (data_get($protocol_settings, 'version', 1)) { - 2 => [ - 'type' => 'hysteria2', - 'password' => $password, - 'obfs' => data_get($protocol_settings, 'obfs.open') ? [ - 'type' => data_get($protocol_settings, 'obfs.type'), - 'password' => data_get($protocol_settings, 'obfs.password') - ] : null, - ], - default => [ - 'type' => 'hysteria', - 'auth_str' => $password, - 'obfs' => data_get($protocol_settings, 'obfs.password'), - 'disable_mtu_discovery' => true, - ] - }; - - return array_filter( - array_merge($baseConfig, $speedConfig, $versionConfig), - fn($v) => !is_null($v) - ); - } - - protected function buildTuic($password, $server): array - { - $protocol_settings = data_get($server, 'protocol_settings', []); - $array = [ - 'type' => 'tuic', - 'tag' => $server['name'], - 'server' => $server['host'], - 'server_port' => $server['port'], - 'congestion_control' => data_get($protocol_settings, 'congestion_control', 'cubic'), - 'udp_relay_mode' => data_get($protocol_settings, 'udp_relay_mode', 'native'), - 'zero_rtt_handshake' => true, - 'heartbeat' => '10s', - 'tls' => [ - 'enabled' => true, - 'insecure' => (bool) data_get($protocol_settings, 'tls.allow_insecure', false), - 'alpn' => data_get($protocol_settings, 'alpn', ['h3']), - ] - ]; - - if ($serverName = data_get($protocol_settings, 'tls.server_name')) { - $array['tls']['server_name'] = $serverName; - } - - if (data_get($protocol_settings, 'version') === 4) { - $array['token'] = $password; - } else { - $array['uuid'] = $password; - $array['password'] = $password; - } - - return $array; - } - - protected function buildAnyTLS($password, $server): array - { - $protocol_settings = data_get($server, 'protocol_settings', []); - $array = [ - 'type' => 'anytls', - 'tag' => $server['name'], - 'server' => $server['host'], - 'password' => $password, - 'server_port' => $server['port'], - 'tls' => [ - 'enabled' => true, - 'insecure' => (bool) data_get($protocol_settings, 'tls.allow_insecure', false), - 'alpn' => data_get($protocol_settings, 'alpn', ['h3']), - ] - ]; - - if ($serverName = data_get($protocol_settings, 'tls.server_name')) { - $array['tls']['server_name'] = $serverName; - } - - return $array; - } - - protected function buildSocks($password, $server): array - { - $protocol_settings = data_get($server, 'protocol_settings', []); - $array = [ - 'type' => 'socks', - 'tag' => $server['name'], - 'server' => $server['host'], - 'server_port' => $server['port'], - 'version' => '5', // 默认使用 socks5 - 'username' => $password, - 'password' => $password, - ]; - - if (data_get($protocol_settings, 'udp_over_tcp')) { - $array['udp_over_tcp'] = true; - } - - return $array; - } - - protected function buildHttp($password, $server): array - { - $protocol_settings = data_get($server, 'protocol_settings', []); - $array = [ - 'type' => 'http', - 'tag' => $server['name'], - 'server' => $server['host'], - 'server_port' => $server['port'], - 'username' => $password, - 'password' => $password, - ]; - - if ($path = data_get($protocol_settings, 'path')) { - $array['path'] = $path; - } - - if ($headers = data_get($protocol_settings, 'headers')) { - $array['headers'] = $headers; - } - - if (data_get($protocol_settings, 'tls')) { - $array['tls'] = [ - 'enabled' => true, - 'insecure' => (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false), - ]; - - if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { - $array['tls']['server_name'] = $serverName; - } - } - - return $array; - } - - protected function buildTransport(array $protocol_settings, array $server): ?array - { - $transport = match (data_get($protocol_settings, 'network')) { - 'tcp' => data_get($protocol_settings, 'network_settings.header.type') === 'http' ? [ - 'type' => 'http', - 'path' => Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])), - 'host' => data_get($protocol_settings, 'network_settings.header.request.headers.Host', []) - ] : null, - 'ws' => [ - 'type' => 'ws', - 'path' => data_get($protocol_settings, 'network_settings.path'), - 'headers' => ($host = data_get($protocol_settings, 'network_settings.headers.Host')) ? ['Host' => $host] : null, - 'max_early_data' => 0, - // 'early_data_header_name' => 'Sec-WebSocket-Protocol' - ], - 'grpc' => [ - 'type' => 'grpc', - 'service_name' => data_get($protocol_settings, 'network_settings.serviceName') - ], - 'h2' => [ - 'type' => 'http', - 'host' => data_get($protocol_settings, 'network_settings.host'), - 'path' => data_get($protocol_settings, 'network_settings.path') - ], - 'httpupgrade' => [ - 'type' => 'httpupgrade', - 'path' => data_get($protocol_settings, 'network_settings.path'), - 'host' => data_get($protocol_settings, 'network_settings.host', $server['host']), - 'headers' => data_get($protocol_settings, 'network_settings.headers') - ], - 'quic' => ['type' => 'quic'], - default => null - }; - - if (!$transport) { - return null; - } - - return array_filter($transport, fn($v) => !is_null($v)); - } - - protected function appendMultiplex(&$array, $protocol_settings) - { - if ($multiplex = data_get($protocol_settings, 'multiplex')) { - if (data_get($multiplex, 'enabled')) { - $array['multiplex'] = [ - 'enabled' => true, - 'protocol' => data_get($multiplex, 'protocol', 'yamux'), - 'max_connections' => data_get($multiplex, 'max_connections'), - 'min_streams' => data_get($multiplex, 'min_streams'), - 'max_streams' => data_get($multiplex, 'max_streams'), - 'padding' => (bool) data_get($multiplex, 'padding', false), - ]; - if (data_get($multiplex, 'brutal.enabled')) { - $array['multiplex']['brutal'] = [ - 'enabled' => true, - 'up_mbps' => data_get($multiplex, 'brutal.up_mbps'), - 'down_mbps' => data_get($multiplex, 'brutal.down_mbps'), - ]; - } - $array['multiplex'] = array_filter($array['multiplex'], fn($v) => !is_null($v)); - } - } - } - - protected function appendUtls(&$tlsConfig, $protocol_settings) - { - if ($utls = data_get($protocol_settings, 'utls')) { - if (data_get($utls, 'enabled')) { - $tlsConfig['utls'] = [ - 'enabled' => true, - 'fingerprint' => Helper::getTlsFingerprint($utls) - ]; - } - } - } -} diff --git a/Xboard/app/Protocols/Stash.php b/Xboard/app/Protocols/Stash.php deleted file mode 100644 index 8a3eadc..0000000 --- a/Xboard/app/Protocols/Stash.php +++ /dev/null @@ -1,587 +0,0 @@ - [ - 'trojan' => [ - 'protocol_settings.tls' => [ - '2' => '9999.0.0', // Trojan Reality not supported in Stash - ], - ], - 'vmess' => [ - 'protocol_settings.network' => [ - 'httpupgrade' => '9999.0.0', // httpupgrade not supported in Stash - ], - ], - ], - 'stash' => [ - 'anytls' => [ - 'base_version' => '3.3.0' // AnyTLS 协议在3.3.0版本中添加 - ], - 'vless' => [ - 'protocol_settings.tls' => [ - '2' => '3.1.0' // Reality 在3.1.0版本中添加 - ], - 'protocol_settings.flow' => [ - 'xtls-rprx-vision' => '3.1.0', - ] - ], - 'hysteria' => [ - 'base_version' => '2.0.0', - 'protocol_settings.version' => [ - '1' => '2.0.0', // Hysteria 1 - '2' => '2.5.0' // Hysteria 2,2.5.0 版本开始支持(2023年11月8日) - ], - // 'protocol_settings.ports' => [ - // 'true' => '2.6.4' // Hysteria 2 端口跳转功能于2.6.4版本支持(2024年8月4日) - // ] - ], - 'tuic' => [ - 'base_version' => '2.3.0' // TUIC 协议自身需要 2.3.0+ - ], - 'shadowsocks' => [ - 'base_version' => '2.0.0', - // ShadowSocks2022 在3.0.0版本中添加(2025年4月2日) - 'protocol_settings.cipher' => [ - '2022-blake3-aes-128-gcm' => '3.0.0', - '2022-blake3-aes-256-gcm' => '3.0.0', - '2022-blake3-chacha20-poly1305' => '3.0.0' - ] - ], - 'shadowtls' => [ - 'base_version' => '3.0.0' // ShadowTLS 在3.0.0版本中添加(2025年4月2日) - ], - 'ssh' => [ - 'base_version' => '2.6.4' // SSH 协议在2.6.4中添加(2024年8月4日) - ], - 'juicity' => [ - 'base_version' => '2.6.4' // Juicity 协议在2.6.4中添加(2024年8月4日) - ] - ] - ]; - - const CUSTOM_TEMPLATE_FILE = 'resources/rules/custom.stash.yaml'; - const CUSTOM_CLASH_TEMPLATE_FILE = 'resources/rules/custom.clash.yaml'; - const DEFAULT_TEMPLATE_FILE = 'resources/rules/default.clash.yaml'; - - public function handle() - { - $servers = $this->servers; - $user = $this->user; - $appName = admin_setting('app_name', 'XBoard'); - - $template = subscribe_template('stash'); - - $config = Yaml::parse($template); - $proxy = []; - $proxies = []; - - foreach ($servers as $item) { - if ($item['type'] === Server::TYPE_SHADOWSOCKS) { - array_push($proxy, self::buildShadowsocks($item['password'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_VMESS) { - array_push($proxy, self::buildVmess($item['password'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_VLESS) { - array_push($proxy, $this->buildVless($item['password'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_HYSTERIA) { - array_push($proxy, self::buildHysteria($item['password'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_TROJAN) { - array_push($proxy, self::buildTrojan($item['password'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_TUIC) { - array_push($proxy, self::buildTuic($item['password'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_ANYTLS) { - array_push($proxy, self::buildAnyTLS($item['password'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_SOCKS) { - array_push($proxy, self::buildSocks5($item['password'], $item)); - array_push($proxies, $item['name']); - } - if ($item['type'] === Server::TYPE_HTTP) { - array_push($proxy, self::buildHttp($item['password'], $item)); - array_push($proxies, $item['name']); - } - } - - $config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy); - foreach ($config['proxy-groups'] as $k => $v) { - if (!is_array($config['proxy-groups'][$k]['proxies'])) - $config['proxy-groups'][$k]['proxies'] = []; - $isFilter = false; - foreach ($config['proxy-groups'][$k]['proxies'] as $src) { - foreach ($proxies as $dst) { - if (!$this->isRegex($src)) - continue; - $isFilter = true; - $config['proxy-groups'][$k]['proxies'] = array_values(array_diff($config['proxy-groups'][$k]['proxies'], [$src])); - if ($this->isMatch($src, $dst)) { - array_push($config['proxy-groups'][$k]['proxies'], $dst); - } - } - if ($isFilter) - continue; - } - if ($isFilter) - continue; - $config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies); - } - $config['proxy-groups'] = array_filter($config['proxy-groups'], function ($group) { - return $group['proxies']; - }); - $config['proxy-groups'] = array_values($config['proxy-groups']); - // Force the current subscription domain to be a direct rule - $subsDomain = request()->header('Host'); - if ($subsDomain) { - array_unshift($config['rules'], "DOMAIN,{$subsDomain},DIRECT"); - } - - $yaml = Yaml::dump($config, 2, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE); - $yaml = str_replace('$app_name', admin_setting('app_name', 'XBoard'), $yaml); - return response($yaml) - ->header('content-type', 'text/yaml') - ->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}") - ->header('profile-update-interval', '24') - ->header('content-disposition', 'attachment;filename*=UTF-8\'\'' . rawurlencode($appName)); - } - - public static function buildShadowsocks($uuid, $server) - { - $protocol_settings = $server['protocol_settings']; - $array = []; - $array['name'] = $server['name']; - $array['type'] = 'ss'; - $array['server'] = $server['host']; - $array['port'] = $server['port']; - $array['cipher'] = data_get($protocol_settings, 'cipher'); - $array['password'] = $uuid; - $array['udp'] = true; - if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) { - $plugin = data_get($protocol_settings, 'plugin'); - $pluginOpts = data_get($protocol_settings, 'plugin_opts', ''); - $array['plugin'] = $plugin; - - // 解析插件选项 - $parsedOpts = collect(explode(';', $pluginOpts)) - ->filter() - ->mapWithKeys(function ($pair) { - if (!str_contains($pair, '=')) { - return []; - } - [$key, $value] = explode('=', $pair, 2); - return [trim($key) => trim($value)]; - }) - ->all(); - - // 根据插件类型进行字段映射 - switch ($plugin) { - case 'obfs': - $array['plugin-opts'] = [ - 'mode' => $parsedOpts['obfs'], - 'host' => $parsedOpts['obfs-host'], - ]; - - // 可选path参数 - if (isset($parsedOpts['path'])) { - $array['plugin-opts']['path'] = $parsedOpts['path']; - } - break; - - case 'v2ray-plugin': - $array['plugin-opts'] = [ - 'mode' => $parsedOpts['mode'] ?? 'websocket', - 'tls' => isset($parsedOpts['tls']) && $parsedOpts['tls'] == 'true', - 'host' => $parsedOpts['host'] ?? '', - 'path' => $parsedOpts['path'] ?? '/', - ]; - break; - - default: - // 对于其他插件,直接使用解析出的键值对 - $array['plugin-opts'] = $parsedOpts; - } - } - return $array; - } - - public static function buildVmess($uuid, $server) - { - $protocol_settings = $server['protocol_settings']; - $array = []; - $array['name'] = $server['name']; - $array['type'] = 'vmess'; - $array['server'] = $server['host']; - $array['port'] = $server['port']; - $array['uuid'] = $uuid; - $array['alterId'] = 0; - $array['cipher'] = 'auto'; - $array['udp'] = true; - - $array['tls'] = (bool) data_get($protocol_settings, 'tls'); - $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false); - if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { - $array['servername'] = $serverName; - } - - switch (data_get($protocol_settings, 'network')) { - case 'tcp': - $headerType = data_get($protocol_settings, 'network_settings.header.type', 'tcp'); - $array['network'] = ($headerType === 'http') ? 'http' : 'tcp'; - if ($headerType === 'http') { - $array['http-opts']['path'] = data_get($protocol_settings, 'network_settings.header.request.path', ['/']); - if ($host = data_get($protocol_settings, 'network_settings.header.request.headers.Host')) { - $array['http-opts']['headers']['Host'] = $host; - } - } - break; - case 'ws': - $array['network'] = 'ws'; - $array['ws-opts']['path'] = data_get($protocol_settings, 'network_settings.path'); - if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) { - $array['ws-opts']['headers'] = ['Host' => $host]; - } - break; - case 'grpc': - $array['network'] = 'grpc'; - $array['grpc-opts'] = []; - $array['grpc-opts']['grpc-service-name'] = data_get($protocol_settings, 'network_settings.serviceName'); - break; - case 'h2': - $array['network'] = 'h2'; - $array['tls'] = true; - $array['h2-opts'] = []; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $array['h2-opts']['path'] = $path; - if ($host = data_get($protocol_settings, 'network_settings.host')) - $array['h2-opts']['host'] = is_array($host) ? $host : [$host]; - break; - default: - break; - } - return $array; - } - - public function buildVless($uuid, $server) - { - $protocol_settings = $server['protocol_settings']; - $array = []; - $array['name'] = $server['name']; - $array['type'] = 'vless'; - $array['server'] = $server['host']; - $array['port'] = $server['port']; - $array['uuid'] = $uuid; - $array['udp'] = true; - - if ($fingerprint = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) { - $array['client-fingerprint'] = $fingerprint; - } - - switch (data_get($protocol_settings, 'tls')) { - case 1: - $array['tls'] = true; - $array['skip-cert-verify'] = data_get($protocol_settings, 'tls_settings.allow_insecure'); - if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { - $array['servername'] = $serverName; - } - break; - case 2: - $array['tls'] = true; - $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'reality_settings.allow_insecure', false); - if ($serverName = data_get($protocol_settings, 'reality_settings.server_name')) { - $array['servername'] = $serverName; - $array['sni'] = $serverName; - } - $array['flow'] = data_get($protocol_settings, 'flow'); - $array['reality-opts'] = [ - 'public-key' => data_get($protocol_settings, 'reality_settings.public_key'), - 'short-id' => data_get($protocol_settings, 'reality_settings.short_id') - ]; - break; - } - - switch (data_get($protocol_settings, 'network')) { - case 'tcp': - $headerType = data_get($protocol_settings, 'network_settings.header.type', 'tcp'); - $array['network'] = ($headerType === 'http') ? 'http' : 'tcp'; - if ($headerType === 'http') { - if ( - $httpOpts = array_filter([ - 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'), - 'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/']) - ]) - ) { - $array['http-opts'] = $httpOpts; - } - } - break; - case 'ws': - $array['network'] = 'ws'; - $array['ws-opts']['path'] = data_get($protocol_settings, 'network_settings.path'); - if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) { - $array['ws-opts']['headers'] = ['Host' => $host]; - } - break; - case 'grpc': - $array['network'] = 'grpc'; - $array['grpc-opts']['grpc-service-name'] = data_get($protocol_settings, 'network_settings.serviceName'); - break; - case 'h2': - $array['network'] = 'h2'; - $array['h2-opts'] = []; - if ($path = data_get($protocol_settings, 'network_settings.path')) - $array['h2-opts']['path'] = $path; - if ($host = data_get($protocol_settings, 'network_settings.host')) - $array['h2-opts']['host'] = is_array($host) ? $host : [$host]; - break; - } - - return $array; - } - - public static function buildTrojan($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $array = [ - 'name' => $server['name'], - 'type' => 'trojan', - 'server' => $server['host'], - 'port' => $server['port'], - 'password' => $password, - 'udp' => true, - ]; - - $tlsMode = (int) data_get($protocol_settings, 'tls', 1); - switch ($tlsMode) { - case 2: // Reality - $array['tls'] = true; - $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'reality_settings.allow_insecure', false); - if ($serverName = data_get($protocol_settings, 'reality_settings.server_name')) { - $array['sni'] = $serverName; - } - $array['reality-opts'] = [ - 'public-key' => data_get($protocol_settings, 'reality_settings.public_key'), - 'short-id' => data_get($protocol_settings, 'reality_settings.short_id'), - ]; - break; - default: // Standard TLS - if ($serverName = data_get($protocol_settings, 'server_name')) { - $array['sni'] = $serverName; - } - $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'allow_insecure', false); - break; - } - - switch (data_get($protocol_settings, 'network')) { - case 'tcp': - $headerType = data_get($protocol_settings, 'network_settings.header.type', 'tcp'); - $array['network'] = ($headerType === 'http') ? 'http' : 'tcp'; - if ($headerType === 'http') { - $array['http-opts']['path'] = data_get($protocol_settings, 'network_settings.header.request.path', ['/']); - } - break; - case 'ws': - $array['network'] = 'ws'; - $array['ws-opts']['path'] = data_get($protocol_settings, 'network_settings.path'); - if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) { - $array['ws-opts']['headers'] = ['Host' => $host]; - } - break; - case 'grpc': - $array['network'] = 'grpc'; - if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName')) - $array['grpc-opts']['grpc-service-name'] = $serviceName; - break; - } - - return $array; - } - - public static function buildHysteria($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $array['name'] = $server['name']; - $array['server'] = $server['host']; - $array['port'] = $server['port']; - $array['up-speed'] = data_get($protocol_settings, 'bandwidth.up'); - $array['down-speed'] = data_get($protocol_settings, 'bandwidth.down'); - $array['skip-cert-verify'] = data_get($protocol_settings, 'tls.allow_insecure'); - if ($serverName = data_get($protocol_settings, 'tls.server_name')) { - $array['sni'] = $serverName; - } - if (isset($server['ports'])) { - $array['ports'] = $server['ports']; - } - switch (data_get($protocol_settings, 'version')) { - case 1: - $array['type'] = 'hysteria'; - $array['auth-str'] = $password; - $array['protocol'] = 'udp'; - if (data_get($protocol_settings, 'obfs.open')) { - $array['obfs'] = data_get($protocol_settings, 'obfs.password'); - } - break; - case 2: - $array['type'] = 'hysteria2'; - $array['auth'] = $password; - $array['fast-open'] = true; - if (data_get($protocol_settings, 'obfs.open')) { - $array['obfs'] = data_get($protocol_settings, 'obfs.type', 'salamander'); - $array['obfs-password'] = data_get($protocol_settings, 'obfs.password'); - } - break; - } - return $array; - } - - public static function buildTuic($password, $server) - { - $protocol_settings = data_get($server, 'protocol_settings', []); - $array = [ - 'name' => $server['name'], - 'type' => 'tuic', - 'server' => $server['host'], - 'port' => $server['port'], - 'congestion-controller' => data_get($protocol_settings, 'congestion_control', 'cubic'), - 'udp-relay-mode' => data_get($protocol_settings, 'udp_relay_mode', 'native'), - 'alpn' => data_get($protocol_settings, 'alpn', ['h3']), - 'reduce-rtt' => true, - 'fast-open' => true, - 'heartbeat-interval' => 10000, - 'request-timeout' => 8000, - 'max-udp-relay-packet-size' => 1500, - 'version' => data_get($protocol_settings, 'version', 5), - ]; - - if (data_get($protocol_settings, 'version') === 4) { - $array['token'] = $password; - } else { - $array['uuid'] = $password; - $array['password'] = $password; - } - - $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls.allow_insecure', false); - if ($serverName = data_get($protocol_settings, 'tls.server_name')) { - $array['sni'] = $serverName; - } - - return $array; - } - - public static function buildAnyTLS($password, $server) - { - $protocol_settings = data_get($server, 'protocol_settings', []); - $array = [ - 'name' => $server['name'], - 'type' => 'anytls', - 'server' => $server['host'], - 'port' => $server['port'], - 'password' => $password, - 'sni' => data_get($protocol_settings, 'tls.server_name'), - 'skip-cert-verify' => (bool) data_get($protocol_settings, 'tls.allow_insecure', false), - 'udp' => true, - ]; - - return $array; - } - - public static function buildSocks5($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $array = [ - 'name' => $server['name'], - 'type' => 'socks5', - 'server' => $server['host'], - 'port' => $server['port'], - 'username' => $password, - 'password' => $password, - 'udp' => true, - ]; - - if (data_get($protocol_settings, 'tls')) { - $array['tls'] = true; - $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false); - if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { - $array['sni'] = $serverName; - } - } - - return $array; - } - - public static function buildHttp($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $array = [ - 'name' => $server['name'], - 'type' => 'http', - 'server' => $server['host'], - 'port' => $server['port'], - 'username' => $password, - 'password' => $password, - ]; - - if (data_get($protocol_settings, 'tls')) { - $array['tls'] = true; - $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false); - if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { - $array['sni'] = $serverName; - } - } - - return $array; - } - - private function isRegex($exp) - { - if (empty($exp)) { - return false; - } - try { - return preg_match($exp, '') !== false; - } catch (\Exception $e) { - return false; - } - } - - private function isMatch($exp, $str) - { - try { - return preg_match($exp, $str); - } catch (\Exception $e) { - return false; - } - } -} diff --git a/Xboard/app/Protocols/Surfboard.php b/Xboard/app/Protocols/Surfboard.php deleted file mode 100644 index 23fcf09..0000000 --- a/Xboard/app/Protocols/Surfboard.php +++ /dev/null @@ -1,229 +0,0 @@ -servers; - $user = $this->user; - - $appName = admin_setting('app_name', 'XBoard'); - - $proxies = ''; - $proxyGroup = ''; - - foreach ($servers as $item) { - if ( - $item['type'] === Server::TYPE_SHADOWSOCKS - && in_array(data_get($item, 'protocol_settings.cipher'), [ - 'aes-128-gcm', - 'aes-192-gcm', - 'aes-256-gcm', - 'chacha20-ietf-poly1305', - '2022-blake3-aes-128-gcm', - '2022-blake3-aes-256-gcm', - '2022-blake3-chacha20-poly1305' - ]) - ) { - // [Proxy] - $proxies .= self::buildShadowsocks($item['password'], $item); - // [Proxy Group] - $proxyGroup .= $item['name'] . ', '; - } - if ($item['type'] === Server::TYPE_VMESS) { - // [Proxy] - $proxies .= self::buildVmess($item['password'], $item); - // [Proxy Group] - $proxyGroup .= $item['name'] . ', '; - } - if ($item['type'] === Server::TYPE_TROJAN) { - // [Proxy] - $proxies .= self::buildTrojan($item['password'], $item); - // [Proxy Group] - $proxyGroup .= $item['name'] . ', '; - } - if ($item['type'] === Server::TYPE_ANYTLS) { - $proxies .= self::buildAnyTLS($item['password'], $item); - $proxyGroup .= $item['name'] . ', '; - } - } - - $config = subscribe_template('surfboard'); - // Subscription link - $subsURL = Helper::getSubscribeUrl($user['token']); - $subsDomain = request()->header('Host'); - - $config = str_replace('$subs_link', $subsURL, $config); - $config = str_replace('$subs_domain', $subsDomain, $config); - $config = str_replace('$proxies', $proxies, $config); - $config = str_replace('$proxy_group', rtrim($proxyGroup, ', '), $config); - - $upload = round($user['u'] / (1024 * 1024 * 1024), 2); - $download = round($user['d'] / (1024 * 1024 * 1024), 2); - $useTraffic = $upload + $download; - $totalTraffic = round($user['transfer_enable'] / (1024 * 1024 * 1024), 2); - $unusedTraffic = $totalTraffic - $useTraffic; - $expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']); - $subscribeInfo = "title={$appName}订阅信息, content=上传流量:{$upload}GB\\n下载流量:{$download}GB\\n剩余流量:{$unusedTraffic}GB\\n套餐流量:{$totalTraffic}GB\\n到期时间:{$expireDate}"; - $config = str_replace('$subscribe_info', $subscribeInfo, $config); - - return response($config, 200) - ->header('content-disposition', "attachment;filename*=UTF-8''" . rawurlencode($appName) . ".conf"); - } - - - public static function buildShadowsocks($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $config = [ - "{$server['name']}=ss", - "{$server['host']}", - "{$server['port']}", - "encrypt-method=" . data_get($protocol_settings, 'cipher'), - "password={$password}", - 'tfo=true', - 'udp-relay=true' - ]; - - - if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) { - $plugin = data_get($protocol_settings, 'plugin'); - $pluginOpts = data_get($protocol_settings, 'plugin_opts', ''); - // 解析插件选项 - $parsedOpts = collect(explode(';', $pluginOpts)) - ->filter() - ->mapWithKeys(function ($pair) { - if (!str_contains($pair, '=')) { - return []; - } - [$key, $value] = explode('=', $pair, 2); - return [trim($key) => trim($value)]; - }) - ->all(); - switch ($plugin) { - case 'obfs': - $config[] = "obfs={$parsedOpts['obfs']}"; - if (isset($parsedOpts['obfs-host'])) { - $config[] = "obfs-host={$parsedOpts['obfs-host']}"; - } - if (isset($parsedOpts['path'])) { - $config[] = "obfs-uri={$parsedOpts['path']}"; - } - break; - } - } - - $config = array_filter($config); - $uri = implode(',', $config); - $uri .= "\r\n"; - return $uri; - } - - public static function buildVmess($uuid, $server) - { - $protocol_settings = $server['protocol_settings']; - $config = [ - "{$server['name']}=vmess", - "{$server['host']}", - "{$server['port']}", - "username={$uuid}", - "vmess-aead=true", - 'tfo=true', - 'udp-relay=true' - ]; - - if (data_get($protocol_settings, 'tls')) { - array_push($config, 'tls=true'); - if (data_get($protocol_settings, 'tls_settings')) { - $tlsSettings = data_get($protocol_settings, 'tls_settings'); - if (data_get($tlsSettings, 'allow_insecure')) { - array_push($config, 'skip-cert-verify=' . ($tlsSettings['allow_insecure'] ? 'true' : 'false')); - } - if ($sni = data_get($tlsSettings, 'server_name')) { - array_push($config, "sni={$sni}"); - } - } - } - if (data_get($protocol_settings, 'network') === 'ws') { - array_push($config, 'ws=true'); - if (data_get($protocol_settings, 'network_settings')) { - $wsSettings = data_get($protocol_settings, 'network_settings'); - if (isset($wsSettings['path']) && !empty($wsSettings['path'])) - array_push($config, "ws-path={$wsSettings['path']}"); - if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host'])) - array_push($config, "ws-headers=Host:{$wsSettings['headers']['Host']}"); - } - } - - $uri = implode(',', $config); - $uri .= "\r\n"; - return $uri; - } - - public static function buildTrojan($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $config = [ - "{$server['name']}=trojan", - "{$server['host']}", - "{$server['port']}", - "password={$password}", - data_get($protocol_settings, 'server_name') ? "sni=" . data_get($protocol_settings, 'server_name') : "", - 'tfo=true', - 'udp-relay=true' - ]; - if (data_get($protocol_settings, 'allow_insecure')) { - array_push($config, !!data_get($protocol_settings, 'allow_insecure') ? 'skip-cert-verify=true' : 'skip-cert-verify=false'); - } - $config = array_filter($config); - $uri = implode(',', $config); - $uri .= "\r\n"; - return $uri; - } - - public static function buildAnyTLS($password, $server) - { - $protocol_settings = data_get($server, 'protocol_settings', []); - - $config = [ - "{$server['name']}=anytls", - "{$server['host']}", - "{$server['port']}", - "password={$password}", - "tfo=true", - "udp-relay=true" - ]; - - // SNI - if ($serverName = data_get($protocol_settings, 'tls.server_name')) { - $config[] = "sni={$serverName}"; - } - - // 跳过证书校验 - if (data_get($protocol_settings, 'tls.allow_insecure')) { - $config[] = "skip-cert-verify=true"; - } - - $config = array_filter($config); - - return implode(',', $config) . "\r\n"; - } -} diff --git a/Xboard/app/Protocols/Surge.php b/Xboard/app/Protocols/Surge.php deleted file mode 100644 index ee1ea62..0000000 --- a/Xboard/app/Protocols/Surge.php +++ /dev/null @@ -1,319 +0,0 @@ - [2 => '2398'], - ]; - - public function handle() - { - $servers = $this->servers; - $user = $this->user; - - $appName = admin_setting('app_name', 'XBoard'); - - $proxies = ''; - $proxyGroup = ''; - - foreach ($servers as $item) { - if ( - $item['type'] === Server::TYPE_SHADOWSOCKS - && in_array(data_get($item, 'protocol_settings.cipher'), [ - 'aes-128-gcm', - 'aes-192-gcm', - 'aes-256-gcm', - 'chacha20-ietf-poly1305', - '2022-blake3-aes-128-gcm', - '2022-blake3-aes-256-gcm' - ]) - ) { - $proxies .= self::buildShadowsocks($item['password'], $item); - $proxyGroup .= $item['name'] . ', '; - } - if ($item['type'] === Server::TYPE_VMESS) { - $proxies .= self::buildVmess($item['password'], $item); - $proxyGroup .= $item['name'] . ', '; - } - if ($item['type'] === Server::TYPE_TROJAN) { - $proxies .= self::buildTrojan($item['password'], $item); - $proxyGroup .= $item['name'] . ', '; - } - if ($item['type'] === Server::TYPE_HYSTERIA) { - $proxies .= self::buildHysteria($item['password'], $item); - $proxyGroup .= $item['name'] . ', '; - } - if ($item['type'] === Server::TYPE_ANYTLS) { - $proxies .= self::buildAnyTLS($item['password'], $item); - $proxyGroup .= $item['name'] . ', '; - } - if ($item['type'] === Server::TYPE_SOCKS) { - $proxies .= self::buildSocks($item['password'], $item); - $proxyGroup .= $item['name'] . ', '; - } - if ($item['type'] === Server::TYPE_HTTP) { - $proxies .= self::buildHttp($item['password'], $item); - $proxyGroup .= $item['name'] . ', '; - } - } - - - $config = subscribe_template('surge'); - - // Subscription link - $subsDomain = request()->header('Host'); - $subsURL = Helper::getSubscribeUrl($user['token'], $subsDomain ? 'https://' . $subsDomain : null); - - $config = str_replace('$subs_link', $subsURL, $config); - $config = str_replace('$subs_domain', $subsDomain, $config); - $config = str_replace('$proxies', $proxies, $config); - $config = str_replace('$proxy_group', rtrim($proxyGroup, ', '), $config); - - $upload = round($user['u'] / (1024 * 1024 * 1024), 2); - $download = round($user['d'] / (1024 * 1024 * 1024), 2); - $useTraffic = $upload + $download; - $totalTraffic = round($user['transfer_enable'] / (1024 * 1024 * 1024), 2); - $unusedTraffic = $totalTraffic - $useTraffic; - $expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']); - $subscribeInfo = "title={$appName}订阅信息, content=上传流量:{$upload}GB\\n下载流量:{$download}GB\\n剩余流量:{$unusedTraffic}GB\\n套餐流量:{$totalTraffic}GB\\n到期时间:{$expireDate}"; - $config = str_replace('$subscribe_info', $subscribeInfo, $config); - - return response($config, 200) - ->header('content-type', 'application/octet-stream') - ->header('content-disposition', "attachment;filename*=UTF-8''" . rawurlencode($appName) . ".conf"); - } - - - public static function buildShadowsocks($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $config = [ - "{$server['name']} = ss", - "{$server['host']}", - "{$server['port']}", - "encrypt-method={$protocol_settings['cipher']}", - "password={$password}", - 'tfo=true', - 'udp-relay=true' - ]; - if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) { - $plugin = data_get($protocol_settings, 'plugin'); - $pluginOpts = data_get($protocol_settings, 'plugin_opts', ''); - // 解析插件选项 - $parsedOpts = collect(explode(';', $pluginOpts)) - ->filter() - ->mapWithKeys(function ($pair) { - if (!str_contains($pair, '=')) { - return []; - } - [$key, $value] = explode('=', $pair, 2); - return [trim($key) => trim($value)]; - }) - ->all(); - switch ($plugin) { - case 'obfs': - $config[] = "obfs={$parsedOpts['obfs']}"; - if (isset($parsedOpts['obfs-host'])) { - $config[] = "obfs-host={$parsedOpts['obfs-host']}"; - } - if (isset($parsedOpts['path'])) { - $config[] = "obfs-uri={$parsedOpts['path']}"; - } - break; - } - } - $config = array_filter($config); - $uri = implode(',', $config); - $uri .= "\r\n"; - return $uri; - } - - public static function buildVmess($uuid, $server) - { - $protocol_settings = $server['protocol_settings']; - $config = [ - "{$server['name']} = vmess", - "{$server['host']}", - "{$server['port']}", - "username={$uuid}", - "vmess-aead=true", - 'tfo=true', - 'udp-relay=true' - ]; - - if (data_get($protocol_settings, 'tls')) { - array_push($config, 'tls=true'); - if (data_get($protocol_settings, 'tls_settings')) { - $tlsSettings = data_get($protocol_settings, 'tls_settings'); - if (data_get($tlsSettings, 'allow_insecure')) - array_push($config, 'skip-cert-verify=' . ($tlsSettings['allow_insecure'] ? 'true' : 'false')); - if (data_get($tlsSettings, 'server_name')) - array_push($config, "sni={$tlsSettings['server_name']}"); - } - } - if (data_get($protocol_settings, 'network') === 'ws') { - array_push($config, 'ws=true'); - if (data_get($protocol_settings, 'network_settings')) { - $wsSettings = data_get($protocol_settings, 'network_settings'); - if (data_get($wsSettings, 'path')) - array_push($config, "ws-path={$wsSettings['path']}"); - if (data_get($wsSettings, 'headers.Host')) - array_push($config, "ws-headers=Host:{$wsSettings['headers']['Host']}"); - } - } - - $uri = implode(',', $config); - $uri .= "\r\n"; - return $uri; - } - - public static function buildTrojan($password, $server) - { - $protocol_settings = $server['protocol_settings']; - $config = [ - "{$server['name']} = trojan", - "{$server['host']}", - "{$server['port']}", - "password={$password}", - data_get($protocol_settings, 'server_name') ? "sni=" . data_get($protocol_settings, 'server_name') : "", - 'tfo=true', - 'udp-relay=true' - ]; - if (!empty($protocol_settings['allow_insecure'])) { - array_push($config, !!data_get($protocol_settings, 'allow_insecure') ? 'skip-cert-verify=true' : 'skip-cert-verify=false'); - } - $config = array_filter($config); - $uri = implode(',', $config); - $uri .= "\r\n"; - return $uri; - } - - //参考文档: https://manual.nssurge.com/policy/proxy.html - public static function buildAnyTLS($password, $server) - { - $protocol_settings = data_get($server, 'protocol_settings', []); - $config = [ - "{$server['name']} = anytls", - "{$server['host']}", - "{$server['port']}", - "password={$password}", - ]; - if ($serverName = data_get($protocol_settings, 'tls.server_name')) { - $config[] = "sni={$serverName}"; - } - if (data_get($protocol_settings, 'tls.allow_insecure')) { - $config[] = 'skip-cert-verify=true'; - } - $config = array_filter($config); - $uri = implode(',', $config); - $uri .= "\r\n"; - return $uri; - } - - //参考文档: https://manual.nssurge.com/policy/proxy.html - public static function buildHysteria($password, $server) - { - $protocol_settings = $server['protocol_settings']; - if ($protocol_settings['version'] != 2) - return ''; - $config = [ - "{$server['name']} = hysteria2", - "{$server['host']}", - "{$server['port']}", - "password={$password}", - $protocol_settings['tls']['server_name'] ? "sni={$protocol_settings['tls']['server_name']}" : "", - // 'tfo=true', - 'udp-relay=true' - ]; - if (data_get($protocol_settings, 'bandwidth.up')) { - $config[] = "upload-bandwidth={$protocol_settings['bandwidth']['up']}"; - } - if (data_get($protocol_settings, 'bandwidth.down')) { - $config[] = "download-bandwidth={$protocol_settings['bandwidth']['down']}"; - } - if (data_get($protocol_settings, 'tls.allow_insecure')) { - $config[] = !!data_get($protocol_settings, 'tls.allow_insecure') ? 'skip-cert-verify=true' : 'skip-cert-verify=false'; - } - $config = array_filter($config); - $uri = implode(',', $config); - $uri .= "\r\n"; - return $uri; - } - - //参考文档: https://manual.nssurge.com/policy/proxy.html - public static function buildSocks($password, $server) - { - $protocol_settings = data_get($server, 'protocol_settings', []); - $type = data_get($protocol_settings, 'tls') ? 'socks5-tls' : 'socks5'; - $config = [ - "{$server['name']} = {$type}", - "{$server['host']}", - "{$server['port']}", - "{$password}", - "{$password}", - ]; - - if (data_get($protocol_settings, 'tls')) { - if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { - $config[] = "sni={$serverName}"; - } - if (data_get($protocol_settings, 'tls_settings.allow_insecure')) { - $config[] = 'skip-cert-verify=true'; - } - } - $config[] = 'udp-relay=true'; - - $config = array_filter($config); - $uri = implode(',', $config); - $uri .= "\r\n"; - return $uri; - } - - //参考文档: https://manual.nssurge.com/policy/proxy.html - public static function buildHttp($password, $server) - { - $protocol_settings = data_get($server, 'protocol_settings', []); - $type = data_get($protocol_settings, 'tls') ? 'https' : 'http'; - $config = [ - "{$server['name']} = {$type}", - "{$server['host']}", - "{$server['port']}", - "{$password}", - "{$password}", - ]; - - if (data_get($protocol_settings, 'tls')) { - if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { - $config[] = "sni={$serverName}"; - } - if (data_get($protocol_settings, 'tls_settings.allow_insecure')) { - $config[] = 'skip-cert-verify=true'; - } - } - - $config = array_filter($config); - $uri = implode(',', $config); - $uri .= "\r\n"; - return $uri; - } -} diff --git a/Xboard/app/Providers/AuthServiceProvider.php b/Xboard/app/Providers/AuthServiceProvider.php deleted file mode 100644 index b92a7dd..0000000 --- a/Xboard/app/Providers/AuthServiceProvider.php +++ /dev/null @@ -1,28 +0,0 @@ - - */ - protected $policies = [ - // 'App\Model' => 'App\Policies\ModelPolicy', - ]; - - /** - * 注册任何认证/授权服务 - * @return void - */ - public function boot() - { - $this->registerPolicies(); - - // - } -} diff --git a/Xboard/app/Providers/BroadcastServiceProvider.php b/Xboard/app/Providers/BroadcastServiceProvider.php deleted file mode 100644 index 395c518..0000000 --- a/Xboard/app/Providers/BroadcastServiceProvider.php +++ /dev/null @@ -1,21 +0,0 @@ -> - */ - protected $listen = [ - ]; - - /** - * 注册任何事件 - * @return void - */ - public function boot() - { - parent::boot(); - - User::observe(UserObserver::class); - Plan::observe(PlanObserver::class); - Server::observe(ServerObserver::class); - ServerRoute::observe(ServerRouteObserver::class); - - - } -} diff --git a/Xboard/app/Providers/HorizonServiceProvider.php b/Xboard/app/Providers/HorizonServiceProvider.php deleted file mode 100644 index 119fd69..0000000 --- a/Xboard/app/Providers/HorizonServiceProvider.php +++ /dev/null @@ -1,43 +0,0 @@ -email, [ - // - ]); - }); - } -} diff --git a/Xboard/app/Providers/OctaneServiceProvider.php b/Xboard/app/Providers/OctaneServiceProvider.php deleted file mode 100644 index 4cdd91c..0000000 --- a/Xboard/app/Providers/OctaneServiceProvider.php +++ /dev/null @@ -1,43 +0,0 @@ -app->runningInConsole()) { - return; - } - if ($this->app->bound('octane')) { - $this->app['events']->listen(WorkerStarting::class, function () { - app(UpdateService::class)->updateVersionCache(); - HookManager::reset(); - }); - } - // 每半钟执行一次调度检查 - Octane::tick('scheduler', function () { - $lock = Cache::lock('scheduler-lock', 30); - - if ($lock->get()) { - try { - Artisan::call('schedule:run'); - } finally { - $lock->release(); - } - } - })->seconds(30); - } -} \ No newline at end of file diff --git a/Xboard/app/Providers/PluginServiceProvider.php b/Xboard/app/Providers/PluginServiceProvider.php deleted file mode 100644 index f0352ec..0000000 --- a/Xboard/app/Providers/PluginServiceProvider.php +++ /dev/null @@ -1,29 +0,0 @@ -app->scoped(PluginManager::class, function ($app) { - return new PluginManager(); - }); - } - - public function boot(): void - { - if (!file_exists(base_path('plugins'))) { - mkdir(base_path('plugins'), 0755, true); - } - } -} \ No newline at end of file diff --git a/Xboard/app/Providers/ProtocolServiceProvider.php b/Xboard/app/Providers/ProtocolServiceProvider.php deleted file mode 100644 index dfa80ee..0000000 --- a/Xboard/app/Providers/ProtocolServiceProvider.php +++ /dev/null @@ -1,50 +0,0 @@ -app->scoped('protocols.manager', function ($app) { - return new ProtocolManager($app); - }); - - $this->app->scoped('protocols.flags', function ($app) { - return $app->make('protocols.manager')->getAllFlags(); - }); - } - - /** - * 启动服务 - * - * @return void - */ - public function boot() - { - // 在启动时预加载协议类并缓存 - $this->app->make('protocols.manager')->registerAllProtocols(); - - } - - /** - * 提供的服务 - * - * @return array - */ - public function provides() - { - return [ - 'protocols.manager', - 'protocols.flags', - ]; - } -} \ No newline at end of file diff --git a/Xboard/app/Providers/RouteServiceProvider.php b/Xboard/app/Providers/RouteServiceProvider.php deleted file mode 100644 index d26a599..0000000 --- a/Xboard/app/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,87 +0,0 @@ -mapApiRoutes(); - $this->mapWebRoutes(); - - // - } - - /** - * Define the "web" routes for the application. - * - * These routes all receive session state, CSRF protection, etc. - * - * @return void - */ - protected function mapWebRoutes() - { - Route::middleware('web') - ->namespace($this->namespace) - ->group(base_path('routes/web.php')); - } - - /** - * Define the "api" routes for the application. - * - * These routes are typically stateless. - * - * @return void - */ - protected function mapApiRoutes() - { - Route::group([ - 'prefix' => '/api/v1', - 'middleware' => 'api', - 'namespace' => $this->namespace - ], function ($router) { - foreach (glob(app_path('Http//Routes//V1') . '/*.php') as $file) { - $this->app->make('App\\Http\\Routes\\V1\\' . basename($file, '.php'))->map($router); - } - }); - - - Route::group([ - 'prefix' => '/api/v2', - 'middleware' => 'api', - 'namespace' => $this->namespace - ], function ($router) { - foreach (glob(app_path('Http//Routes//V2') . '/*.php') as $file) { - $this->app->make('App\\Http\\Routes\\V2\\' . basename($file, '.php'))->map($router); - } - }); - } -} diff --git a/Xboard/app/Providers/SettingServiceProvider.php b/Xboard/app/Providers/SettingServiceProvider.php deleted file mode 100644 index eee267f..0000000 --- a/Xboard/app/Providers/SettingServiceProvider.php +++ /dev/null @@ -1,33 +0,0 @@ -app->scoped(Setting::class, function (Application $app) { - return new Setting(); - }); - - } - - /** - * Bootstrap services. - * - * @return void - */ - public function boot() - { - // App URL is forced per-request via middleware (Octane-safe). - } -} diff --git a/Xboard/app/Scope/FilterScope.php b/Xboard/app/Scope/FilterScope.php deleted file mode 100644 index fcb92a5..0000000 --- a/Xboard/app/Scope/FilterScope.php +++ /dev/null @@ -1,50 +0,0 @@ -validate([ - 'filter.*.key' => "required|in:{$allowKeys}", - 'filter.*.condition' => 'required|in:in,is,not,like,lt,gt', - 'filter.*.value' => 'required' - ]); - $filters = $request->input('filter'); - if ($filters) { - foreach ($filters as $k => $filter) { - if ($filter['condition'] === 'in') { - $builder->whereIn($filter['key'], $filter['value']); - continue; - } - if ($filter['condition'] === 'is') { - $builder->where($filter['key'], $filter['value']); - continue; - } - if ($filter['condition'] === 'not') { - $builder->where($filter['key'], '!=', $filter['value']); - continue; - } - if ($filter['condition'] === 'gt') { - $builder->where($filter['key'], '>', $filter['value']); - continue; - } - if ($filter['condition'] === 'lt') { - $builder->where($filter['key'], '<', $filter['value']); - continue; - } - if ($filter['condition'] === 'like') { - $builder->where($filter['key'], 'like', "%{$filter['value']}%"); - continue; - } - } - } - return $builder; - } -} \ No newline at end of file diff --git a/Xboard/app/Services/Auth/LoginService.php b/Xboard/app/Services/Auth/LoginService.php deleted file mode 100644 index 363e82b..0000000 --- a/Xboard/app/Services/Auth/LoginService.php +++ /dev/null @@ -1,154 +0,0 @@ -= (int) admin_setting('password_limit_count', 5)) { - return [ - false, - [ - 429, - __('There are too many password errors, please try again after :minute minutes.', [ - 'minute' => admin_setting('password_limit_expire', 60) - ]) - ] - ]; - } - } - - // 查找用户 - $user = User::byEmail($email)->first(); - if (!$user) { - return [false, [400, __('Incorrect email or password')]]; - } - - // 验证密码 - if ( - !Helper::multiPasswordVerify( - $user->password_algo, - $user->password_salt, - $password, - $user->password - ) - ) { - // 增加密码错误计数 - if ((int) admin_setting('password_limit_enable', true)) { - $passwordErrorCount = (int) Cache::get(CacheKey::get('PASSWORD_ERROR_LIMIT', $email), 0); - Cache::put( - CacheKey::get('PASSWORD_ERROR_LIMIT', $email), - (int) $passwordErrorCount + 1, - 60 * (int) admin_setting('password_limit_expire', 60) - ); - } - return [false, [400, __('Incorrect email or password')]]; - } - - // 检查账户状态 - if ($user->banned) { - return [false, [400, __('Your account has been suspended')]]; - } - - // 更新最后登录时间 - $user->last_login_at = time(); - $user->save(); - - HookManager::call('user.login.after', $user); - return [true, $user]; - } - - /** - * 处理密码重置 - * - * @param string $email 用户邮箱 - * @param string $emailCode 邮箱验证码 - * @param string $password 新密码 - * @return array [成功状态, 结果或错误信息] - */ - public function resetPassword(string $email, string $emailCode, string $password): array - { - // 检查重置请求限制 - $forgetRequestLimitKey = CacheKey::get('FORGET_REQUEST_LIMIT', $email); - $forgetRequestLimit = (int) Cache::get($forgetRequestLimitKey); - if ($forgetRequestLimit >= 3) { - return [false, [429, __('Reset failed, Please try again later')]]; - } - - // 验证邮箱验证码 - if ((string) Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $email)) !== (string) $emailCode) { - Cache::put($forgetRequestLimitKey, $forgetRequestLimit ? $forgetRequestLimit + 1 : 1, 300); - return [false, [400, __('Incorrect email verification code')]]; - } - - // 查找用户 - $user = User::byEmail($email)->first(); - if (!$user) { - return [false, [400, __('This email is not registered in the system')]]; - } - - // 更新密码 - $user->password = password_hash($password, PASSWORD_DEFAULT); - $user->password_algo = NULL; - $user->password_salt = NULL; - - if (!$user->save()) { - return [false, [500, __('Reset failed')]]; - } - - HookManager::call('user.password.reset.after', $user); - - // 清除邮箱验证码 - Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $email)); - - return [true, true]; - } - - - /** - * 生成临时登录令牌和快速登录URL - * - * @param User $user 用户对象 - * @param string $redirect 重定向路径 - * @return string|null 快速登录URL - */ - public function generateQuickLoginUrl(User $user, ?string $redirect = null): ?string - { - if (!$user || !$user->exists) { - return null; - } - - $code = Helper::guid(); - $key = CacheKey::get('TEMP_TOKEN', $code); - - Cache::put($key, $user->id, 60); - - $redirect = $redirect ?: 'dashboard'; - $loginRedirect = '/#/login?verify=' . $code . '&redirect=' . rawurlencode($redirect); - - if (admin_setting('app_url')) { - $url = admin_setting('app_url') . $loginRedirect; - } else { - $url = url($loginRedirect); - } - - return $url; - } -} \ No newline at end of file diff --git a/Xboard/app/Services/Auth/MailLinkService.php b/Xboard/app/Services/Auth/MailLinkService.php deleted file mode 100644 index 259ced7..0000000 --- a/Xboard/app/Services/Auth/MailLinkService.php +++ /dev/null @@ -1,100 +0,0 @@ -first(); - if (!$user) { - return [true, true]; // 成功但用户不存在,保护用户隐私 - } - - $code = Helper::guid(); - $key = CacheKey::get('TEMP_TOKEN', $code); - Cache::put($key, $user->id, 300); - Cache::put(CacheKey::get('LAST_SEND_LOGIN_WITH_MAIL_LINK_TIMESTAMP', $email), time(), 60); - - $redirectUrl = '/#/login?verify=' . $code . '&redirect=' . ($redirect ? $redirect : 'dashboard'); - if (admin_setting('app_url')) { - $link = admin_setting('app_url') . $redirectUrl; - } else { - $link = url($redirectUrl); - } - - $this->sendMailLinkEmail($user, $link); - - return [true, $link]; - } - - /** - * 发送邮件链接登录邮件 - * - * @param User $user 用户对象 - * @param string $link 登录链接 - * @return void - */ - private function sendMailLinkEmail(User $user, string $link): void - { - SendEmailJob::dispatch([ - 'email' => $user->email, - 'subject' => __('Login to :name', [ - 'name' => admin_setting('app_name', 'XBoard') - ]), - 'template_name' => 'login', - 'template_value' => [ - 'name' => admin_setting('app_name', 'XBoard'), - 'link' => $link, - 'url' => admin_setting('app_url') - ] - ]); - } - - /** - * 处理Token登录 - * - * @param string $token 登录令牌 - * @return int|null 用户ID或null - */ - public function handleTokenLogin(string $token): ?int - { - $key = CacheKey::get('TEMP_TOKEN', $token); - $userId = Cache::get($key); - - if (!$userId) { - return null; - } - - $user = User::find($userId); - - if (!$user || $user->banned) { - return null; - } - - Cache::forget($key); - - return $userId; - } -} \ No newline at end of file diff --git a/Xboard/app/Services/Auth/RegisterService.php b/Xboard/app/Services/Auth/RegisterService.php deleted file mode 100644 index 78d207e..0000000 --- a/Xboard/app/Services/Auth/RegisterService.php +++ /dev/null @@ -1,193 +0,0 @@ -ip())) ?? 0; - if ((int) $registerCountByIP >= (int) admin_setting('register_limit_count', 3)) { - return [ - false, - [ - 429, - __('Register frequently, please try again after :minute minute', [ - 'minute' => admin_setting('register_limit_expire', 60) - ]) - ] - ]; - } - } - - // 检查验证码 - $captchaService = app(CaptchaService::class); - [$captchaValid, $captchaError] = $captchaService->verify($request); - if (!$captchaValid) { - return [false, $captchaError]; - } - - // 检查邮箱白名单 - if ((int) admin_setting('email_whitelist_enable', 0)) { - if ( - !Helper::emailSuffixVerify( - $request->input('email'), - admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT) - ) - ) { - return [false, [400, __('Email suffix is not in the Whitelist')]]; - } - } - - // 检查Gmail限制 - if ((int) admin_setting('email_gmail_limit_enable', 0)) { - $prefix = explode('@', $request->input('email'))[0]; - if (strpos($prefix, '.') !== false || strpos($prefix, '+') !== false) { - return [false, [400, __('Gmail alias is not supported')]]; - } - } - - // 检查是否关闭注册 - if ((int) admin_setting('stop_register', 0)) { - return [false, [400, __('Registration has closed')]]; - } - - // 检查邀请码要求 - if ((int) admin_setting('invite_force', 0)) { - if (empty($request->input('invite_code'))) { - return [false, [422, __('You must use the invitation code to register')]]; - } - } - - // 检查邮箱验证 - if ((int) admin_setting('email_verify', 0)) { - if (empty($request->input('email_code'))) { - return [false, [422, __('Email verification code cannot be empty')]]; - } - if ((string) Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== (string) $request->input('email_code')) { - return [false, [400, __('Incorrect email verification code')]]; - } - } - - // 检查邮箱是否存在 - $exist = User::byEmail($request->input('email'))->first(); - if ($exist) { - return [false, [400201, __('Email already exists')]]; - } - - return [true, null]; - } - - /** - * 处理邀请码 - * - * @param string $inviteCode 邀请码 - * @return int|null 邀请人ID - */ - public function handleInviteCode(string $inviteCode): int|null - { - $inviteCodeModel = InviteCode::where('code', $inviteCode) - ->where('status', InviteCode::STATUS_UNUSED) - ->first(); - - if (!$inviteCodeModel) { - if ((int) admin_setting('invite_force', 0)) { - throw new ApiException(__('Invalid invitation code')); - } - return null; - } - - if (!(int) admin_setting('invite_never_expire', 0)) { - $inviteCodeModel->status = InviteCode::STATUS_USED; - $inviteCodeModel->save(); - } - - return $inviteCodeModel->user_id; - } - - - - /** - * 注册用户 - * - * @param Request $request 请求对象 - * @return array [成功状态, 用户对象或错误信息] - */ - public function register(Request $request): array - { - // 验证注册数据 - [$valid, $error] = $this->validateRegister($request); - if (!$valid) { - return [false, $error]; - } - - HookManager::call('user.register.before', $request); - - $email = $request->input('email'); - $password = $request->input('password'); - $inviteCode = $request->input('invite_code'); - - // 处理邀请码获取邀请人ID - $inviteUserId = null; - if ($inviteCode) { - $inviteUserId = $this->handleInviteCode($inviteCode); - } - - // 创建用户 - $userService = app(UserService::class); - $user = $userService->createUser([ - 'email' => $email, - 'password' => $password, - 'invite_user_id' => $inviteUserId, - ]); - - // 保存用户 - if (!$user->save()) { - return [false, [500, __('Register failed')]]; - } - - HookManager::call('user.register.after', $user); - - // 清除邮箱验证码 - if ((int) admin_setting('email_verify', 0)) { - Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $email)); - } - - // 更新最近登录时间 - $user->last_login_at = time(); - $user->save(); - - // 更新IP注册计数 - if ((int) admin_setting('register_limit_by_ip_enable', 0)) { - $registerCountByIP = Cache::get(CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip())) ?? 0; - Cache::put( - CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip()), - (int) $registerCountByIP + 1, - (int) admin_setting('register_limit_expire', 60) * 60 - ); - } - - return [true, $user]; - } -} \ No newline at end of file diff --git a/Xboard/app/Services/AuthService.php b/Xboard/app/Services/AuthService.php deleted file mode 100644 index 299f610..0000000 --- a/Xboard/app/Services/AuthService.php +++ /dev/null @@ -1,87 +0,0 @@ -user = $user; - } - - public function generateAuthData(): array - { - // Create a new Sanctum token with device info - $token = $this->user->createToken( - Str::random(20), // token name (device identifier) - ['*'], // abilities - now()->addYear() // expiration - ); - - // Format token: remove ID prefix and add Bearer - $tokenParts = explode('|', $token->plainTextToken); - $formattedToken = 'Bearer ' . ($tokenParts[1] ?? $tokenParts[0]); - - return [ - 'token' => $this->user->token, - 'auth_data' => $formattedToken, - 'is_admin' => $this->user->is_admin, - ]; - } - - public function getSessions(): array - { - return $this->user->tokens()->get()->toArray(); - } - - public function removeSession(string $sessionId): bool - { - $this->user->tokens()->where('id', $sessionId)->delete(); - return true; - } - - public function removeAllSessions(): bool - { - $this->user->tokens()->delete(); - return true; - } - - public static function findUserByBearerToken(string $bearerToken): ?User - { - $token = str_replace('Bearer ', '', $bearerToken); - - $accessToken = PersonalAccessToken::findToken($token); - - $tokenable = $accessToken?->tokenable; - - return $tokenable instanceof User ? $tokenable : null; - } - - /** - * 解密认证数据 - * - * @param string $authorization - * @return array|null 用户数据或null - */ - public static function decryptAuthData(string $authorization): ?array - { - $user = self::findUserByBearerToken($authorization); - - if (!$user) { - return null; - } - - return [ - 'id' => $user->id, - 'email' => $user->email, - 'is_admin' => (bool)$user->is_admin, - 'is_staff' => (bool)$user->is_staff - ]; - } -} diff --git a/Xboard/app/Services/CaptchaService.php b/Xboard/app/Services/CaptchaService.php deleted file mode 100644 index b6a4f05..0000000 --- a/Xboard/app/Services/CaptchaService.php +++ /dev/null @@ -1,112 +0,0 @@ - $this->verifyTurnstile($request), - 'recaptcha-v3' => $this->verifyRecaptchaV3($request), - 'recaptcha' => $this->verifyRecaptcha($request), - default => [false, [400, __('Invalid captcha type')]] - }; - } - - /** - * 验证 Cloudflare Turnstile - * - * @param Request $request - * @return array - */ - private function verifyTurnstile(Request $request): array - { - $turnstileToken = $request->input('turnstile_token'); - if (!$turnstileToken) { - return [false, [400, __('Invalid code is incorrect')]]; - } - - $response = Http::post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [ - 'secret' => admin_setting('turnstile_secret_key'), - 'response' => $turnstileToken, - 'remoteip' => $request->ip() - ]); - - $result = $response->json(); - if (!$result['success']) { - return [false, [400, __('Invalid code is incorrect')]]; - } - - return [true, null]; - } - - /** - * 验证 Google reCAPTCHA v3 - * - * @param Request $request - * @return array - */ - private function verifyRecaptchaV3(Request $request): array - { - $recaptchaV3Token = $request->input('recaptcha_v3_token'); - if (!$recaptchaV3Token) { - return [false, [400, __('Invalid code is incorrect')]]; - } - - $recaptcha = new ReCaptcha(admin_setting('recaptcha_v3_secret_key')); - $recaptchaResp = $recaptcha->verify($recaptchaV3Token, $request->ip()); - - if (!$recaptchaResp->isSuccess()) { - return [false, [400, __('Invalid code is incorrect')]]; - } - - // 检查分数阈值(如果有的话) - $score = $recaptchaResp->getScore(); - $threshold = admin_setting('recaptcha_v3_score_threshold', 0.5); - if ($score < $threshold) { - return [false, [400, __('Invalid code is incorrect')]]; - } - - return [true, null]; - } - - /** - * 验证 Google reCAPTCHA v2 - * - * @param Request $request - * @return array - */ - private function verifyRecaptcha(Request $request): array - { - $recaptchaData = $request->input('recaptcha_data'); - if (!$recaptchaData) { - return [false, [400, __('Invalid code is incorrect')]]; - } - - $recaptcha = new ReCaptcha(admin_setting('recaptcha_key')); - $recaptchaResp = $recaptcha->verify($recaptchaData); - - if (!$recaptchaResp->isSuccess()) { - return [false, [400, __('Invalid code is incorrect')]]; - } - - return [true, null]; - } -} \ No newline at end of file diff --git a/Xboard/app/Services/CouponService.php b/Xboard/app/Services/CouponService.php deleted file mode 100644 index e4bdae7..0000000 --- a/Xboard/app/Services/CouponService.php +++ /dev/null @@ -1,122 +0,0 @@ -coupon = Coupon::where('code', $code) - ->lockForUpdate() - ->first(); - } - - public function use(Order $order): bool - { - $this->setPlanId($order->plan_id); - $this->setUserId($order->user_id); - $this->setPeriod($order->period); - $this->check(); - switch ($this->coupon->type) { - case 1: - $order->discount_amount = $this->coupon->value; - break; - case 2: - $order->discount_amount = $order->total_amount * ($this->coupon->value / 100); - break; - } - if ($order->discount_amount > $order->total_amount) { - $order->discount_amount = $order->total_amount; - } - if ($this->coupon->limit_use !== NULL) { - if ($this->coupon->limit_use <= 0) - return false; - $this->coupon->limit_use = $this->coupon->limit_use - 1; - if (!$this->coupon->save()) { - return false; - } - } - return true; - } - - public function getId() - { - return $this->coupon->id; - } - - public function getCoupon() - { - return $this->coupon; - } - - public function setPlanId($planId) - { - $this->planId = $planId; - } - - public function setUserId($userId) - { - $this->userId = $userId; - } - - public function setPeriod($period) - { - if ($period) { - $this->period = PlanService::getPeriodKey($period); - } - } - - public function checkLimitUseWithUser(): bool - { - $usedCount = Order::where('coupon_id', $this->coupon->id) - ->where('user_id', $this->userId) - ->whereNotIn('status', [0, 2]) - ->count(); - if ($usedCount >= $this->coupon->limit_use_with_user) - return false; - return true; - } - - public function check() - { - if (!$this->coupon || !$this->coupon->show) { - throw new ApiException(__('Invalid coupon')); - } - if ($this->coupon->limit_use <= 0 && $this->coupon->limit_use !== NULL) { - throw new ApiException(__('This coupon is no longer available')); - } - if (time() < $this->coupon->started_at) { - throw new ApiException(__('This coupon has not yet started')); - } - if (time() > $this->coupon->ended_at) { - throw new ApiException(__('This coupon has expired')); - } - if ($this->coupon->limit_plan_ids && $this->planId) { - if (!in_array($this->planId, $this->coupon->limit_plan_ids)) { - throw new ApiException(__('The coupon code cannot be used for this subscription')); - } - } - if ($this->coupon->limit_period && $this->period) { - if (!in_array($this->period, $this->coupon->limit_period)) { - throw new ApiException(__('The coupon code cannot be used for this period')); - } - } - if ($this->coupon->limit_use_with_user !== NULL && $this->userId) { - if (!$this->checkLimitUseWithUser()) { - throw new ApiException(__('The coupon can only be used :limit_use_with_user per person', [ - 'limit_use_with_user' => $this->coupon->limit_use_with_user - ])); - } - } - } -} diff --git a/Xboard/app/Services/DeviceStateService.php b/Xboard/app/Services/DeviceStateService.php deleted file mode 100644 index 09e106c..0000000 --- a/Xboard/app/Services/DeviceStateService.php +++ /dev/null @@ -1,187 +0,0 @@ -removeNodeDevices($nodeId, $userId); - - if (!empty($ips)) { - $fields = []; - foreach ($ips as $ip) { - $fields["{$nodeId}:{$ip}"] = $timestamp; - } - Redis::hMset($key, $fields); - Redis::expire($key, self::TTL); - } - - $this->notifyUpdate($userId); - } - - /** - * 获取某节点的所有设备数据 - * 返回: {userId: [ip1, ip2, ...], ...} - */ - public function getNodeDevices(int $nodeId): array - { - $keys = Redis::keys(self::PREFIX . '*'); - $prefix = "{$nodeId}:"; - $result = []; - foreach ($keys as $key) { - $actualKey = $this->removeRedisPrefix($key); - $uid = (int) substr($actualKey, strlen(self::PREFIX)); - $data = Redis::hgetall($actualKey); - foreach ($data as $field => $timestamp) { - if (str_starts_with($field, $prefix)) { - $ip = substr($field, strlen($prefix)); - $result[$uid][] = $ip; - } - } - } - - return $result; - } - - /** - * 删除某节点某用户的设备 - */ - public function removeNodeDevices(int $nodeId, int $userId): void - { - $key = self::PREFIX . $userId; - $prefix = "{$nodeId}:"; - - foreach (Redis::hkeys($key) as $field) { - if (str_starts_with($field, $prefix)) { - Redis::hdel($key, $field); - } - } - } - - /** - * 清除节点所有设备数据(用于节点断开连接) - */ - public function clearAllNodeDevices(int $nodeId): array - { - $oldDevices = $this->getNodeDevices($nodeId); - $prefix = "{$nodeId}:"; - - foreach ($oldDevices as $userId => $ips) { - $key = self::PREFIX . $userId; - foreach (Redis::hkeys($key) as $field) { - if (str_starts_with($field, $prefix)) { - Redis::hdel($key, $field); - } - } - } - - return array_keys($oldDevices); - } - - /** - * get user device count (deduplicated by IP, filter expired data) - */ - public function getDeviceCount(int $userId): int - { - $data = Redis::hgetall(self::PREFIX . $userId); - $now = time(); - $ips = []; - - foreach ($data as $field => $timestamp) { - if ($now - $timestamp <= self::TTL) { - $ips[] = substr($field, strpos($field, ':') + 1); - } - } - - return count(array_unique($ips)); - } - - /** - * get user device count (for alivelist interface) - */ - public function getAliveList(Collection $users): array - { - if ($users->isEmpty()) { - return []; - } - - $result = []; - foreach ($users as $user) { - $count = $this->getDeviceCount($user->id); - if ($count > 0) { - $result[$user->id] = $count; - } - } - - return $result; - } - - /** - * get devices of multiple users (for sync.devices, filter expired data) - */ - public function getUsersDevices(array $userIds): array - { - $result = []; - $now = time(); - foreach ($userIds as $userId) { - $data = Redis::hgetall(self::PREFIX . $userId); - if (!empty($data)) { - $ips = []; - foreach ($data as $field => $timestamp) { - if ($now - $timestamp <= self::TTL) { - $ips[] = substr($field, strpos($field, ':') + 1); - } - } - if (!empty($ips)) { - $result[$userId] = array_unique($ips); - } - } - } - - return $result; - } - - /** - * notify update (throttle control) - */ - public function notifyUpdate(int $userId): void - { - $dbThrottleKey = "device:db_throttle:{$userId}"; - - // if (Redis::setnx($dbThrottleKey, 1)) { - // Redis::expire($dbThrottleKey, self::DB_THROTTLE); - - User::query() - ->whereKey($userId) - ->update([ - 'online_count' => $this->getDeviceCount($userId), - 'last_online_at' => now(), - ]); - // } - } -} diff --git a/Xboard/app/Services/GiftCardService.php b/Xboard/app/Services/GiftCardService.php deleted file mode 100644 index 8f14cbe..0000000 --- a/Xboard/app/Services/GiftCardService.php +++ /dev/null @@ -1,334 +0,0 @@ -code = GiftCardCode::where('code', $code)->first() - ?? throw new ApiException('兑换码不存在'); - - $this->template = $this->code->template; - } - - /** - * 设置使用用户 - */ - public function setUser(User $user): self - { - $this->user = $user; - return $this; - } - - /** - * 验证兑换码 - */ - public function validate(): self - { - $this->validateIsActive(); - - $eligibility = $this->checkUserEligibility(); - if (!$eligibility['can_redeem']) { - throw new ApiException($eligibility['reason']); - } - - return $this; - } - - /** - * 验证礼品卡本身是否可用 (不检查用户条件) - * @throws ApiException - */ - public function validateIsActive(): self - { - if (!$this->template->isAvailable()) { - throw new ApiException('该礼品卡类型已停用'); - } - - if (!$this->code->isAvailable()) { - throw new ApiException('兑换码不可用:' . $this->code->status_name); - } - return $this; - } - - /** - * 检查用户是否满足兑换条件 (不抛出异常) - */ - public function checkUserEligibility(): array - { - if (!$this->user) { - return [ - 'can_redeem' => false, - 'reason' => '用户信息未提供' - ]; - } - - if (!$this->template->checkUserConditions($this->user)) { - return [ - 'can_redeem' => false, - 'reason' => '您不满足此礼品卡的使用条件' - ]; - } - - if (!$this->template->checkUsageLimit($this->user)) { - return [ - 'can_redeem' => false, - 'reason' => '您已达到此礼品卡的使用限制' - ]; - } - - return ['can_redeem' => true, 'reason' => null]; - } - - /** - * 使用礼品卡 - */ - public function redeem(array $options = []): array - { - if (!$this->user) { - throw new ApiException('未设置使用用户'); - } - - return DB::transaction(function () use ($options) { - $actualRewards = $this->template->calculateActualRewards($this->user); - - if ($this->template->type === GiftCardTemplate::TYPE_MYSTERY) { - $this->code->setActualRewards($actualRewards); - } - - $this->giveRewards($actualRewards); - - $inviteRewards = null; - if ($this->user->invite_user_id && isset($actualRewards['invite_reward_rate'])) { - $inviteRewards = $this->giveInviteRewards($actualRewards); - } - - $this->code->markAsUsed($this->user); - - GiftCardUsage::createRecord( - $this->code, - $this->user, - $actualRewards, - array_merge($options, [ - 'invite_rewards' => $inviteRewards, - 'multiplier' => $this->calculateMultiplier(), - ]) - ); - - return [ - 'rewards' => $actualRewards, - 'invite_rewards' => $inviteRewards, - 'code' => $this->code->code, - 'template_name' => $this->template->name, - ]; - }); - } - - /** - * 发放奖励 - */ - protected function giveRewards(array $rewards): void - { - $userService = app(UserService::class); - - if (isset($rewards['balance']) && $rewards['balance'] > 0) { - if (!$userService->addBalance($this->user->id, $rewards['balance'])) { - throw new ApiException('余额发放失败'); - } - } - - if (isset($rewards['transfer_enable']) && $rewards['transfer_enable'] > 0) { - $this->user->transfer_enable = ($this->user->transfer_enable ?? 0) + $rewards['transfer_enable']; - } - - if (isset($rewards['device_limit']) && $rewards['device_limit'] > 0) { - $this->user->device_limit = ($this->user->device_limit ?? 0) + $rewards['device_limit']; - } - - if (isset($rewards['reset_package']) && $rewards['reset_package']) { - if ($this->user->plan_id) { - app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_GIFT_CARD); - } - } - - if (isset($rewards['plan_id'])) { - $plan = Plan::find($rewards['plan_id']); - if ($plan) { - $userService->assignPlan( - $this->user, - $plan, - $rewards['plan_validity_days'] ?? 0 - ); - } - } else { - // 只有在不是套餐卡的情况下,才处理独立的有效期奖励 - if (isset($rewards['expire_days']) && $rewards['expire_days'] > 0) { - $userService->extendSubscription($this->user, $rewards['expire_days']); - } - } - - // 保存用户更改 - if (!$this->user->save()) { - throw new ApiException('用户信息更新失败'); - } - } - - /** - * 发放邀请人奖励 - */ - protected function giveInviteRewards(array $rewards): ?array - { - if (!$this->user->invite_user_id) { - return null; - } - - $inviteUser = User::find($this->user->invite_user_id); - if (!$inviteUser) { - return null; - } - - $rate = $rewards['invite_reward_rate'] ?? 0.2; - $inviteRewards = []; - - $userService = app(UserService::class); - - // 邀请人余额奖励 - if (isset($rewards['balance']) && $rewards['balance'] > 0) { - $inviteBalance = intval($rewards['balance'] * $rate); - if ($inviteBalance > 0) { - $userService->addBalance($inviteUser->id, $inviteBalance); - $inviteRewards['balance'] = $inviteBalance; - } - } - - // 邀请人流量奖励 - if (isset($rewards['transfer_enable']) && $rewards['transfer_enable'] > 0) { - $inviteTransfer = intval($rewards['transfer_enable'] * $rate); - if ($inviteTransfer > 0) { - $inviteUser->transfer_enable = ($inviteUser->transfer_enable ?? 0) + $inviteTransfer; - $inviteUser->save(); - $inviteRewards['transfer_enable'] = $inviteTransfer; - } - } - - return $inviteRewards; - } - - /** - * 计算倍率 - */ - protected function calculateMultiplier(): float - { - return $this->getFestivalBonus(); - } - - /** - * 获取节日加成倍率 - */ - private function getFestivalBonus(): float - { - $festivalConfig = $this->template->special_config ?? []; - $now = time(); - - if ( - isset($festivalConfig['start_time'], $festivalConfig['end_time']) && - $now >= $festivalConfig['start_time'] && - $now <= $festivalConfig['end_time'] - ) { - return $festivalConfig['festival_bonus'] ?? 1.0; - } - - return 1.0; - } - - /** - * 获取兑换码信息(不包含敏感信息) - */ - public function getCodeInfo(): array - { - $info = [ - 'code' => $this->code->code, - 'template' => [ - 'name' => $this->template->name, - 'description' => $this->template->description, - 'type' => $this->template->type, - 'type_name' => $this->template->type_name, - 'icon' => $this->template->icon, - 'background_image' => $this->template->background_image, - 'theme_color' => $this->template->theme_color, - ], - 'status' => $this->code->status, - 'status_name' => $this->code->status_name, - 'expires_at' => $this->code->expires_at, - 'usage_count' => $this->code->usage_count, - 'max_usage' => $this->code->max_usage, - ]; - if ($this->template->type === GiftCardTemplate::TYPE_PLAN) { - $plan = Plan::find($this->code->template->rewards['plan_id']); - if ($plan) { - $info['plan_info'] = PlanResource::make($plan)->toArray(request()); - } - } - return $info; - } - - /** - * 预览奖励(不实际发放) - */ - public function previewRewards(): array - { - if (!$this->user) { - throw new ApiException('未设置使用用户'); - } - - return $this->template->calculateActualRewards($this->user); - } - - /** - * 获取兑换码 - */ - public function getCode(): GiftCardCode - { - return $this->code; - } - - /** - * 获取模板 - */ - public function getTemplate(): GiftCardTemplate - { - return $this->template; - } - - /** - * 记录日志 - */ - protected function logUsage(string $action, array $data = []): void - { - Log::info('礼品卡使用记录', [ - 'action' => $action, - 'code' => $this->code->code, - 'template_id' => $this->template->id, - 'user_id' => $this->user?->id, - 'data' => $data, - 'ip' => request()->ip(), - 'user_agent' => request()->userAgent(), - ]); - } -} diff --git a/Xboard/app/Services/MailService.php b/Xboard/app/Services/MailService.php deleted file mode 100644 index 7630111..0000000 --- a/Xboard/app/Services/MailService.php +++ /dev/null @@ -1,295 +0,0 @@ -where('remind_expire', true) - ->orWhere('remind_traffic', true); - }) - ->where('banned', false) - ->whereNotNull('email') - ->count(); - } - - /** - * 分块处理用户提醒邮件 - */ - public function processUsersInChunks(int $chunkSize, ?callable $progressCallback = null): array - { - $statistics = [ - 'processed_users' => 0, - 'expire_emails' => 0, - 'traffic_emails' => 0, - 'errors' => 0, - 'skipped' => 0, - ]; - - User::select('id', 'email', 'expired_at', 'transfer_enable', 'u', 'd', 'remind_expire', 'remind_traffic') - ->where(function ($query) { - $query->where('remind_expire', true) - ->orWhere('remind_traffic', true); - }) - ->where('banned', false) - ->whereNotNull('email') - ->chunk($chunkSize, function ($users) use (&$statistics, $progressCallback) { - $this->processUserChunk($users, $statistics); - - if ($progressCallback) { - $progressCallback(); - } - - // 定期清理内存 - if ($statistics['processed_users'] % 2500 === 0) { - gc_collect_cycles(); - } - }); - - return $statistics; - } - - /** - * 处理用户块 - */ - private function processUserChunk($users, array &$statistics): void - { - foreach ($users as $user) { - try { - $statistics['processed_users']++; - $emailsSent = 0; - - // 检查并发送过期提醒 - if ($user->remind_expire && $this->shouldSendExpireRemind($user)) { - $this->remindExpire($user); - $statistics['expire_emails']++; - $emailsSent++; - } - - // 检查并发送流量提醒 - if ($user->remind_traffic && $this->shouldSendTrafficRemind($user)) { - $this->remindTraffic($user); - $statistics['traffic_emails']++; - $emailsSent++; - } - - if ($emailsSent === 0) { - $statistics['skipped']++; - } - - } catch (\Exception $e) { - $statistics['errors']++; - - Log::error('发送提醒邮件失败', [ - 'user_id' => $user->id, - 'email' => $user->email, - 'error' => $e->getMessage() - ]); - } - } - } - - /** - * 检查是否应该发送过期提醒 - */ - private function shouldSendExpireRemind(User $user): bool - { - if ($user->expired_at === NULL) { - return false; - } - $expiredAt = $user->expired_at; - $now = time(); - if (($expiredAt - 86400) < $now && $expiredAt > $now) { - return true; - } - return false; - } - - /** - * 检查是否应该发送流量提醒 - */ - private function shouldSendTrafficRemind(User $user): bool - { - if ($user->transfer_enable <= 0) { - return false; - } - - $usedBytes = $user->u + $user->d; - $usageRatio = $usedBytes / $user->transfer_enable; - - // 流量使用超过80%时发送提醒 - return $usageRatio >= 0.8; - } - - public function remindTraffic(User $user) - { - if (!$user->remind_traffic) - return; - if (!$this->remindTrafficIsWarnValue($user->u, $user->d, $user->transfer_enable)) - return; - $flag = CacheKey::get('LAST_SEND_EMAIL_REMIND_TRAFFIC', $user->id); - if (Cache::get($flag)) - return; - if (!Cache::put($flag, 1, 24 * 3600)) - return; - - SendEmailJob::dispatch([ - 'email' => $user->email, - 'subject' => __('The traffic usage in :app_name has reached 80%', [ - 'app_name' => admin_setting('app_name', 'XBoard') - ]), - 'template_name' => 'remindTraffic', - 'template_value' => [ - 'name' => admin_setting('app_name', 'XBoard'), - 'url' => admin_setting('app_url') - ] - ]); - } - - public function remindExpire(User $user) - { - if (!$this->shouldSendExpireRemind($user)) { - return; - } - - SendEmailJob::dispatch([ - 'email' => $user->email, - 'subject' => __('The service in :app_name is about to expire', [ - 'app_name' => admin_setting('app_name', 'XBoard') - ]), - 'template_name' => 'remindExpire', - 'template_value' => [ - 'name' => admin_setting('app_name', 'XBoard'), - 'url' => admin_setting('app_url') - ] - ]); - } - - private function remindTrafficIsWarnValue($u, $d, $transfer_enable) - { - $ud = $u + $d; - if (!$ud) - return false; - if (!$transfer_enable) - return false; - $percentage = ($ud / $transfer_enable) * 100; - if ($percentage < 80) - return false; - if ($percentage >= 100) - return false; - return true; - } - - /** - * 发送邮件 - * - * @param array $params 包含邮件参数的数组,必须包含以下字段: - * - email: 收件人邮箱地址 - * - subject: 邮件主题 - * - template_name: 邮件模板名称,例如 "welcome" 或 "password_reset" - * - template_value: 邮件模板变量,一个关联数组,包含模板中需要替换的变量和对应的值 - * @return array 包含邮件发送结果的数组,包含以下字段: - * - email: 收件人邮箱地址 - * - subject: 邮件主题 - * - template_name: 邮件模板名称 - * - error: 如果邮件发送失败,包含错误信息;否则为 null - * @throws \InvalidArgumentException 如果 $params 参数缺少必要的字段,抛出此异常 - */ - public static function sendEmail(array $params) - { - if (admin_setting('email_host')) { - Config::set('mail.host', admin_setting('email_host', config('mail.host'))); - Config::set('mail.port', admin_setting('email_port', config('mail.port'))); - Config::set('mail.encryption', admin_setting('email_encryption', config('mail.encryption'))); - Config::set('mail.username', admin_setting('email_username', config('mail.username'))); - Config::set('mail.password', admin_setting('email_password', config('mail.password'))); - Config::set('mail.from.address', admin_setting('email_from_address', config('mail.from.address'))); - Config::set('mail.from.name', admin_setting('app_name', 'XBoard')); - } - $email = $params['email']; - $subject = $params['subject']; - - $templateValue = $params['template_value'] ?? []; - $vars = is_array($templateValue) ? ($templateValue['vars'] ?? []) : []; - $contentMode = is_array($templateValue) ? ($templateValue['content_mode'] ?? null) : null; - - if (is_array($vars) && !empty($vars)) { - $subject = self::renderPlaceholders((string) $subject, $vars); - - if (is_array($templateValue) && isset($templateValue['content']) && is_string($templateValue['content'])) { - $templateValue['content'] = self::renderPlaceholders($templateValue['content'], $vars); - } - } - - // Mass mail default: treat admin content as plain text and escape. - if ($contentMode === 'text' && is_array($templateValue) && isset($templateValue['content']) && is_string($templateValue['content'])) { - $templateValue['content'] = e($templateValue['content']); - } - - $params['template_value'] = $templateValue; - $params['template_name'] = 'mail.' . admin_setting('email_template', 'default') . '.' . $params['template_name']; - try { - Mail::send( - $params['template_name'], - $params['template_value'], - function ($message) use ($email, $subject) { - $message->to($email)->subject($subject); - } - ); - $error = null; - } catch (\Exception $e) { - Log::error($e); - $error = $e->getMessage(); - } - $log = [ - 'email' => $params['email'], - 'subject' => $params['subject'], - 'template_name' => $params['template_name'], - 'error' => $error, - 'config' => config('mail') - ]; - MailLog::create($log); - return $log; - } -} diff --git a/Xboard/app/Services/NodeRegistry.php b/Xboard/app/Services/NodeRegistry.php deleted file mode 100644 index c0f7d13..0000000 --- a/Xboard/app/Services/NodeRegistry.php +++ /dev/null @@ -1,77 +0,0 @@ - nodeId → connection */ - private static array $connections = []; - - public static function add(int $nodeId, TcpConnection $conn): void - { - // Close existing connection for this node (if reconnecting) - if (isset(self::$connections[$nodeId])) { - self::$connections[$nodeId]->close(); - } - self::$connections[$nodeId] = $conn; - } - - public static function remove(int $nodeId): void - { - unset(self::$connections[$nodeId]); - } - - public static function get(int $nodeId): ?TcpConnection - { - return self::$connections[$nodeId] ?? null; - } - - /** - * Send a JSON message to a specific node. - */ - public static function send(int $nodeId, string $event, array $data): bool - { - $conn = self::get($nodeId); - if (!$conn) { - return false; - } - - $payload = json_encode([ - 'event' => $event, - 'data' => $data, - 'timestamp' => time(), - ]); - - $conn->send($payload); - return true; - } - - /** - * Get the connection for a node by ID, checking if it's still alive. - */ - public static function isOnline(int $nodeId): bool - { - $conn = self::get($nodeId); - return $conn !== null && $conn->getStatus() === TcpConnection::STATUS_ESTABLISHED; - } - - /** - * Get all connected node IDs. - * @return int[] - */ - public static function getConnectedNodeIds(): array - { - return array_keys(self::$connections); - } - - public static function count(): int - { - return count(self::$connections); - } -} diff --git a/Xboard/app/Services/NodeSyncService.php b/Xboard/app/Services/NodeSyncService.php deleted file mode 100644 index ebab769..0000000 --- a/Xboard/app/Services/NodeSyncService.php +++ /dev/null @@ -1,143 +0,0 @@ - ServerService::buildNodeConfig($node)]); - } - - /** - * Push all users to all nodes in the group - */ - public static function notifyUsersUpdatedByGroup(int $groupId): void - { - $servers = Server::whereJsonContains('group_ids', (string) $groupId) - ->get(); - - foreach ($servers as $server) { - if (!self::isNodeOnline($server->id)) - continue; - - $users = ServerService::getAvailableUsers($server)->toArray(); - self::push($server->id, 'sync.users', ['users' => $users]); - } - } - - /** - * Push user changes (add/remove) to affected nodes - */ - public static function notifyUserChanged(User $user): void - { - if (!$user->group_id) - return; - - $servers = Server::whereJsonContains('group_ids', (string) $user->group_id)->get(); - foreach ($servers as $server) { - if (!self::isNodeOnline($server->id)) - continue; - - if ($user->isAvailable()) { - self::push($server->id, 'sync.user.delta', [ - 'action' => 'add', - 'users' => [ - [ - 'id' => $user->id, - 'uuid' => $user->uuid, - 'speed_limit' => $user->speed_limit, - 'device_limit' => $user->device_limit, - ] - ], - ]); - } else { - self::push($server->id, 'sync.user.delta', [ - 'action' => 'remove', - 'users' => [['id' => $user->id]], - ]); - } - } - } - - /** - * Push user removal from a specific group's nodes - */ - public static function notifyUserRemovedFromGroup(int $userId, int $groupId): void - { - $servers = Server::whereJsonContains('group_ids', (string) $groupId) - ->get(); - - foreach ($servers as $server) { - if (!self::isNodeOnline($server->id)) - continue; - - self::push($server->id, 'sync.user.delta', [ - 'action' => 'remove', - 'users' => [['id' => $userId]], - ]); - } - } - - /** - * Full sync: push config + users to a node - */ - public static function notifyFullSync(int $nodeId): void - { - if (!self::isNodeOnline($nodeId)) - return; - - $node = Server::find($nodeId); - if (!$node) - return; - - self::push($nodeId, 'sync.config', ['config' => ServerService::buildNodeConfig($node)]); - - $users = ServerService::getAvailableUsers($node)->toArray(); - self::push($nodeId, 'sync.users', ['users' => $users]); - } - - /** - * Publish a push command to Redis — picked up by the Workerman WS server - */ - public static function push(int $nodeId, string $event, array $data): void - { - try { - Redis::publish('node:push', json_encode([ - 'node_id' => $nodeId, - 'event' => $event, - 'data' => $data, - ])); - } catch (\Throwable $e) { - Log::warning("[NodePush] Redis publish failed: {$e->getMessage()}", [ - 'node_id' => $nodeId, - 'event' => $event, - ]); - } - } -} diff --git a/Xboard/app/Services/OrderService.php b/Xboard/app/Services/OrderService.php deleted file mode 100644 index 900be59..0000000 --- a/Xboard/app/Services/OrderService.php +++ /dev/null @@ -1,429 +0,0 @@ - 1, - Plan::PERIOD_QUARTERLY => 3, - Plan::PERIOD_HALF_YEARLY => 6, - Plan::PERIOD_YEARLY => 12, - Plan::PERIOD_TWO_YEARLY => 24, - Plan::PERIOD_THREE_YEARLY => 36 - ]; - public $order; - public $user; - - public function __construct(Order $order) - { - $this->order = $order; - } - - /** - * Create an order from a request. - * - * @param User $user - * @param Plan $plan - * @param string $period - * @param string|null $couponCode - * @return Order - * @throws ApiException - */ - public static function createFromRequest( - User $user, - Plan $plan, - string $period, - ?string $couponCode = null, - ): Order { - $userService = app(UserService::class); - $planService = new PlanService($plan); - - $planService->validatePurchase($user, $period); - HookManager::call('order.create.before', [$user, $plan, $period, $couponCode]); - - return DB::transaction(function () use ($user, $plan, $period, $couponCode, $userService) { - $newPeriod = PlanService::getPeriodKey($period); - - $order = new Order([ - 'user_id' => $user->id, - 'plan_id' => $plan->id, - 'period' => $newPeriod, - 'trade_no' => Helper::generateOrderNo(), - 'total_amount' => (int) (optional($plan->prices)[$newPeriod] * 100), - ]); - - $orderService = new self($order); - - if ($couponCode) { - $orderService->applyCoupon($couponCode); - } - - $orderService->setVipDiscount($user); - $orderService->setOrderType($user); - $orderService->setInvite(user: $user); - - if ($user->balance && $order->total_amount > 0) { - $orderService->handleUserBalance($user, $userService); - } - - if (!$order->save()) { - throw new ApiException(__('Failed to create order')); - } - - HookManager::call('order.create.after', $order); - // 兼容旧钩子 - HookManager::call('order.after_create', $order); - - return $order; - }); - } - - public function open(): void - { - $order = $this->order; - $plan = Plan::find($order->plan_id); - - HookManager::call('order.open.before', $order); - - - DB::transaction(function () use ($order, $plan) { - $this->user = User::lockForUpdate()->find($order->user_id); - - if ($order->refund_amount) { - $this->user->balance += $order->refund_amount; - } - - if ($order->surplus_order_ids) { - Order::whereIn('id', $order->surplus_order_ids) - ->update(['status' => Order::STATUS_DISCOUNTED]); - } - - match ((string) $order->period) { - Plan::PERIOD_ONETIME => $this->buyByOneTime($plan), - Plan::PERIOD_RESET_TRAFFIC => app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER), - default => $this->buyByPeriod($order, $plan), - }; - - $this->setSpeedLimit($plan->speed_limit); - $this->setDeviceLimit($plan->device_limit); - - if (!$this->user->save()) { - throw new \RuntimeException('用户信息保存失败'); - } - - $order->status = Order::STATUS_COMPLETED; - if (!$order->save()) { - throw new \RuntimeException('订单信息保存失败'); - } - }); - - $eventId = match ((int) $order->type) { - Order::STATUS_PROCESSING => admin_setting('new_order_event_id', 0), - Order::TYPE_RENEWAL => admin_setting('renew_order_event_id', 0), - Order::TYPE_UPGRADE => admin_setting('change_order_event_id', 0), - default => 0, - }; - - if ($eventId) { - $this->openEvent($eventId); - } - - HookManager::call('order.open.after', $order); - } - - - public function setOrderType(User $user) - { - $order = $this->order; - if ($order->period === Plan::PERIOD_RESET_TRAFFIC) { - $order->type = Order::TYPE_RESET_TRAFFIC; - } else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id && ($user->expired_at > time() || $user->expired_at === NULL)) { - if (!(int) admin_setting('plan_change_enable', 1)) - throw new ApiException('目前不允许更改订阅,请联系客服或提交工单操作'); - $order->type = Order::TYPE_UPGRADE; - if ((int) admin_setting('surplus_enable', 1)) - $this->getSurplusValue($user, $order); - if ($order->surplus_amount >= $order->total_amount) { - $order->refund_amount = (int) ($order->surplus_amount - $order->total_amount); - $order->total_amount = 0; - } else { - $order->total_amount = (int) ($order->total_amount - $order->surplus_amount); - } - } else if (($user->expired_at === null || $user->expired_at > time()) && $order->plan_id == $user->plan_id) { // 用户订阅未过期或按流量订阅 且购买订阅与当前订阅相同 === 续费 - $order->type = Order::TYPE_RENEWAL; - } else { // 新购 - $order->type = Order::TYPE_NEW_PURCHASE; - } - } - - public function setVipDiscount(User $user) - { - $order = $this->order; - if ($user->discount) { - $order->discount_amount = $order->discount_amount + ($order->total_amount * ($user->discount / 100)); - } - $order->total_amount = $order->total_amount - $order->discount_amount; - } - - public function setInvite(User $user): void - { - $order = $this->order; - if ($user->invite_user_id && ($order->total_amount <= 0)) - return; - $order->invite_user_id = $user->invite_user_id; - $inviter = User::find($user->invite_user_id); - if (!$inviter) - return; - $commissionType = (int) $inviter->commission_type; - if ($commissionType === User::COMMISSION_TYPE_SYSTEM) { - $commissionType = (bool) admin_setting('commission_first_time_enable', true) ? User::COMMISSION_TYPE_ONETIME : User::COMMISSION_TYPE_PERIOD; - } - $isCommission = false; - switch ($commissionType) { - case User::COMMISSION_TYPE_PERIOD: - $isCommission = true; - break; - case User::COMMISSION_TYPE_ONETIME: - $isCommission = !$this->haveValidOrder($user); - break; - } - - if (!$isCommission) - return; - if ($inviter->commission_rate) { - $order->commission_balance = $order->total_amount * ($inviter->commission_rate / 100); - } else { - $order->commission_balance = $order->total_amount * (admin_setting('invite_commission', 10) / 100); - } - } - - private function haveValidOrder(User $user): Order|null - { - return Order::where('user_id', $user->id) - ->whereNotIn('status', [Order::STATUS_PENDING, Order::STATUS_CANCELLED]) - ->first(); - } - - private function getSurplusValue(User $user, Order $order) - { - if ($user->expired_at === NULL) { - $lastOneTimeOrder = Order::where('user_id', $user->id) - ->where('period', Plan::PERIOD_ONETIME) - ->where('status', Order::STATUS_COMPLETED) - ->orderBy('id', 'DESC') - ->first(); - if (!$lastOneTimeOrder) - return; - $nowUserTraffic = Helper::transferToGB($user->transfer_enable); - if (!$nowUserTraffic) - return; - $paidTotalAmount = ($lastOneTimeOrder->total_amount + $lastOneTimeOrder->balance_amount); - if (!$paidTotalAmount) - return; - $trafficUnitPrice = $paidTotalAmount / $nowUserTraffic; - $notUsedTraffic = $nowUserTraffic - Helper::transferToGB($user->u + $user->d); - $result = $trafficUnitPrice * $notUsedTraffic; - $order->surplus_amount = (int) ($result > 0 ? $result : 0); - $order->surplus_order_ids = Order::where('user_id', $user->id) - ->where('period', '!=', Plan::PERIOD_RESET_TRAFFIC) - ->where('status', Order::STATUS_COMPLETED) - ->pluck('id') - ->all(); - } else { - $orders = Order::query() - ->where('user_id', $user->id) - ->whereNotIn('period', [Plan::PERIOD_RESET_TRAFFIC, Plan::PERIOD_ONETIME]) - ->where('status', Order::STATUS_COMPLETED) - ->get(); - - if ($orders->isEmpty()) { - $order->surplus_amount = 0; - $order->surplus_order_ids = []; - return; - } - - $orderAmountSum = $orders->sum(fn($item) => $item->total_amount + $item->balance_amount + $item->surplus_amount - $item->refund_amount); - $orderMonthSum = $orders->sum(fn($item) => self::STR_TO_TIME[PlanService::getPeriodKey($item->period)] ?? 0); - $firstOrderAt = $orders->min('created_at'); - $expiredAt = Carbon::createFromTimestamp($firstOrderAt)->addMonths($orderMonthSum); - - $now = now(); - $totalSeconds = $expiredAt->timestamp - $firstOrderAt; - $remainSeconds = max(0, $expiredAt->timestamp - $now->timestamp); - $cycleRatio = $totalSeconds > 0 ? $remainSeconds / $totalSeconds : 0; - - $plan = Plan::find($user->plan_id); - $totalTraffic = $plan?->transfer_enable * $orderMonthSum; - $usedTraffic = Helper::transferToGB($user->u + $user->d); - $remainTraffic = max(0, $totalTraffic - $usedTraffic); - $trafficRatio = $totalTraffic > 0 ? $remainTraffic / $totalTraffic : 0; - - $ratio = $cycleRatio; - if (admin_setting('change_order_event_id', 0) == 1) { - $ratio = min($cycleRatio, $trafficRatio); - } - - - $order->surplus_amount = (int) max(0, $orderAmountSum * $ratio); - $order->surplus_order_ids = $orders->pluck('id')->all(); - } - } - - public function paid(string $callbackNo) - { - $order = $this->order; - if ($order->status !== Order::STATUS_PENDING) - return true; - $order->status = Order::STATUS_PROCESSING; - $order->paid_at = time(); - $order->callback_no = $callbackNo; - if (!$order->save()) - return false; - try { - OrderHandleJob::dispatchSync($order->trade_no); - } catch (\Exception $e) { - Log::error($e); - return false; - } - return true; - } - - public function cancel(): bool - { - $order = $this->order; - HookManager::call('order.cancel.before', $order); - try { - DB::beginTransaction(); - $order->status = Order::STATUS_CANCELLED; - if (!$order->save()) { - throw new \Exception('Failed to save order status.'); - } - if ($order->balance_amount) { - $userService = new UserService(); - if (!$userService->addBalance($order->user_id, $order->balance_amount)) { - throw new \Exception('Failed to add balance.'); - } - } - DB::commit(); - HookManager::call('order.cancel.after', $order); - return true; - } catch (\Exception $e) { - DB::rollBack(); - Log::error($e); - return false; - } - } - - private function setSpeedLimit($speedLimit) - { - $this->user->speed_limit = $speedLimit; - } - - private function setDeviceLimit($deviceLimit) - { - $this->user->device_limit = $deviceLimit; - } - - private function buyByPeriod(Order $order, Plan $plan) - { - // change plan process - if ((int) $order->type === Order::TYPE_UPGRADE) { - $this->user->expired_at = time(); - } - $this->user->transfer_enable = $plan->transfer_enable * 1073741824; - // 从一次性转换到循环或者新购的时候,重置流量 - if ($this->user->expired_at === NULL || $order->type === Order::TYPE_NEW_PURCHASE) - app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER); - $this->user->plan_id = $plan->id; - $this->user->group_id = $plan->group_id; - $this->user->expired_at = $this->getTime($order->period, $this->user->expired_at); - } - - private function buyByOneTime(Plan $plan) - { - app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER); - $this->user->transfer_enable = $plan->transfer_enable * 1073741824; - $this->user->plan_id = $plan->id; - $this->user->group_id = $plan->group_id; - $this->user->expired_at = NULL; - } - - /** - * 计算套餐到期时间 - * @param string $periodKey - * @param int $timestamp - * @return int - * @throws ApiException - */ - private function getTime(string $periodKey, ?int $timestamp = null): int - { - $timestamp = $timestamp < time() ? time() : $timestamp; - $periodKey = PlanService::getPeriodKey($periodKey); - - if (isset(self::STR_TO_TIME[$periodKey])) { - $months = self::STR_TO_TIME[$periodKey]; - return Carbon::createFromTimestamp($timestamp)->addMonths($months)->timestamp; - } - - throw new ApiException('无效的套餐周期'); - } - - private function openEvent($eventId) - { - switch ((int) $eventId) { - case 0: - break; - case 1: - app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER); - break; - } - } - - protected function applyCoupon(string $couponCode): void - { - $couponService = new CouponService($couponCode); - if (!$couponService->use($this->order)) { - throw new ApiException(__('Coupon failed')); - } - $this->order->coupon_id = $couponService->getId(); - } - - /** - * Summary of handleUserBalance - * @param User $user - * @param UserService $userService - * @return void - */ - protected function handleUserBalance(User $user, UserService $userService): void - { - $remainingBalance = $user->balance - $this->order->total_amount; - - if ($remainingBalance >= 0) { - if (!$userService->addBalance($this->order->user_id, -$this->order->total_amount)) { - throw new ApiException(__('Insufficient balance')); - } - $this->order->balance_amount = $this->order->total_amount; - $this->order->total_amount = 0; - } else { - if (!$userService->addBalance($this->order->user_id, -$user->balance)) { - throw new ApiException(__('Insufficient balance')); - } - $this->order->balance_amount = $user->balance; - $this->order->total_amount = $this->order->total_amount - $user->balance; - } - } -} diff --git a/Xboard/app/Services/PaymentService.php b/Xboard/app/Services/PaymentService.php deleted file mode 100644 index c496d38..0000000 --- a/Xboard/app/Services/PaymentService.php +++ /dev/null @@ -1,135 +0,0 @@ -method = $method; - $this->pluginManager = app(PluginManager::class); - - if ($method === 'temp') { - return; - } - - if ($id) { - $paymentModel = Payment::find($id); - if (!$paymentModel) { - throw new ApiException('payment not found'); - } - $payment = $paymentModel->toArray(); - } - if ($uuid) { - $paymentModel = Payment::where('uuid', $uuid)->first(); - if (!$paymentModel) { - throw new ApiException('payment not found'); - } - $payment = $paymentModel->toArray(); - } - - $this->config = []; - if (isset($payment)) { - $this->config = is_string($payment['config']) ? json_decode($payment['config'], true) : $payment['config']; - $this->config['enable'] = $payment['enable']; - $this->config['id'] = $payment['id']; - $this->config['uuid'] = $payment['uuid']; - $this->config['notify_domain'] = $payment['notify_domain'] ?? ''; - } - - $paymentMethods = $this->getAvailablePaymentMethods(); - if (isset($paymentMethods[$this->method])) { - $pluginCode = $paymentMethods[$this->method]['plugin_code']; - $paymentPlugins = $this->pluginManager->getEnabledPaymentPlugins(); - foreach ($paymentPlugins as $plugin) { - if ($plugin->getPluginCode() === $pluginCode) { - $plugin->setConfig($this->config); - $this->payment = $plugin; - return; - } - } - } - - $this->payment = new $this->class($this->config); - } - - public function notify($params) - { - if (!$this->config['enable']) - throw new ApiException('gate is not enable'); - return $this->payment->notify($params); - } - - public function pay($order) - { - // custom notify domain name - $notifyUrl = url("/api/v1/guest/payment/notify/{$this->method}/{$this->config['uuid']}"); - if ($this->config['notify_domain']) { - $parseUrl = parse_url($notifyUrl); - $notifyUrl = $this->config['notify_domain'] . $parseUrl['path']; - } - - return $this->payment->pay([ - 'notify_url' => $notifyUrl, - 'return_url' => source_base_url('/#/order/' . $order['trade_no']), - 'trade_no' => $order['trade_no'], - 'total_amount' => $order['total_amount'], - 'user_id' => $order['user_id'], - 'stripe_token' => $order['stripe_token'] - ]); - } - - public function form() - { - $form = $this->payment->form(); - $result = []; - foreach ($form as $key => $field) { - $result[$key] = [ - 'type' => $field['type'], - 'label' => $field['label'] ?? '', - 'placeholder' => $field['placeholder'] ?? '', - 'description' => $field['description'] ?? '', - 'value' => $this->config[$key] ?? $field['default'] ?? '', - 'options' => $field['select_options'] ?? $field['options'] ?? [] - ]; - } - return $result; - } - - /** - * 获取所有可用的支付方式 - */ - public function getAvailablePaymentMethods(): array - { - $methods = []; - - $methods = HookManager::filter('available_payment_methods', $methods); - - return $methods; - } - - /** - * 获取所有支付方式名称列表(用于管理后台) - */ - public static function getAllPaymentMethodNames(): array - { - $pluginManager = app(PluginManager::class); - $pluginManager->initializeEnabledPlugins(); - - $instance = new self('temp'); - $methods = $instance->getAvailablePaymentMethods(); - - return array_keys($methods); - } -} diff --git a/Xboard/app/Services/PlanService.php b/Xboard/app/Services/PlanService.php deleted file mode 100644 index e67363d..0000000 --- a/Xboard/app/Services/PlanService.php +++ /dev/null @@ -1,194 +0,0 @@ -plan = $plan; - } - - /** - * 获取所有可销售的订阅计划列表 - * 条件:show 和 sell 为 true,且容量充足 - * - * @return Collection - */ - public function getAvailablePlans(): Collection - { - return Plan::where('show', true) - ->where('sell', true) - ->orderBy('sort') - ->get() - ->filter(function ($plan) { - return $this->hasCapacity($plan); - }); - } - - /** - * 获取指定订阅计划的可用状态 - * 条件:renew 和 sell 为 true - * - * @param int $planId - * @return Plan|null - */ - public function getAvailablePlan(int $planId): ?Plan - { - return Plan::where('id', $planId) - ->where('sell', true) - ->where('renew', true) - ->first(); - } - - /** - * 检查指定计划是否可用于指定用户 - * - * @param Plan $plan - * @param User $user - * @return bool - */ - public function isPlanAvailableForUser(Plan $plan, User $user): bool - { - // 如果是续费 - if ($user->plan_id === $plan->id) { - return $plan->renew; - } - - // 如果是新购 - return $plan->show && $plan->sell && $this->hasCapacity($plan); - } - - public function validatePurchase(User $user, string $period): void - { - if (!$this->plan) { - throw new ApiException(__('Subscription plan does not exist')); - } - - // 转换周期格式为新版格式 - $periodKey = self::getPeriodKey($period); - $price = $this->plan->prices[$periodKey] ?? null; - - if ($price === null) { - throw new ApiException(__('This payment period cannot be purchased, please choose another period')); - } - - if ($periodKey === Plan::PERIOD_RESET_TRAFFIC) { - $this->validateResetTrafficPurchase($user); - return; - } - - if ($user->plan_id !== $this->plan->id && !$this->hasCapacity($this->plan)) { - throw new ApiException(__('Current product is sold out')); - } - - $this->validatePlanAvailability($user); - } - - /** - * 智能转换周期格式为新版格式 - * 如果是新版格式直接返回,如果是旧版格式则转换为新版格式 - * - * @param string $period - * @return string - */ - public static function getPeriodKey(string $period): string - { - // 如果是新版格式直接返回 - if (in_array($period, self::getNewPeriods())) { - return $period; - } - - // 如果是旧版格式则转换为新版格式 - return Plan::LEGACY_PERIOD_MAPPING[$period] ?? $period; - } - /** - * 只能转换周期格式为旧版本 - */ - public static function convertToLegacyPeriod(string $period): string - { - $flippedMapping = array_flip(Plan::LEGACY_PERIOD_MAPPING); - return $flippedMapping[$period] ?? $period; - } - - /** - * 获取所有支持的新版周期格式 - * - * @return array - */ - public static function getNewPeriods(): array - { - return array_values(Plan::LEGACY_PERIOD_MAPPING); - } - - /** - * 获取旧版周期格式 - * - * @param string $period - * @return string - */ - public static function getLegacyPeriod(string $period): string - { - $flipped = array_flip(Plan::LEGACY_PERIOD_MAPPING); - return $flipped[$period] ?? $period; - } - - protected function validateResetTrafficPurchase(User $user): void - { - if (!app(UserService::class)->isAvailable($user) || $this->plan->id !== $user->plan_id) { - throw new ApiException(__('Subscription has expired or no active subscription, unable to purchase Data Reset Package')); - } - } - - protected function validatePlanAvailability(User $user): void - { - if ((!$this->plan->show && !$this->plan->renew) || (!$this->plan->show && $user->plan_id !== $this->plan->id)) { - throw new ApiException(__('This subscription has been sold out, please choose another subscription')); - } - - if (!$this->plan->renew && $user->plan_id == $this->plan->id) { - throw new ApiException(__('This subscription cannot be renewed, please change to another subscription')); - } - - if (!$this->plan->show && $this->plan->renew && !app(UserService::class)->isAvailable($user)) { - throw new ApiException(__('This subscription has expired, please change to another subscription')); - } - } - - public function hasCapacity(Plan $plan): bool - { - if ($plan->capacity_limit === null) { - return true; - } - - $activeUserCount = User::where('plan_id', $plan->id) - ->where(function ($query) { - $query->where('expired_at', '>=', time()) - ->orWhereNull('expired_at'); - }) - ->count(); - - return ($plan->capacity_limit - $activeUserCount) > 0; - } - - public function getAvailablePeriods(Plan $plan): array - { - return array_filter( - $plan->getActivePeriods(), - fn($period) => isset($plan->prices[$period]) && $plan->prices[$period] > 0 - ); - } - - public function canResetTraffic(Plan $plan): bool - { - return $plan->reset_traffic_method !== Plan::RESET_TRAFFIC_NEVER - && $plan->getResetTrafficPrice() > 0; - } -} diff --git a/Xboard/app/Services/Plugin/AbstractPlugin.php b/Xboard/app/Services/Plugin/AbstractPlugin.php deleted file mode 100644 index 23da9a3..0000000 --- a/Xboard/app/Services/Plugin/AbstractPlugin.php +++ /dev/null @@ -1,222 +0,0 @@ -pluginCode = $pluginCode; - $this->namespace = 'Plugin\\' . Str::studly($pluginCode); - $reflection = new \ReflectionClass($this); - $this->basePath = dirname($reflection->getFileName()); - } - - /** - * 获取插件代码 - */ - public function getPluginCode(): string - { - return $this->pluginCode; - } - - /** - * 获取插件命名空间 - */ - public function getNamespace(): string - { - return $this->namespace; - } - - /** - * 获取插件基础路径 - */ - public function getBasePath(): string - { - return $this->basePath; - } - - /** - * 设置配置 - */ - public function setConfig(array $config): void - { - $this->config = $config; - } - - /** - * 获取配置 - */ - public function getConfig(?string $key = null, $default = null): mixed - { - $config = $this->config; - if ($key) { - $config = $config[$key] ?? $default; - } - return $config; - } - - /** - * 获取视图 - */ - protected function view(string $view, array $data = [], array $mergeData = []): \Illuminate\Contracts\View\View - { - return view(Str::studly($this->pluginCode) . '::' . $view, $data, $mergeData); - } - - /** - * 注册动作钩子监听器 - */ - protected function listen(string $hook, callable $callback, int $priority = 20): void - { - HookManager::register($hook, $callback, $priority); - } - - /** - * 注册过滤器钩子 - */ - protected function filter(string $hook, callable $callback, int $priority = 20): void - { - HookManager::registerFilter($hook, $callback, $priority); - } - - /** - * 移除事件监听器 - */ - protected function removeListener(string $hook): void - { - HookManager::remove($hook); - } - - /** - * 注册 Artisan 命令 - */ - protected function registerCommand(string $commandClass): void - { - if (class_exists($commandClass)) { - app('Illuminate\Contracts\Console\Kernel')->registerCommand(new $commandClass()); - } - } - - /** - * 注册插件命令目录 - */ - public function registerCommands(): void - { - $commandsPath = $this->basePath . '/Commands'; - if (File::exists($commandsPath)) { - $files = File::glob($commandsPath . '/*.php'); - foreach ($files as $file) { - $className = pathinfo($file, PATHINFO_FILENAME); - $commandClass = $this->namespace . '\\Commands\\' . $className; - - if (class_exists($commandClass)) { - $this->registerCommand($commandClass); - } - } - } - } - - /** - * 中断当前请求并返回新的响应 - * - * @param Response|string|array $response - * @return never - */ - protected function intercept(Response|string|array $response): never - { - HookManager::intercept($response); - } - - /** - * 插件启动时调用 - */ - public function boot(): void - { - // 插件启动时的初始化逻辑 - } - - /** - * 插件安装时调用 - */ - public function install(): void - { - // 插件安装时的初始化逻辑 - } - - /** - * 插件卸载时调用 - */ - public function cleanup(): void - { - // 插件卸载时的清理逻辑 - } - - /** - * 插件更新时调用 - */ - public function update(string $oldVersion, string $newVersion): void - { - // 插件更新时的迁移逻辑 - } - - /** - * 获取插件资源URL - */ - protected function asset(string $path): string - { - return asset('plugins/' . $this->pluginCode . '/' . ltrim($path, '/')); - } - - /** - * 获取插件配置项 - */ - protected function getConfigValue(string $key, $default = null) - { - return $this->config[$key] ?? $default; - } - - /** - * 获取插件数据库迁移路径 - */ - protected function getMigrationsPath(): string - { - return $this->basePath . '/database/migrations'; - } - - /** - * 获取插件视图路径 - */ - protected function getViewsPath(): string - { - return $this->basePath . '/resources/views'; - } - - /** - * 获取插件资源路径 - */ - protected function getAssetsPath(): string - { - return $this->basePath . '/resources/assets'; - } - - /** - * Register plugin scheduled tasks. Plugins can override this method. - * - * @param \Illuminate\Console\Scheduling\Schedule $schedule - * @return void - */ - public function schedule(\Illuminate\Console\Scheduling\Schedule $schedule): void - { - // Plugin can override this method to register scheduled tasks - } -} \ No newline at end of file diff --git a/Xboard/app/Services/Plugin/HookManager.php b/Xboard/app/Services/Plugin/HookManager.php deleted file mode 100644 index 5b6e9af..0000000 --- a/Xboard/app/Services/Plugin/HookManager.php +++ /dev/null @@ -1,286 +0,0 @@ -json($response); - } - - throw new InterceptResponseException($response); - } - - /** - * Trigger action hook - * - * @param string $hook Hook name - * @param mixed $payload Data passed to hook - * @return void - */ - public static function call(string $hook, mixed $payload = null): void - { - $actions = self::getActions(); - - if (!isset($actions[$hook])) { - return; - } - - ksort($actions[$hook]); - - foreach ($actions[$hook] as $callbacks) { - foreach ($callbacks as $callback) { - $callback($payload); - } - } - } - - /** - * Trigger filter hook - * - * @param string $hook Hook name - * @param mixed $value Value to filter - * @param mixed ...$args Other parameters - * @return mixed - */ - public static function filter(string $hook, mixed $value, mixed ...$args): mixed - { - $filters = self::getFilters(); - - if (!isset($filters[$hook])) { - return $value; - } - - ksort($filters[$hook]); - - $result = $value; - foreach ($filters[$hook] as $callbacks) { - foreach ($callbacks as $callback) { - $result = $callback($result, ...$args); - } - } - - return $result; - } - - /** - * Register action hook listener - * - * @param string $hook Hook name - * @param callable $callback Callback function - * @param int $priority Priority - * @return void - */ - public static function register(string $hook, callable $callback, int $priority = 20): void - { - $actions = self::getActions(); - - if (!isset($actions[$hook])) { - $actions[$hook] = []; - } - - if (!isset($actions[$hook][$priority])) { - $actions[$hook][$priority] = []; - } - - $actions[$hook][$priority][self::getCallableId($callback)] = $callback; - - self::setActions($actions); - } - - /** - * Register filter hook - * - * @param string $hook Hook name - * @param callable $callback Callback function - * @param int $priority Priority - * @return void - */ - public static function registerFilter(string $hook, callable $callback, int $priority = 20): void - { - $filters = self::getFilters(); - - if (!isset($filters[$hook])) { - $filters[$hook] = []; - } - - if (!isset($filters[$hook][$priority])) { - $filters[$hook][$priority] = []; - } - - $filters[$hook][$priority][self::getCallableId($callback)] = $callback; - - self::setFilters($filters); - } - - /** - * Remove hook listener - * - * @param string $hook Hook name - * @param callable|null $callback Callback function - * @return void - */ - public static function remove(string $hook, ?callable $callback = null): void - { - $actions = self::getActions(); - $filters = self::getFilters(); - - if ($callback === null) { - if (isset($actions[$hook])) { - unset($actions[$hook]); - self::setActions($actions); - } - - if (isset($filters[$hook])) { - unset($filters[$hook]); - self::setFilters($filters); - } - - return; - } - - $callbackId = self::getCallableId($callback); - - if (isset($actions[$hook])) { - foreach ($actions[$hook] as $priority => $callbacks) { - if (isset($callbacks[$callbackId])) { - unset($actions[$hook][$priority][$callbackId]); - - if (empty($actions[$hook][$priority])) { - unset($actions[$hook][$priority]); - } - - if (empty($actions[$hook])) { - unset($actions[$hook]); - } - } - } - self::setActions($actions); - } - - if (isset($filters[$hook])) { - foreach ($filters[$hook] as $priority => $callbacks) { - if (isset($callbacks[$callbackId])) { - unset($filters[$hook][$priority][$callbackId]); - - if (empty($filters[$hook][$priority])) { - unset($filters[$hook][$priority]); - } - - if (empty($filters[$hook])) { - unset($filters[$hook]); - } - } - } - self::setFilters($filters); - } - } - - /** - * Check if hook exists - * - * @param string $hook Hook name - * @return bool - */ - public static function hasHook(string $hook): bool - { - $actions = self::getActions(); - $filters = self::getFilters(); - - return isset($actions[$hook]) || isset($filters[$hook]); - } - - /** - * Clear all hooks (called when Octane resets) - */ - public static function reset(): void - { - App::instance('hook.actions', []); - App::instance('hook.filters', []); - } -} \ No newline at end of file diff --git a/Xboard/app/Services/Plugin/InterceptResponseException.php b/Xboard/app/Services/Plugin/InterceptResponseException.php deleted file mode 100644 index 298d8d3..0000000 --- a/Xboard/app/Services/Plugin/InterceptResponseException.php +++ /dev/null @@ -1,22 +0,0 @@ -response = $response; - } - - public function getResponse(): Response - { - return $this->response; - } -} \ No newline at end of file diff --git a/Xboard/app/Services/Plugin/PluginConfigService.php b/Xboard/app/Services/Plugin/PluginConfigService.php deleted file mode 100644 index 977614b..0000000 --- a/Xboard/app/Services/Plugin/PluginConfigService.php +++ /dev/null @@ -1,111 +0,0 @@ -pluginManager = app(PluginManager::class); - } - - /** - * 获取插件配置 - * - * @param string $pluginCode - * @return array - */ - public function getConfig(string $pluginCode): array - { - $defaultConfig = $this->getDefaultConfig($pluginCode); - if (empty($defaultConfig)) { - return []; - } - $dbConfig = $this->getDbConfig($pluginCode); - - $result = []; - foreach ($defaultConfig as $key => $item) { - $result[$key] = [ - 'type' => $item['type'], - 'label' => $item['label'] ?? '', - 'placeholder' => $item['placeholder'] ?? '', - 'description' => $item['description'] ?? '', - 'value' => $dbConfig[$key] ?? $item['default'], - 'options' => $item['options'] ?? [] - ]; - } - - return $result; - } - - /** - * 更新插件配置 - * - * @param string $pluginCode - * @param array $config - * @return bool - */ - public function updateConfig(string $pluginCode, array $config): bool - { - $defaultConfig = $this->getDefaultConfig($pluginCode); - if (empty($defaultConfig)) { - throw new \Exception('插件配置结构不存在'); - } - $values = []; - foreach ($config as $key => $value) { - if (!isset($defaultConfig[$key])) { - continue; - } - $values[$key] = $value; - } - Plugin::query() - ->where('code', $pluginCode) - ->update([ - 'config' => json_encode($values), - 'updated_at' => now() - ]); - - return true; - } - - /** - * 获取插件默认配置 - * - * @param string $pluginCode - * @return array - */ - protected function getDefaultConfig(string $pluginCode): array - { - $configFile = $this->pluginManager->getPluginPath($pluginCode) . '/config.json'; - if (!File::exists($configFile)) { - return []; - } - - $config = json_decode(File::get($configFile), true); - return $config['config'] ?? []; - } - - /** - * 获取数据库中的配置 - * - * @param string $pluginCode - * @return array - */ - public function getDbConfig(string $pluginCode): array - { - $plugin = Plugin::query() - ->where('code', $pluginCode) - ->first(); - - if (!$plugin || empty($plugin->config)) { - return []; - } - - return json_decode($plugin->config, true); - } -} \ No newline at end of file diff --git a/Xboard/app/Services/Plugin/PluginManager.php b/Xboard/app/Services/Plugin/PluginManager.php deleted file mode 100644 index 7859da0..0000000 --- a/Xboard/app/Services/Plugin/PluginManager.php +++ /dev/null @@ -1,727 +0,0 @@ -pluginPath = base_path('plugins'); - } - - /** - * 获取插件的命名空间 - */ - public function getPluginNamespace(string $pluginCode): string - { - return 'Plugin\\' . Str::studly($pluginCode); - } - - /** - * 获取插件的基础路径 - */ - public function getPluginPath(string $pluginCode): string - { - return $this->pluginPath . '/' . Str::studly($pluginCode); - } - - /** - * 加载插件类 - */ - protected function loadPlugin(string $pluginCode): ?AbstractPlugin - { - if (isset($this->loadedPlugins[$pluginCode])) { - return $this->loadedPlugins[$pluginCode]; - } - - $pluginClass = $this->getPluginNamespace($pluginCode) . '\\Plugin'; - - if (!class_exists($pluginClass)) { - $pluginFile = $this->getPluginPath($pluginCode) . '/Plugin.php'; - if (!File::exists($pluginFile)) { - Log::warning("Plugin class file not found: {$pluginFile}"); - Plugin::query()->where('code', $pluginCode)->delete(); - return null; - } - require_once $pluginFile; - } - - if (!class_exists($pluginClass)) { - Log::error("Plugin class not found: {$pluginClass}"); - return null; - } - - $plugin = new $pluginClass($pluginCode); - $this->loadedPlugins[$pluginCode] = $plugin; - - return $plugin; - } - - /** - * 注册插件的服务提供者 - */ - protected function registerServiceProvider(string $pluginCode): void - { - $providerClass = $this->getPluginNamespace($pluginCode) . '\\Providers\\PluginServiceProvider'; - - if (class_exists($providerClass)) { - app()->register($providerClass); - } - } - - /** - * 加载插件的路由 - */ - protected function loadRoutes(string $pluginCode): void - { - $routesPath = $this->getPluginPath($pluginCode) . '/routes'; - if (File::exists($routesPath)) { - $webRouteFile = $routesPath . '/web.php'; - $apiRouteFile = $routesPath . '/api.php'; - if (File::exists($webRouteFile)) { - Route::middleware('web') - ->namespace($this->getPluginNamespace($pluginCode) . '\\Controllers') - ->group(function () use ($webRouteFile) { - require $webRouteFile; - }); - } - if (File::exists($apiRouteFile)) { - Route::middleware('api') - ->namespace($this->getPluginNamespace($pluginCode) . '\\Controllers') - ->group(function () use ($apiRouteFile) { - require $apiRouteFile; - }); - } - } - } - - /** - * 加载插件的视图 - */ - protected function loadViews(string $pluginCode): void - { - $viewsPath = $this->getPluginPath($pluginCode) . '/resources/views'; - if (File::exists($viewsPath)) { - View::addNamespace(Str::studly($pluginCode), $viewsPath); - return; - } - } - - /** - * 注册插件命令 - */ - protected function registerPluginCommands(string $pluginCode, AbstractPlugin $pluginInstance): void - { - try { - // 调用插件的命令注册方法 - $pluginInstance->registerCommands(); - } catch (\Exception $e) { - Log::error("Failed to register commands for plugin '{$pluginCode}': " . $e->getMessage()); - } - } - - /** - * 安装插件 - */ - public function install(string $pluginCode): bool - { - $configFile = $this->getPluginPath($pluginCode) . '/config.json'; - - if (!File::exists($configFile)) { - throw new \Exception('Plugin config file not found'); - } - - $config = json_decode(File::get($configFile), true); - if (!$this->validateConfig($config)) { - throw new \Exception('Invalid plugin config'); - } - - // 检查插件是否已安装 - if (Plugin::where('code', $pluginCode)->exists()) { - throw new \Exception('Plugin already installed'); - } - - // 检查依赖 - if (!$this->checkDependencies($config['require'] ?? [])) { - throw new \Exception('Dependencies not satisfied'); - } - - // 运行数据库迁移 - $this->runMigrations(pluginCode: $pluginCode); - - DB::beginTransaction(); - try { - // 提取配置默认值 - $defaultValues = $this->extractDefaultConfig($config); - - // 创建插件实例 - $plugin = $this->loadPlugin($pluginCode); - - // 注册到数据库 - Plugin::create([ - 'code' => $pluginCode, - 'name' => $config['name'], - 'version' => $config['version'], - 'type' => $config['type'] ?? Plugin::TYPE_FEATURE, - 'is_enabled' => false, - 'config' => json_encode($defaultValues), - 'installed_at' => now(), - ]); - - // 运行插件安装方法 - if (method_exists($plugin, 'install')) { - $plugin->install(); - } - - // 发布插件资源 - $this->publishAssets($pluginCode); - - DB::commit(); - return true; - } catch (\Exception $e) { - if (DB::transactionLevel() > 0) { - DB::rollBack(); - } - throw $e; - } - } - - /** - * 提取插件默认配置 - */ - protected function extractDefaultConfig(array $config): array - { - $defaultValues = []; - if (isset($config['config']) && is_array($config['config'])) { - foreach ($config['config'] as $key => $item) { - if (is_array($item)) { - $defaultValues[$key] = $item['default'] ?? null; - } else { - $defaultValues[$key] = $item; - } - } - } - return $defaultValues; - } - - /** - * 运行插件数据库迁移 - */ - protected function runMigrations(string $pluginCode): void - { - $migrationsPath = $this->getPluginPath($pluginCode) . '/database/migrations'; - - if (File::exists($migrationsPath)) { - Artisan::call('migrate', [ - '--path' => "plugins/" . Str::studly($pluginCode) . "/database/migrations", - '--force' => true - ]); - } - } - - /** - * 回滚插件数据库迁移 - */ - protected function runMigrationsRollback(string $pluginCode): void - { - $migrationsPath = $this->getPluginPath($pluginCode) . '/database/migrations'; - - if (File::exists($migrationsPath)) { - Artisan::call('migrate:rollback', [ - '--path' => "plugins/" . Str::studly($pluginCode) . "/database/migrations", - '--force' => true - ]); - } - } - - /** - * 发布插件资源 - */ - protected function publishAssets(string $pluginCode): void - { - $assetsPath = $this->getPluginPath($pluginCode) . '/resources/assets'; - if (File::exists($assetsPath)) { - $publishPath = public_path('plugins/' . $pluginCode); - File::ensureDirectoryExists($publishPath); - File::copyDirectory($assetsPath, $publishPath); - } - } - - /** - * 验证配置文件 - */ - protected function validateConfig(array $config): bool - { - $requiredFields = [ - 'name', - 'code', - 'version', - 'description', - 'author' - ]; - - foreach ($requiredFields as $field) { - if (!isset($config[$field]) || empty($config[$field])) { - return false; - } - } - - // 验证插件代码格式 - if (!preg_match('/^[a-z0-9_]+$/', $config['code'])) { - return false; - } - - // 验证版本号格式 - if (!preg_match('/^\d+\.\d+\.\d+$/', $config['version'])) { - return false; - } - - // 验证插件类型 - if (isset($config['type'])) { - $validTypes = ['feature', 'payment']; - if (!in_array($config['type'], $validTypes)) { - return false; - } - } - - return true; - } - - /** - * 启用插件 - */ - public function enable(string $pluginCode): bool - { - $plugin = $this->loadPlugin($pluginCode); - - if (!$plugin) { - Plugin::where('code', $pluginCode)->delete(); - throw new \Exception('Plugin not found: ' . $pluginCode); - } - - // 获取插件配置 - $dbPlugin = Plugin::query() - ->where('code', $pluginCode) - ->first(); - - if ($dbPlugin && !empty($dbPlugin->config)) { - $values = json_decode($dbPlugin->config, true) ?: []; - $values = $this->castConfigValuesByType($pluginCode, $values); - $plugin->setConfig($values); - } - - // 注册服务提供者 - $this->registerServiceProvider($pluginCode); - - // 加载路由 - $this->loadRoutes($pluginCode); - - // 加载视图 - $this->loadViews($pluginCode); - - // 更新数据库状态 - Plugin::query() - ->where('code', $pluginCode) - ->update([ - 'is_enabled' => true, - 'updated_at' => now(), - ]); - // 初始化插件 - $plugin->boot(); - - return true; - } - - /** - * 禁用插件 - */ - public function disable(string $pluginCode): bool - { - $plugin = $this->loadPlugin($pluginCode); - if (!$plugin) { - throw new \Exception('Plugin not found'); - } - - Plugin::query() - ->where('code', $pluginCode) - ->update([ - 'is_enabled' => false, - 'updated_at' => now(), - ]); - - $plugin->cleanup(); - - return true; - } - - /** - * 卸载插件 - */ - public function uninstall(string $pluginCode): bool - { - $this->disable($pluginCode); - $this->runMigrationsRollback($pluginCode); - Plugin::query()->where('code', $pluginCode)->delete(); - - return true; - } - - /** - * 删除插件 - * - * @param string $pluginCode - * @return bool - * @throws \Exception - */ - public function delete(string $pluginCode): bool - { - // 先卸载插件 - if (Plugin::where('code', $pluginCode)->exists()) { - $this->uninstall($pluginCode); - } - - $pluginPath = $this->getPluginPath($pluginCode); - if (!File::exists($pluginPath)) { - throw new \Exception('插件不存在'); - } - - // 删除插件目录 - File::deleteDirectory($pluginPath); - - return true; - } - - /** - * 检查依赖关系 - */ - protected function checkDependencies(array $requires): bool - { - foreach ($requires as $package => $version) { - if ($package === 'xboard') { - // 检查xboard版本 - // 实现版本比较逻辑 - } - } - return true; - } - - /** - * 升级插件 - * - * @param string $pluginCode - * @return bool - * @throws \Exception - */ - public function update(string $pluginCode): bool - { - $dbPlugin = Plugin::where('code', $pluginCode)->first(); - if (!$dbPlugin) { - throw new \Exception('Plugin not installed: ' . $pluginCode); - } - - // 获取插件配置文件中的最新版本 - $configFile = $this->getPluginPath($pluginCode) . '/config.json'; - if (!File::exists($configFile)) { - throw new \Exception('Plugin config file not found'); - } - - $config = json_decode(File::get($configFile), true); - if (!$config || !isset($config['version'])) { - throw new \Exception('Invalid plugin config or missing version'); - } - - $newVersion = $config['version']; - $oldVersion = $dbPlugin->version; - - if (version_compare($newVersion, $oldVersion, '<=')) { - throw new \Exception('Plugin is already up to date'); - } - - $this->disable($pluginCode); - $this->runMigrations($pluginCode); - - $plugin = $this->loadPlugin($pluginCode); - if ($plugin) { - if (!empty($dbPlugin->config)) { - $values = json_decode($dbPlugin->config, true) ?: []; - $values = $this->castConfigValuesByType($pluginCode, $values); - $plugin->setConfig($values); - } - - $plugin->update($oldVersion, $newVersion); - } - - $dbPlugin->update([ - 'version' => $newVersion, - 'updated_at' => now(), - ]); - - $this->enable($pluginCode); - - return true; - } - - /** - * 上传插件 - * - * @param \Illuminate\Http\UploadedFile $file - * @return bool - * @throws \Exception - */ - public function upload($file): bool - { - $tmpPath = storage_path('tmp/plugins'); - if (!File::exists($tmpPath)) { - File::makeDirectory($tmpPath, 0755, true); - } - - $extractPath = $tmpPath . '/' . uniqid(); - $zip = new \ZipArchive(); - - if ($zip->open($file->path()) !== true) { - throw new \Exception('无法打开插件包文件'); - } - - $zip->extractTo($extractPath); - $zip->close(); - - $configFile = File::glob($extractPath . '/*/config.json'); - if (empty($configFile)) { - $configFile = File::glob($extractPath . '/config.json'); - } - - if (empty($configFile)) { - File::deleteDirectory($extractPath); - throw new \Exception('插件包格式错误:缺少配置文件'); - } - - $pluginPath = dirname(reset($configFile)); - $config = json_decode(File::get($pluginPath . '/config.json'), true); - - if (!$this->validateConfig($config)) { - File::deleteDirectory($extractPath); - throw new \Exception('插件配置文件格式错误'); - } - - $targetPath = $this->pluginPath . '/' . Str::studly($config['code']); - if (File::exists($targetPath)) { - $installedConfigPath = $targetPath . '/config.json'; - if (!File::exists($installedConfigPath)) { - throw new \Exception('已安装插件缺少配置文件,无法判断是否可升级'); - } - $installedConfig = json_decode(File::get($installedConfigPath), true); - - $oldVersion = $installedConfig['version'] ?? null; - $newVersion = $config['version'] ?? null; - if (!$oldVersion || !$newVersion) { - throw new \Exception('插件缺少版本号,无法判断是否可升级'); - } - if (version_compare($newVersion, $oldVersion, '<=')) { - throw new \Exception('上传插件版本不高于已安装版本,无法升级'); - } - - File::deleteDirectory($targetPath); - } - - File::copyDirectory($pluginPath, $targetPath); - File::deleteDirectory($pluginPath); - File::deleteDirectory($extractPath); - - if (Plugin::where('code', $config['code'])->exists()) { - return $this->update($config['code']); - } - - return true; - } - - /** - * Initializes all enabled plugins from the database. - * This method ensures that plugins are loaded, and their routes, views, - * and service providers are registered only once per request cycle. - */ - public function initializeEnabledPlugins(): void - { - if ($this->pluginsInitialized) { - return; - } - - $enabledPlugins = Plugin::where('is_enabled', true)->get(); - - foreach ($enabledPlugins as $dbPlugin) { - try { - $pluginCode = $dbPlugin->code; - - $pluginInstance = $this->loadPlugin($pluginCode); - if (!$pluginInstance) { - continue; - } - - if (!empty($dbPlugin->config)) { - $values = json_decode($dbPlugin->config, true) ?: []; - $values = $this->castConfigValuesByType($pluginCode, $values); - $pluginInstance->setConfig($values); - } - - $this->registerServiceProvider($pluginCode); - $this->loadRoutes($pluginCode); - $this->loadViews($pluginCode); - $this->registerPluginCommands($pluginCode, $pluginInstance); - - $pluginInstance->boot(); - - } catch (\Exception $e) { - Log::error("Failed to initialize plugin '{$dbPlugin->code}': " . $e->getMessage()); - } - } - - $this->pluginsInitialized = true; - } - - /** - * Register scheduled tasks for all enabled plugins. - * Called from Console Kernel. Only loads main plugin class and config for scheduling. - * Avoids full HTTP/plugin boot overhead. - * - * @param \Illuminate\Console\Scheduling\Schedule $schedule - */ - public function registerPluginSchedules(Schedule $schedule): void - { - Plugin::where('is_enabled', true) - ->get() - ->each(function ($dbPlugin) use ($schedule) { - try { - $pluginInstance = $this->loadPlugin($dbPlugin->code); - if (!$pluginInstance) { - return; - } - if (!empty($dbPlugin->config)) { - $values = json_decode($dbPlugin->config, true) ?: []; - $values = $this->castConfigValuesByType($dbPlugin->code, $values); - $pluginInstance->setConfig($values); - } - $pluginInstance->schedule($schedule); - - } catch (\Exception $e) { - Log::error("Failed to register schedule for plugin '{$dbPlugin->code}': " . $e->getMessage()); - } - }); - } - - /** - * Get all enabled plugin instances. - * - * This method ensures that all enabled plugins are initialized and then returns them. - * It's the central point for accessing active plugins. - * - * @return array - */ - public function getEnabledPlugins(): array - { - $this->initializeEnabledPlugins(); - - $enabledPluginCodes = Plugin::where('is_enabled', true) - ->pluck('code') - ->all(); - - return array_intersect_key($this->loadedPlugins, array_flip($enabledPluginCodes)); - } - - /** - * Get enabled plugins by type - */ - public function getEnabledPluginsByType(string $type): array - { - $this->initializeEnabledPlugins(); - - $enabledPluginCodes = Plugin::where('is_enabled', true) - ->byType($type) - ->pluck('code') - ->all(); - - return array_intersect_key($this->loadedPlugins, array_flip($enabledPluginCodes)); - } - - /** - * Get enabled payment plugins - */ - public function getEnabledPaymentPlugins(): array - { - return $this->getEnabledPluginsByType('payment'); - } - - /** - * install default plugins - */ - public static function installDefaultPlugins(): void - { - foreach (Plugin::PROTECTED_PLUGINS as $pluginCode) { - if (!Plugin::where('code', $pluginCode)->exists()) { - $pluginManager = app(self::class); - $pluginManager->install($pluginCode); - $pluginManager->enable($pluginCode); - Log::info("Installed and enabled default plugin: {$pluginCode}"); - } - } - } - - /** - * 根据 config.json 的类型信息对配置值进行类型转换(仅处理 type=json 键)。 - */ - protected function castConfigValuesByType(string $pluginCode, array $values): array - { - $types = $this->getConfigTypes($pluginCode); - foreach ($values as $key => $value) { - $type = $types[$key] ?? null; - - if ($type === 'json') { - if (is_array($value)) { - continue; - } - - if (is_string($value) && $value !== '') { - $decoded = json_decode($value, true); - if (json_last_error() === JSON_ERROR_NONE) { - $values[$key] = $decoded; - } - } - } - } - return $values; - } - - /** - * 读取并缓存插件 config.json 中的键类型映射。 - */ - protected function getConfigTypes(string $pluginCode): array - { - if (isset($this->configTypesCache[$pluginCode])) { - return $this->configTypesCache[$pluginCode]; - } - $types = []; - $configFile = $this->getPluginPath($pluginCode) . '/config.json'; - if (File::exists($configFile)) { - $config = json_decode(File::get($configFile), true); - $fields = $config['config'] ?? []; - foreach ($fields as $key => $meta) { - $types[$key] = is_array($meta) ? ($meta['type'] ?? 'string') : 'string'; - } - } - $this->configTypesCache[$pluginCode] = $types; - return $types; - } -} \ No newline at end of file diff --git a/Xboard/app/Services/ServerService.php b/Xboard/app/Services/ServerService.php deleted file mode 100644 index 00491ca..0000000 --- a/Xboard/app/Services/ServerService.php +++ /dev/null @@ -1,296 +0,0 @@ -get()->append([ - 'last_check_at', - 'last_push_at', - 'online', - 'is_online', - 'available_status', - 'cache_key', - 'load_status', - 'metrics', - 'online_conn' - ]); - } - - /** - * 获取指定用户可用的服务器列表 - * @param User $user - * @return array - */ - public static function getAvailableServers(User $user): array - { - $servers = Server::whereJsonContains('group_ids', (string) $user->group_id) - ->where('show', true) - ->where(function ($query) { - $query->whereNull('transfer_enable') - ->orWhere('transfer_enable', 0) - ->orWhereRaw('u + d < transfer_enable'); - }) - ->orderBy('sort', 'ASC') - ->get() - ->append(['last_check_at', 'last_push_at', 'online', 'is_online', 'available_status', 'cache_key', 'server_key']); - - $servers = collect($servers)->map(function ($server) use ($user) { - // 判断动态端口 - if (str_contains($server->port, '-')) { - $port = $server->port; - $server->port = (int) Helper::randomPort($port); - $server->ports = $port; - } else { - $server->port = (int) $server->port; - } - $server->password = $server->generateServerPassword($user); - $server->rate = $server->getCurrentRate(); - return $server; - })->toArray(); - - return $servers; - } - - /** - * 根据权限组获取可用的用户列表 - * @param array $groupIds - * @return Collection - */ - public static function getAvailableUsers(Server $node) - { - $users = User::toBase() - ->whereIn('group_id', $node->group_ids) - ->whereRaw('u + d < transfer_enable') - ->where(function ($query) { - $query->where('expired_at', '>=', time()) - ->orWhere('expired_at', NULL); - }) - ->where('banned', 0) - ->select([ - 'id', - 'uuid', - 'speed_limit', - 'device_limit' - ]) - ->get(); - return HookManager::filter('server.users.get', $users, $node); - } - - // 获取路由规则 - public static function getRoutes(array $routeIds) - { - $routes = ServerRoute::select(['id', 'match', 'action', 'action_value'])->whereIn('id', $routeIds)->get(); - return $routes; - } - - /** - * Update node metrics and load status - */ - public static function updateMetrics(Server $node, array $metrics): void - { - $nodeType = strtoupper($node->type); - $nodeId = $node->id; - $cacheTime = max(300, (int) admin_setting('server_push_interval', 60) * 3); - - $metricsData = [ - 'uptime' => (int) ($metrics['uptime'] ?? 0), - 'goroutines' => (int) ($metrics['goroutines'] ?? 0), - 'active_connections' => (int) ($metrics['active_connections'] ?? 0), - 'total_connections' => (int) ($metrics['total_connections'] ?? 0), - 'total_users' => (int) ($metrics['total_users'] ?? 0), - 'active_users' => (int) ($metrics['active_users'] ?? 0), - 'inbound_speed' => (int) ($metrics['inbound_speed'] ?? 0), - 'outbound_speed' => (int) ($metrics['outbound_speed'] ?? 0), - 'cpu_per_core' => $metrics['cpu_per_core'] ?? [], - 'load' => $metrics['load'] ?? [], - 'speed_limiter' => $metrics['speed_limiter'] ?? [], - 'gc' => $metrics['gc'] ?? [], - 'api' => $metrics['api'] ?? [], - 'ws' => $metrics['ws'] ?? [], - 'limits' => $metrics['limits'] ?? [], - 'updated_at' => now()->timestamp, - 'kernel_status' => (bool) ($metrics['kernel_status'] ?? false), - ]; - - \Illuminate\Support\Facades\Cache::put( - \App\Utils\CacheKey::get('SERVER_' . $nodeType . '_METRICS', $nodeId), - $metricsData, - $cacheTime - ); - } - - public static function buildNodeConfig(Server $node): array - { - $nodeType = $node->type; - $protocolSettings = $node->protocol_settings; - $serverPort = $node->server_port; - $host = $node->host; - - $baseConfig = [ - 'protocol' => $nodeType, - 'listen_ip' => '0.0.0.0', - 'server_port' => (int) $serverPort, - 'network' => data_get($protocolSettings, 'network'), - 'networkSettings' => data_get($protocolSettings, 'network_settings') ?: null, - ]; - - $response = match ($nodeType) { - 'shadowsocks' => [ - ...$baseConfig, - 'cipher' => $protocolSettings['cipher'], - 'plugin' => $protocolSettings['plugin'], - 'plugin_opts' => $protocolSettings['plugin_opts'], - 'server_key' => match ($protocolSettings['cipher']) { - '2022-blake3-aes-128-gcm' => Helper::getServerKey($node->created_at, 16), - '2022-blake3-aes-256-gcm' => Helper::getServerKey($node->created_at, 32), - default => null, - }, - ], - 'vmess' => [ - ...$baseConfig, - 'tls' => (int) $protocolSettings['tls'], - 'multiplex' => data_get($protocolSettings, 'multiplex'), - ], - 'trojan' => [ - ...$baseConfig, - 'host' => $host, - 'server_name' => $protocolSettings['server_name'], - 'multiplex' => data_get($protocolSettings, 'multiplex'), - 'tls' => (int) $protocolSettings['tls'], - 'tls_settings' => match ((int) $protocolSettings['tls']) { - 2 => $protocolSettings['reality_settings'], - default => null, - }, - ], - 'vless' => [ - ...$baseConfig, - 'tls' => (int) $protocolSettings['tls'], - 'flow' => $protocolSettings['flow'], - 'decryption' => data_get($protocolSettings, 'encryption.decryption'), - 'tls_settings' => match ((int) $protocolSettings['tls']) { - 2 => $protocolSettings['reality_settings'], - default => $protocolSettings['tls_settings'], - }, - 'multiplex' => data_get($protocolSettings, 'multiplex'), - ], - 'hysteria' => [ - ...$baseConfig, - 'server_port' => (int) $serverPort, - 'version' => (int) $protocolSettings['version'], - 'host' => $host, - 'server_name' => $protocolSettings['tls']['server_name'], - 'up_mbps' => (int) $protocolSettings['bandwidth']['up'], - 'down_mbps' => (int) $protocolSettings['bandwidth']['down'], - ...match ((int) $protocolSettings['version']) { - 1 => ['obfs' => $protocolSettings['obfs']['password'] ?? null], - 2 => [ - 'obfs' => $protocolSettings['obfs']['open'] ? $protocolSettings['obfs']['type'] : null, - 'obfs-password' => $protocolSettings['obfs']['password'] ?? null, - ], - default => [], - }, - ], - 'tuic' => [ - ...$baseConfig, - 'version' => (int) $protocolSettings['version'], - 'server_port' => (int) $serverPort, - 'server_name' => $protocolSettings['tls']['server_name'], - 'congestion_control' => $protocolSettings['congestion_control'], - 'tls_settings' => data_get($protocolSettings, 'tls_settings'), - 'auth_timeout' => '3s', - 'zero_rtt_handshake' => false, - 'heartbeat' => '3s', - ], - 'anytls' => [ - ...$baseConfig, - 'server_port' => (int) $serverPort, - 'server_name' => $protocolSettings['tls']['server_name'], - 'padding_scheme' => $protocolSettings['padding_scheme'], - ], - 'socks' => [ - ...$baseConfig, - 'server_port' => (int) $serverPort, - ], - 'naive' => [ - ...$baseConfig, - 'server_port' => (int) $serverPort, - 'tls' => (int) $protocolSettings['tls'], - 'tls_settings' => $protocolSettings['tls_settings'], - ], - 'http' => [ - ...$baseConfig, - 'server_port' => (int) $serverPort, - 'tls' => (int) $protocolSettings['tls'], - 'tls_settings' => $protocolSettings['tls_settings'], - ], - 'mieru' => [ - ...$baseConfig, - 'server_port' => (int) $serverPort, - 'transport' => data_get($protocolSettings, 'transport', 'TCP'), - 'traffic_pattern' => $protocolSettings['traffic_pattern'], - // 'multiplex' => data_get($protocolSettings, 'multiplex'), - ], - default => [], - }; - - // $response = array_filter( - // $response, - // static fn ($value) => $value !== null - // ); - - if (!empty($node['route_ids'])) { - $response['routes'] = self::getRoutes($node['route_ids']); - } - - if (!empty($node['custom_outbounds'])) { - $response['custom_outbounds'] = $node['custom_outbounds']; - } - - if (!empty($node['custom_routes'])) { - $response['custom_routes'] = $node['custom_routes']; - } - - if (!empty($node['cert_config']) && data_get($node['cert_config'],'cert_mode') !== 'none' ) { - $response['cert_config'] = $node['cert_config']; - } - - return $response; - } - - /** - * 根据协议类型和标识获取服务器 - * @param int $serverId - * @param string $serverType - * @return Server|null - */ - public static function getServer($serverId, ?string $serverType = null): Server | null - { - return Server::query() - ->when($serverType, function ($query) use ($serverType) { - $query->where('type', Server::normalizeType($serverType)); - }) - ->where(function ($query) use ($serverId) { - $query->where('code', $serverId) - ->orWhere('id', $serverId); - }) - ->orderByRaw('CASE WHEN code = ? THEN 0 ELSE 1 END', [$serverId]) - ->first(); - } -} diff --git a/Xboard/app/Services/SettingService.php b/Xboard/app/Services/SettingService.php deleted file mode 100644 index 37e34d9..0000000 --- a/Xboard/app/Services/SettingService.php +++ /dev/null @@ -1,18 +0,0 @@ -first(); - return $setting ? $setting->value : $default; - } - - public function getAll(){ - return SettingModel::all()->pluck('value', 'name')->toArray(); - } -} diff --git a/Xboard/app/Services/StatisticalService.php b/Xboard/app/Services/StatisticalService.php deleted file mode 100644 index e57d6ae..0000000 --- a/Xboard/app/Services/StatisticalService.php +++ /dev/null @@ -1,378 +0,0 @@ -redis = Redis::connection(); - - } - - public function setStartAt($timestamp) - { - $this->startAt = $timestamp; - $this->statServerKey = "stat_server_{$this->startAt}"; - $this->statUserKey = "stat_user_{$this->startAt}"; - } - - public function setEndAt($timestamp) - { - $this->endAt = $timestamp; - } - - /** - * 生成统计报表 - */ - public function generateStatData(): array - { - $startAt = $this->startAt; - $endAt = $this->endAt; - if (!$startAt || !$endAt) { - $startAt = strtotime(date('Y-m-d')); - $endAt = strtotime('+1 day', $startAt); - } - $data = []; - $data['order_count'] = Order::where('created_at', '>=', $startAt) - ->where('created_at', '<', $endAt) - ->count(); - $data['order_total'] = Order::where('created_at', '>=', $startAt) - ->where('created_at', '<', $endAt) - ->sum('total_amount'); - $data['paid_count'] = Order::where('paid_at', '>=', $startAt) - ->where('paid_at', '<', $endAt) - ->whereNotIn('status', [0, 2]) - ->count(); - $data['paid_total'] = Order::where('paid_at', '>=', $startAt) - ->where('paid_at', '<', $endAt) - ->whereNotIn('status', [0, 2]) - ->sum('total_amount'); - $commissionLogBuilder = CommissionLog::where('created_at', '>=', $startAt) - ->where('created_at', '<', $endAt); - $data['commission_count'] = $commissionLogBuilder->count(); - $data['commission_total'] = $commissionLogBuilder->sum('get_amount'); - $data['register_count'] = User::where('created_at', '>=', $startAt) - ->where('created_at', '<', $endAt) - ->count(); - $data['invite_count'] = User::where('created_at', '>=', $startAt) - ->where('created_at', '<', $endAt) - ->whereNotNull('invite_user_id') - ->count(); - $data['transfer_used_total'] = StatServer::where('created_at', '>=', $startAt) - ->where('created_at', '<', $endAt) - ->select(DB::raw('SUM(u) + SUM(d) as total')) - ->value('total') ?? 0; - return $data; - } - - /** - * 往服务器报表缓存正追加流量使用数据 - */ - public function statServer($serverId, $serverType, $u, $d) - { - $u_menber = "{$serverType}_{$serverId}_u"; //储存上传流量的集合成员 - $d_menber = "{$serverType}_{$serverId}_d"; //储存下载流量的集合成员 - $this->redis->zincrby($this->statServerKey, $u, $u_menber); - $this->redis->zincrby($this->statServerKey, $d, $d_menber); - } - - /** - * 追加用户使用流量 - */ - public function statUser($rate, $userId, $u, $d) - { - $u_menber = "{$rate}_{$userId}_u"; //储存上传流量的集合成员 - $d_menber = "{$rate}_{$userId}_d"; //储存下载流量的集合成员 - $this->redis->zincrby($this->statUserKey, $u, $u_menber); - $this->redis->zincrby($this->statUserKey, $d, $d_menber); - } - - /** - * 获取指定用户的流量使用情况 - */ - public function getStatUserByUserID(int|string $userId): array - { - - $stats = []; - $statsUser = $this->redis->zrange($this->statUserKey, 0, -1, true); - foreach ($statsUser as $member => $value) { - list($rate, $uid, $type) = explode('_', $member); - if (intval($uid) !== intval($userId)) - continue; - $key = "{$rate}_{$uid}"; - $stats[$key] = $stats[$key] ?? [ - 'record_at' => $this->startAt, - 'server_rate' => number_format((float) $rate, 2, '.', ''), - 'u' => 0, - 'd' => 0, - 'user_id' => intval($userId), - ]; - $stats[$key][$type] += $value; - } - return array_values($stats); - } - - /** - * 获取缓存中的用户报表 - */ - public function getStatUser() - { - $stats = []; - $statsUser = $this->redis->zrange($this->statUserKey, 0, -1, true); - foreach ($statsUser as $member => $value) { - list($rate, $uid, $type) = explode('_', $member); - $key = "{$rate}_{$uid}"; - $stats[$key] = $stats[$key] ?? [ - 'record_at' => $this->startAt, - 'server_rate' => $rate, - 'u' => 0, - 'd' => 0, - 'user_id' => intval($uid), - ]; - $stats[$key][$type] += $value; - } - return array_values($stats); - } - - - /** - * Retrieve server statistics from Redis cache. - * - * @return array - */ - public function getStatServer(): array - { - /** @var array $stats */ - $stats = []; - $statsServer = $this->redis->zrange($this->statServerKey, 0, -1, true); - - foreach ($statsServer as $member => $value) { - $parts = explode('_', $member); - if (count($parts) !== 3) { - continue; // Skip malformed members - } - [$serverType, $serverId, $type] = $parts; - - if (!in_array($type, ['u', 'd'], true)) { - continue; // Skip invalid types - } - - $key = "{$serverType}_{$serverId}"; - if (!isset($stats[$key])) { - $stats[$key] = [ - 'server_id' => (int) $serverId, - 'server_type' => $serverType, - 'u' => 0.0, - 'd' => 0.0, - ]; - } - $stats[$key][$type] += (float) $value; - } - - return array_values($stats); - } - - /** - * 清除用户报表缓存数据 - */ - public function clearStatUser() - { - $this->redis->del($this->statUserKey); - } - - /** - * 清除服务器报表缓存数据 - */ - public function clearStatServer() - { - $this->redis->del($this->statServerKey); - } - - public function getStatRecord($type) - { - switch ($type) { - case "paid_total": { - return Stat::select([ - '*', - DB::raw('paid_total / 100 as paid_total') - ]) - ->where('record_at', '>=', $this->startAt) - ->where('record_at', '<', $this->endAt) - ->orderBy('record_at', 'ASC') - ->get(); - } - case "commission_total": { - return Stat::select([ - '*', - DB::raw('commission_total / 100 as commission_total') - ]) - ->where('record_at', '>=', $this->startAt) - ->where('record_at', '<', $this->endAt) - ->orderBy('record_at', 'ASC') - ->get(); - } - case "register_count": { - return Stat::where('record_at', '>=', $this->startAt) - ->where('record_at', '<', $this->endAt) - ->orderBy('record_at', 'ASC') - ->get(); - } - } - } - - public function getRanking($type, $limit = 20) - { - switch ($type) { - case 'server_traffic_rank': { - return $this->buildServerTrafficRank($limit); - } - case 'user_consumption_rank': { - return $this->buildUserConsumptionRank($limit); - } - case 'invite_rank': { - return $this->buildInviteRank($limit); - } - } - } - - /** - * 获取指定日期范围内的节点流量排行 - * @param mixed ...$times 可选值:'today', 'tomorrow', 'last_week',或指定日期范围,格式:timestamp - * @return array - */ - - public static function getServerRank(...$times) - { - $startAt = 0; - $endAt = Carbon::tomorrow()->endOfDay()->timestamp; - - if (count($times) == 1) { - switch ($times[0]) { - case 'today': - $startAt = Carbon::today()->startOfDay()->timestamp; - $endAt = Carbon::today()->endOfDay()->timestamp; - break; - case 'yesterday': - $startAt = Carbon::yesterday()->startOfDay()->timestamp; - $endAt = Carbon::yesterday()->endOfDay()->timestamp; - break; - case 'last_week': - $startAt = Carbon::now()->subWeek()->startOfWeek()->timestamp; - $endAt = Carbon::now()->endOfDay()->timestamp; - break; - } - } else if (count($times) == 2) { - $startAt = $times[0]; - $endAt = $times[1]; - } - - $statistics = Server::whereHas( - 'stats', - function ($query) use ($startAt, $endAt) { - $query->where('record_at', '>=', $startAt) - ->where('record_at', '<', $endAt) - ->where('record_type', 'd'); - } - ) - ->withSum('stats as u', 'u') // 预加载 u 的总和 - ->withSum('stats as d', 'd') // 预加载 d 的总和 - ->get() - ->map(function ($item) { - return [ - 'server_name' => optional($item->parent)->name ?? $item->name, - 'server_id' => $item->id, - 'server_type' => $item->type, - 'u' => (int) $item->u, - 'd' => (int) $item->d, - 'total' => (int) $item->u + (int) $item->d, - ]; - }) - ->sortByDesc('total') - ->values() - ->toArray(); - return $statistics; - } - - private function buildInviteRank($limit) - { - $stats = User::select([ - 'invite_user_id', - DB::raw('count(*) as count') - ]) - ->where('created_at', '>=', $this->startAt) - ->where('created_at', '<', $this->endAt) - ->whereNotNull('invite_user_id') - ->groupBy('invite_user_id') - ->orderBy('count', 'DESC') - ->limit($limit) - ->get(); - - $users = User::whereIn('id', $stats->pluck('invite_user_id')->toArray())->get()->keyBy('id'); - foreach ($stats as $k => $v) { - if (!isset($users[$v['invite_user_id']])) - continue; - $stats[$k]['email'] = $users[$v['invite_user_id']]['email']; - } - return $stats; - } - - private function buildUserConsumptionRank($limit) - { - $stats = StatUser::select([ - 'user_id', - DB::raw('sum(u) as u'), - DB::raw('sum(d) as d'), - DB::raw('sum(u) + sum(d) as total') - ]) - ->where('record_at', '>=', $this->startAt) - ->where('record_at', '<', $this->endAt) - ->groupBy('user_id') - ->orderBy('total', 'DESC') - ->limit($limit) - ->get(); - $users = User::whereIn('id', $stats->pluck('user_id')->toArray())->get()->keyBy('id'); - foreach ($stats as $k => $v) { - if (!isset($users[$v['user_id']])) - continue; - $stats[$k]['email'] = $users[$v['user_id']]['email']; - } - return $stats; - } - - private function buildServerTrafficRank($limit) - { - return StatServer::select([ - 'server_id', - 'server_type', - DB::raw('sum(u) as u'), - DB::raw('sum(d) as d'), - DB::raw('sum(u) + sum(d) as total') - ]) - ->where('record_at', '>=', $this->startAt) - ->where('record_at', '<', $this->endAt) - ->groupBy('server_id', 'server_type') - ->orderBy('total', 'DESC') - ->limit($limit) - ->get(); - } -} diff --git a/Xboard/app/Services/TelegramService.php b/Xboard/app/Services/TelegramService.php deleted file mode 100644 index 83f52c7..0000000 --- a/Xboard/app/Services/TelegramService.php +++ /dev/null @@ -1,160 +0,0 @@ -apiUrl = "https://api.telegram.org/bot{$botToken}/"; - - $this->http = Http::timeout(30) - ->retry(3, 1000) - ->withHeaders([ - 'Accept' => 'application/json', - ]); - } - - public function sendMessage(int $chatId, string $text, string $parseMode = ''): void - { - $text = $parseMode === 'markdown' ? str_replace('_', '\_', $text) : $text; - - $this->request('sendMessage', [ - 'chat_id' => $chatId, - 'text' => $text, - 'parse_mode' => $parseMode ?: null, - ]); - } - - public function approveChatJoinRequest(int $chatId, int $userId): void - { - $this->request('approveChatJoinRequest', [ - 'chat_id' => $chatId, - 'user_id' => $userId, - ]); - } - - public function declineChatJoinRequest(int $chatId, int $userId): void - { - $this->request('declineChatJoinRequest', [ - 'chat_id' => $chatId, - 'user_id' => $userId, - ]); - } - - public function getMe(): object - { - return $this->request('getMe'); - } - - public function setWebhook(string $url): object - { - $result = $this->request('setWebhook', ['url' => $url]); - return $result; - } - - /** - * 注册 Bot 命令列表 - */ - public function registerBotCommands(): void - { - try { - $commands = HookManager::filter('telegram.bot.commands', []); - - if (empty($commands)) { - Log::warning('没有找到任何 Telegram Bot 命令'); - return; - } - - $this->request('setMyCommands', [ - 'commands' => json_encode($commands), - 'scope' => json_encode(['type' => 'default']) - ]); - - Log::info('Telegram Bot 命令注册成功', [ - 'commands_count' => count($commands), - 'commands' => $commands - ]); - - } catch (\Exception $e) { - Log::error('Telegram Bot 命令注册失败', [ - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString() - ]); - } - } - - /** - * 获取当前注册的命令列表 - */ - public function getMyCommands(): object - { - return $this->request('getMyCommands'); - } - - /** - * 删除所有命令 - */ - public function deleteMyCommands(): object - { - return $this->request('deleteMyCommands'); - } - - public function sendMessageWithAdmin(string $message, bool $isStaff = false): void - { - $query = User::where('telegram_id', '!=', null); - $query->where( - fn($q) => $q->where('is_admin', 1) - ->when($isStaff, fn($q) => $q->orWhere('is_staff', 1)) - ); - $users = $query->get(); - foreach ($users as $user) { - SendTelegramJob::dispatch($user->telegram_id, $message); - } - } - - protected function request(string $method, array $params = []): object - { - try { - $response = $this->http->get($this->apiUrl . $method, $params); - - if (!$response->successful()) { - throw new ApiException("HTTP 请求失败: {$response->status()}"); - } - - $data = $response->object(); - - if (!isset($data->ok)) { - throw new ApiException('无效的 Telegram API 响应'); - } - - if (!$data->ok) { - $description = $data->description ?? '未知错误'; - throw new ApiException("Telegram API 错误: {$description}"); - } - - return $data; - - } catch (\Exception $e) { - Log::error('Telegram API 请求失败', [ - 'method' => $method, - 'params' => $params, - 'error' => $e->getMessage(), - ]); - - throw new ApiException("Telegram 服务错误: {$e->getMessage()}"); - } - } -} diff --git a/Xboard/app/Services/ThemeService.php b/Xboard/app/Services/ThemeService.php deleted file mode 100644 index 232682d..0000000 --- a/Xboard/app/Services/ThemeService.php +++ /dev/null @@ -1,424 +0,0 @@ -registerThemeViewPaths(); - } - - /** - * Register theme view paths - */ - private function registerThemeViewPaths(): void - { - $systemPath = base_path(self::SYSTEM_THEME_DIR); - if (File::exists($systemPath)) { - View::addNamespace('theme', $systemPath); - } - - $userPath = base_path(self::USER_THEME_DIR); - if (File::exists($userPath)) { - View::prependNamespace('theme', $userPath); - } - } - - /** - * Get theme view path - */ - public function getThemeViewPath(string $theme): ?string - { - $themePath = $this->getThemePath($theme); - if (!$themePath) { - return null; - } - return $themePath . '/dashboard.blade.php'; - } - - /** - * Get all available themes - */ - public function getList(): array - { - $themes = []; - - // 获取系统主题 - $systemPath = base_path(self::SYSTEM_THEME_DIR); - if (File::exists($systemPath)) { - $themes = $this->getThemesFromPath($systemPath, false); - } - - // 获取用户主题 - $userPath = base_path(self::USER_THEME_DIR); - if (File::exists($userPath)) { - $themes = array_merge($themes, $this->getThemesFromPath($userPath, true)); - } - - return $themes; - } - - /** - * Get themes from specified path - */ - private function getThemesFromPath(string $path, bool $canDelete): array - { - return collect(File::directories($path)) - ->mapWithKeys(function ($dir) use ($canDelete) { - $name = basename($dir); - if ( - !File::exists($dir . '/' . self::CONFIG_FILE) || - !File::exists($dir . '/dashboard.blade.php') - ) { - return []; - } - $config = $this->readConfigFile($name); - if (!$config) { - return []; - } - - $config['can_delete'] = $canDelete && $name !== admin_setting('current_theme'); - $config['is_system'] = !$canDelete; - return [$name => $config]; - })->toArray(); - } - - /** - * Upload new theme - */ - public function upload(UploadedFile $file): bool - { - $zip = new ZipArchive; - $tmpPath = storage_path('tmp/' . uniqid()); - - try { - if ($zip->open($file->path()) !== true) { - throw new Exception('Invalid theme package'); - } - - $configEntry = collect(range(0, $zip->numFiles - 1)) - ->map(fn($i) => $zip->getNameIndex($i)) - ->first(fn($name) => basename($name) === self::CONFIG_FILE); - - if (!$configEntry) { - throw new Exception('Theme config file not found'); - } - - $zip->extractTo($tmpPath); - $zip->close(); - - $sourcePath = $tmpPath . '/' . rtrim(dirname($configEntry), '.'); - $configFile = $sourcePath . '/' . self::CONFIG_FILE; - - if (!File::exists($configFile)) { - throw new Exception('Theme config file not found'); - } - - $config = json_decode(File::get($configFile), true); - if (empty($config['name'])) { - throw new Exception('Theme name not configured'); - } - - if (in_array($config['name'], self::SYSTEM_THEMES)) { - throw new Exception('Cannot upload theme with same name as system theme'); - } - - if (!File::exists($sourcePath . '/dashboard.blade.php')) { - throw new Exception('Missing required theme file: dashboard.blade.php'); - } - - $userThemePath = base_path(self::USER_THEME_DIR); - if (!File::exists($userThemePath)) { - File::makeDirectory($userThemePath, 0755, true); - } - - $targetPath = $userThemePath . $config['name']; - if (File::exists($targetPath)) { - $oldConfigFile = $targetPath . '/config.json'; - if (!File::exists($oldConfigFile)) { - throw new Exception('Existing theme missing config file'); - } - $oldConfig = json_decode(File::get($oldConfigFile), true); - $oldVersion = $oldConfig['version'] ?? '0.0.0'; - $newVersion = $config['version'] ?? '0.0.0'; - if (version_compare($newVersion, $oldVersion, '>')) { - $this->cleanupThemeFiles($config['name']); - File::deleteDirectory($targetPath); - File::copyDirectory($sourcePath, $targetPath); - // 更新主题时保留用户配置 - $this->initConfig($config['name'], true); - return true; - } else { - throw new Exception('Theme exists and not a newer version'); - } - } - - File::copyDirectory($sourcePath, $targetPath); - $this->initConfig($config['name']); - - return true; - - } catch (Exception $e) { - throw $e; - } finally { - if (File::exists($tmpPath)) { - File::deleteDirectory($tmpPath); - } - } - } - - /** - * Switch theme - */ - public function switch(string|null $theme): bool - { - if ($theme === null) { - return true; - } - - $currentTheme = admin_setting('current_theme'); - - try { - $themePath = $this->getThemePath($theme); - if (!$themePath) { - throw new Exception('Theme not found'); - } - - if (!File::exists($this->getThemeViewPath($theme))) { - throw new Exception('Theme view file not found'); - } - - if ($currentTheme && $currentTheme !== $theme) { - $this->cleanupThemeFiles($currentTheme); - } - - $targetPath = public_path('theme/' . $theme); - if (!File::copyDirectory($themePath, $targetPath)) { - throw new Exception('Failed to copy theme files'); - } - - admin_setting(['current_theme' => $theme]); - return true; - - } catch (Exception $e) { - Log::error('Theme switch failed', ['theme' => $theme, 'error' => $e->getMessage()]); - throw $e; - } - } - - /** - * Delete theme - */ - public function delete(string $theme): bool - { - try { - if (in_array($theme, self::SYSTEM_THEMES)) { - throw new Exception('System theme cannot be deleted'); - } - - if ($theme === admin_setting('current_theme')) { - throw new Exception('Current theme cannot be deleted'); - } - - $themePath = base_path(self::USER_THEME_DIR . $theme); - if (!File::exists($themePath)) { - throw new Exception('Theme not found'); - } - - $this->cleanupThemeFiles($theme); - File::deleteDirectory($themePath); - admin_setting([self::SETTING_PREFIX . $theme => null]); - return true; - - } catch (Exception $e) { - Log::error('Theme deletion failed', ['theme' => $theme, 'error' => $e->getMessage()]); - throw $e; - } - } - - /** - * Check if theme exists - */ - public function exists(string $theme): bool - { - return $this->getThemePath($theme) !== null; - } - - /** - * Get theme path - */ - public function getThemePath(string $theme): ?string - { - $systemPath = base_path(self::SYSTEM_THEME_DIR . $theme); - if (File::exists($systemPath)) { - return $systemPath; - } - - $userPath = base_path(self::USER_THEME_DIR . $theme); - if (File::exists($userPath)) { - return $userPath; - } - - return null; - } - - /** - * Get theme config - */ - public function getConfig(string $theme): ?array - { - $config = admin_setting(self::SETTING_PREFIX . $theme); - if ($config === null) { - $this->initConfig($theme); - $config = admin_setting(self::SETTING_PREFIX . $theme); - } - return $config; - } - - /** - * Update theme config - */ - public function updateConfig(string $theme, array $config): bool - { - try { - if (!$this->getThemePath($theme)) { - throw new Exception('Theme not found'); - } - - $schema = $this->readConfigFile($theme); - if (!$schema) { - throw new Exception('Invalid theme config file'); - } - - $validFields = collect($schema['configs'] ?? [])->pluck('field_name')->toArray(); - $validConfig = collect($config) - ->only($validFields) - ->toArray(); - - $currentConfig = $this->getConfig($theme) ?? []; - $newConfig = array_merge($currentConfig, $validConfig); - - admin_setting([self::SETTING_PREFIX . $theme => $newConfig]); - return true; - - } catch (Exception $e) { - Log::error('Config update failed', ['theme' => $theme, 'error' => $e->getMessage()]); - throw $e; - } - } - - /** - * Read theme config file - */ - private function readConfigFile(string $theme): ?array - { - $themePath = $this->getThemePath($theme); - if (!$themePath) { - return null; - } - - $file = $themePath . '/' . self::CONFIG_FILE; - return File::exists($file) ? json_decode(File::get($file), true) : null; - } - - /** - * Clean up theme files including public directory - */ - public function cleanupThemeFiles(string $theme): void - { - try { - $publicThemePath = public_path('theme/' . $theme); - if (File::exists($publicThemePath)) { - File::deleteDirectory($publicThemePath); - Log::info('Cleaned up public theme files', ['theme' => $theme, 'path' => $publicThemePath]); - } - - $cacheKey = "theme_{$theme}_assets"; - if (cache()->has($cacheKey)) { - cache()->forget($cacheKey); - Log::info('Cleaned up theme cache', ['theme' => $theme, 'cache_key' => $cacheKey]); - } - - } catch (Exception $e) { - Log::warning('Failed to cleanup theme files', [ - 'theme' => $theme, - 'error' => $e->getMessage() - ]); - } - } - - /** - * Force refresh current theme public files - */ - public function refreshCurrentTheme(): bool - { - try { - $currentTheme = admin_setting('current_theme'); - if (!$currentTheme) { - return false; - } - - $this->cleanupThemeFiles($currentTheme); - - $themePath = $this->getThemePath($currentTheme); - if (!$themePath) { - throw new Exception('Current theme path not found'); - } - - $targetPath = public_path('theme/' . $currentTheme); - if (!File::copyDirectory($themePath, $targetPath)) { - throw new Exception('Failed to copy theme files'); - } - - Log::info('Refreshed current theme files', ['theme' => $currentTheme]); - return true; - - } catch (Exception $e) { - Log::error('Failed to refresh current theme', [ - 'theme' => $currentTheme, - 'error' => $e->getMessage() - ]); - return false; - } - } - - /** - * Initialize theme config - * - * @param string $theme 主题名称 - * @param bool $preserveExisting 是否保留现有配置(更新主题时使用) - */ - private function initConfig(string $theme, bool $preserveExisting = false): void - { - $config = $this->readConfigFile($theme); - if (!$config) { - return; - } - - $defaults = collect($config['configs'] ?? []) - ->mapWithKeys(fn($col) => [$col['field_name'] => $col['default_value'] ?? '']) - ->toArray(); - - if ($preserveExisting) { - $existingConfig = admin_setting(self::SETTING_PREFIX . $theme) ?? []; - $mergedConfig = array_merge($defaults, $existingConfig); - admin_setting([self::SETTING_PREFIX . $theme => $mergedConfig]); - } else { - admin_setting([self::SETTING_PREFIX . $theme => $defaults]); - } - } -} diff --git a/Xboard/app/Services/TicketService.php b/Xboard/app/Services/TicketService.php deleted file mode 100644 index f99d7c7..0000000 --- a/Xboard/app/Services/TicketService.php +++ /dev/null @@ -1,125 +0,0 @@ - $userId, - 'ticket_id' => $ticket->id, - 'message' => $message - ]); - if ($userId !== $ticket->user_id) { - $ticket->reply_status = Ticket::STATUS_OPENING; - } else { - $ticket->reply_status = Ticket::STATUS_CLOSED; - } - if (!$ticketMessage || !$ticket->save()) { - throw new \Exception(); - } - DB::commit(); - return $ticketMessage; - } catch (\Exception $e) { - DB::rollback(); - return false; - } - } - - public function replyByAdmin($ticketId, $message, $userId): void - { - $ticket = Ticket::where('id', $ticketId) - ->first(); - if (!$ticket) { - throw new ApiException('工单不存在'); - } - $ticket->status = Ticket::STATUS_OPENING; - try { - DB::beginTransaction(); - $ticketMessage = TicketMessage::create([ - 'user_id' => $userId, - 'ticket_id' => $ticket->id, - 'message' => $message - ]); - if ($userId !== $ticket->user_id) { - $ticket->reply_status = Ticket::STATUS_OPENING; - } else { - $ticket->reply_status = Ticket::STATUS_CLOSED; - } - if (!$ticketMessage || !$ticket->save()) { - throw new ApiException('工单回复失败'); - } - DB::commit(); - HookManager::call('ticket.reply.admin.after', [$ticket, $ticketMessage]); - } catch (\Exception $e) { - DB::rollBack(); - throw $e; - } - $this->sendEmailNotify($ticket, $ticketMessage); - } - - public function createTicket($userId, $subject, $level, $message) - { - try { - DB::beginTransaction(); - if (Ticket::where('status', 0)->where('user_id', $userId)->lockForUpdate()->first()) { - DB::rollBack(); - throw new ApiException('存在未关闭的工单'); - } - $ticket = Ticket::create([ - 'user_id' => $userId, - 'subject' => $subject, - 'level' => $level - ]); - if (!$ticket) { - throw new ApiException('工单创建失败'); - } - $ticketMessage = TicketMessage::create([ - 'user_id' => $userId, - 'ticket_id' => $ticket->id, - 'message' => $message - ]); - if (!$ticketMessage) { - DB::rollBack(); - throw new ApiException('工单消息创建失败'); - } - DB::commit(); - return $ticket; - } catch (\Exception $e) { - DB::rollBack(); - throw $e; - } - } - - // 半小时内不再重复通知 - private function sendEmailNotify(Ticket $ticket, TicketMessage $ticketMessage) - { - $user = User::find($ticket->user_id); - $cacheKey = 'ticket_sendEmailNotify_' . $ticket->user_id; - if (!Cache::get($cacheKey)) { - Cache::put($cacheKey, 1, 1800); - SendEmailJob::dispatch([ - 'email' => $user->email, - 'subject' => '您在' . admin_setting('app_name', 'XBoard') . '的工单得到了回复', - 'template_name' => 'notify', - 'template_value' => [ - 'name' => admin_setting('app_name', 'XBoard'), - 'url' => admin_setting('app_url'), - 'content' => "主题:{$ticket->subject}\r\n回复内容:{$ticketMessage->message}" - ] - ]); - } - } -} diff --git a/Xboard/app/Services/TrafficResetService.php b/Xboard/app/Services/TrafficResetService.php deleted file mode 100644 index 256fd69..0000000 --- a/Xboard/app/Services/TrafficResetService.php +++ /dev/null @@ -1,415 +0,0 @@ -shouldResetTraffic()) { - return false; - } - - return $this->performReset($user, $triggerSource); - } - - /** - * Perform the traffic reset for a user. - */ - public function performReset(User $user, string $triggerSource = TrafficResetLog::SOURCE_MANUAL): bool - { - try { - return DB::transaction(function () use ($user, $triggerSource) { - $oldUpload = $user->u ?? 0; - $oldDownload = $user->d ?? 0; - $oldTotal = $oldUpload + $oldDownload; - - $nextResetTime = $this->calculateNextResetTime($user); - - $user->update([ - 'u' => 0, - 'd' => 0, - 'last_reset_at' => time(), - 'reset_count' => $user->reset_count + 1, - 'next_reset_at' => $nextResetTime ? $nextResetTime->timestamp : null, - ]); - - $this->recordResetLog($user, [ - 'reset_type' => $this->getResetTypeFromPlan($user->plan), - 'trigger_source' => $triggerSource, - 'old_upload' => $oldUpload, - 'old_download' => $oldDownload, - 'old_total' => $oldTotal, - 'new_upload' => 0, - 'new_download' => 0, - 'new_total' => 0, - ]); - - $this->clearUserCache($user); - HookManager::call('traffic.reset.after', $user); - return true; - }); - } catch (\Exception $e) { - Log::error(__('traffic_reset.reset_failed'), [ - 'user_id' => $user->id, - 'email' => $user->email, - 'error' => $e->getMessage(), - 'trigger_source' => $triggerSource, - ]); - - return false; - } - } - - /** - * Calculate the next traffic reset time for a user. - */ - public function calculateNextResetTime(User $user): ?Carbon - { - if ( - !$user->plan - || $user->plan->reset_traffic_method === Plan::RESET_TRAFFIC_NEVER - || ($user->plan->reset_traffic_method === Plan::RESET_TRAFFIC_FOLLOW_SYSTEM - && (int) admin_setting('reset_traffic_method', Plan::RESET_TRAFFIC_MONTHLY) === Plan::RESET_TRAFFIC_NEVER) - || $user->expired_at === NULL - ) { - return null; - } - - $resetMethod = $user->plan->reset_traffic_method; - - if ($resetMethod === Plan::RESET_TRAFFIC_FOLLOW_SYSTEM) { - $resetMethod = (int) admin_setting('reset_traffic_method', Plan::RESET_TRAFFIC_MONTHLY); - } - - $now = Carbon::now(config('app.timezone')); - - return match ($resetMethod) { - Plan::RESET_TRAFFIC_FIRST_DAY_MONTH => $this->getNextMonthFirstDay($now), - Plan::RESET_TRAFFIC_MONTHLY => $this->getNextMonthlyReset($user, $now), - Plan::RESET_TRAFFIC_FIRST_DAY_YEAR => $this->getNextYearFirstDay($now), - Plan::RESET_TRAFFIC_YEARLY => $this->getNextYearlyReset($user, $now), - default => null, - }; - } - - /** - * Get the first day of the next month. - */ - private function getNextMonthFirstDay(Carbon $from): Carbon - { - return $from->copy()->addMonth()->startOfMonth(); - } - - /** - * Get the next monthly reset time based on the user's expiration date. - * - * Logic: - * 1. If the user has no expiration date, reset on the 1st of each month. - * 2. If the user has an expiration date, use the day of that date as the monthly reset day. - * 3. Prioritize the reset day in the current month if it has not passed yet. - * 4. Handle cases where the day does not exist in a month (e.g., 31st in February). - */ - private function getNextMonthlyReset(User $user, Carbon $from): Carbon - { - $expiredAt = Carbon::createFromTimestamp($user->expired_at, config('app.timezone')); - $resetDay = $expiredAt->day; - $resetTime = [$expiredAt->hour, $expiredAt->minute, $expiredAt->second]; - - $currentMonthTarget = $from->copy()->day($resetDay)->setTime(...$resetTime); - if ($currentMonthTarget->timestamp > $from->timestamp) { - return $currentMonthTarget; - } - - $nextMonthTarget = $from->copy()->startOfMonth()->addMonths(1)->day($resetDay)->setTime(...$resetTime); - - if ($nextMonthTarget->month !== ($from->month % 12) + 1) { - $nextMonth = ($from->month % 12) + 1; - $nextYear = $from->year + ($from->month === 12 ? 1 : 0); - $lastDayOfNextMonth = Carbon::create($nextYear, $nextMonth, 1)->endOfMonth()->day; - $targetDay = min($resetDay, $lastDayOfNextMonth); - $nextMonthTarget = Carbon::create($nextYear, $nextMonth, $targetDay)->setTime(...$resetTime); - } - - return $nextMonthTarget; - } - - /** - * Get the first day of the next year. - */ - private function getNextYearFirstDay(Carbon $from): Carbon - { - return $from->copy()->addYear()->startOfYear(); - } - - /** - * Get the next yearly reset time based on the user's expiration date. - * - * Logic: - * 1. If the user has no expiration date, reset on January 1st of each year. - * 2. If the user has an expiration date, use the month and day of that date as the yearly reset date. - * 3. Prioritize the reset date in the current year if it has not passed yet. - * 4. Handle the case of February 29th in a leap year. - */ - private function getNextYearlyReset(User $user, Carbon $from): Carbon - { - $expiredAt = Carbon::createFromTimestamp($user->expired_at, config('app.timezone')); - $resetMonth = $expiredAt->month; - $resetDay = $expiredAt->day; - $resetTime = [$expiredAt->hour, $expiredAt->minute, $expiredAt->second]; - - $currentYearTarget = $from->copy()->month($resetMonth)->day($resetDay)->setTime(...$resetTime); - if ($currentYearTarget->timestamp > $from->timestamp) { - return $currentYearTarget; - } - - $nextYearTarget = $from->copy()->startOfYear()->addYears(1)->month($resetMonth)->day($resetDay)->setTime(...$resetTime); - - if ($nextYearTarget->month !== $resetMonth) { - $nextYear = $from->year + 1; - $lastDayOfMonth = Carbon::create($nextYear, $resetMonth, 1)->endOfMonth()->day; - $targetDay = min($resetDay, $lastDayOfMonth); - $nextYearTarget = Carbon::create($nextYear, $resetMonth, $targetDay)->setTime(...$resetTime); - } - - return $nextYearTarget; - } - - - /** - * Record the traffic reset log. - */ - private function recordResetLog(User $user, array $data): void - { - TrafficResetLog::create([ - 'user_id' => $user->id, - 'reset_type' => $data['reset_type'], - 'reset_time' => now(), - 'old_upload' => $data['old_upload'], - 'old_download' => $data['old_download'], - 'old_total' => $data['old_total'], - 'new_upload' => $data['new_upload'], - 'new_download' => $data['new_download'], - 'new_total' => $data['new_total'], - 'trigger_source' => $data['trigger_source'], - 'metadata' => $data['metadata'] ?? null, - ]); - } - - /** - * Get the reset type from the user's plan. - */ - private function getResetTypeFromPlan(?Plan $plan): string - { - if (!$plan) { - return TrafficResetLog::TYPE_MANUAL; - } - - $resetMethod = $plan->reset_traffic_method; - - if ($resetMethod === Plan::RESET_TRAFFIC_FOLLOW_SYSTEM) { - $resetMethod = (int) admin_setting('reset_traffic_method', Plan::RESET_TRAFFIC_MONTHLY); - } - - return match ($resetMethod) { - Plan::RESET_TRAFFIC_FIRST_DAY_MONTH => TrafficResetLog::TYPE_FIRST_DAY_MONTH, - Plan::RESET_TRAFFIC_MONTHLY => TrafficResetLog::TYPE_MONTHLY, - Plan::RESET_TRAFFIC_FIRST_DAY_YEAR => TrafficResetLog::TYPE_FIRST_DAY_YEAR, - Plan::RESET_TRAFFIC_YEARLY => TrafficResetLog::TYPE_YEARLY, - Plan::RESET_TRAFFIC_NEVER => TrafficResetLog::TYPE_MANUAL, - default => TrafficResetLog::TYPE_MANUAL, - }; - } - - /** - * Clear user-related cache. - */ - private function clearUserCache(User $user): void - { - $cacheKeys = [ - "user_traffic_{$user->id}", - "user_reset_status_{$user->id}", - "user_subscription_{$user->token}", - ]; - - foreach ($cacheKeys as $key) { - Cache::forget($key); - } - } - - /** - * Batch check and reset users. Processes all eligible users in batches. - */ - public function batchCheckReset(int $batchSize = 100, ?callable $progressCallback = null): array - { - $startTime = microtime(true); - $totalResetCount = 0; - $totalProcessedCount = 0; - $batchNumber = 1; - $errors = []; - $lastProcessedId = 0; - - try { - do { - $users = User::where('next_reset_at', '<=', time()) - ->whereNotNull('next_reset_at') - ->where('id', '>', $lastProcessedId) - ->where(function ($query) { - $query->where('expired_at', '>', time()) - ->orWhereNull('expired_at'); - }) - ->where('banned', 0) - ->whereNotNull('plan_id') - ->orderBy('id') - ->limit($batchSize) - ->get(); - - if ($users->isEmpty()) { - break; - } - - $batchResetCount = 0; - - if ($progressCallback) { - $progressCallback([ - 'batch_number' => $batchNumber, - 'batch_size' => $users->count(), - 'total_processed' => $totalProcessedCount, - ]); - } - - foreach ($users as $user) { - try { - if ($this->checkAndReset($user, TrafficResetLog::SOURCE_CRON)) { - $batchResetCount++; - $totalResetCount++; - } - $totalProcessedCount++; - $lastProcessedId = $user->id; - } catch (\Exception $e) { - $error = [ - 'user_id' => $user->id, - 'email' => $user->email, - 'error' => $e->getMessage(), - 'batch' => $batchNumber, - 'timestamp' => now()->toDateTimeString(), - ]; - $batchErrors[] = $error; - $errors[] = $error; - - Log::error('User traffic reset failed', $error); - - $totalProcessedCount++; - $lastProcessedId = $user->id; - } - } - - $batchNumber++; - - if ($batchNumber % 10 === 0) { - gc_collect_cycles(); - } - - if ($batchNumber % 5 === 0) { - usleep(100000); - } - - } while (true); - - } catch (\Exception $e) { - Log::error('Batch traffic reset task failed with an exception', [ - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - 'total_processed' => $totalProcessedCount, - 'total_reset' => $totalResetCount, - 'last_processed_id' => $lastProcessedId, - ]); - - $errors[] = [ - 'type' => 'system_error', - 'error' => $e->getMessage(), - 'batch' => $batchNumber, - 'last_processed_id' => $lastProcessedId, - 'timestamp' => now()->toDateTimeString(), - ]; - } - - $totalDuration = round(microtime(true) - $startTime, 2); - - $result = [ - 'total_processed' => $totalProcessedCount, - 'total_reset' => $totalResetCount, - 'total_batches' => $batchNumber - 1, - 'error_count' => count($errors), - 'errors' => $errors, - 'duration' => $totalDuration, - 'batch_size' => $batchSize, - 'last_processed_id' => $lastProcessedId, - 'completed_at' => now()->toDateTimeString(), - ]; - - return $result; - } - - /** - * Set the initial reset time for a new user. - */ - public function setInitialResetTime(User $user): void - { - if ($user->next_reset_at !== null) { - return; - } - - $nextResetTime = $this->calculateNextResetTime($user); - - if ($nextResetTime) { - $user->update(['next_reset_at' => $nextResetTime->timestamp]); - } - } - - /** - * Get the user's traffic reset history. - */ - public function getUserResetHistory(User $user, int $limit = 10): \Illuminate\Database\Eloquent\Collection - { - return $user->trafficResetLogs() - ->orderBy('reset_time', 'desc') - ->limit($limit) - ->get(); - } - - /** - * Check if the user is eligible for traffic reset. - */ - public function canReset(User $user): bool - { - return $user->isActive() && $user->plan !== null; - } - - /** - * Manually reset a user's traffic (Admin function). - */ - public function manualReset(User $user, array $metadata = []): bool - { - if (!$this->canReset($user)) { - return false; - } - - return $this->performReset($user, TrafficResetLog::SOURCE_MANUAL); - } -} \ No newline at end of file diff --git a/Xboard/app/Services/UpdateService.php b/Xboard/app/Services/UpdateService.php deleted file mode 100644 index de063cd..0000000 --- a/Xboard/app/Services/UpdateService.php +++ /dev/null @@ -1,458 +0,0 @@ -getCurrentCommit(); - }); - return $date . '-' . $hash; - } - - /** - * Update version cache - */ - public function updateVersionCache(): void - { - try { - $result = Process::run('git log -1 --format=%cd:%H --date=format:%Y%m%d'); - if ($result->successful()) { - list($date, $hash) = explode(':', trim($result->output())); - Cache::forever(self::CACHE_VERSION_DATE, $date); - Cache::forever(self::CACHE_VERSION, substr($hash, 0, 7)); - // Log::info('Version cache updated: ' . $date . '-' . substr($hash, 0, 7)); - return; - } - } catch (\Exception $e) { - Log::error('Failed to get version with date: ' . $e->getMessage()); - } - - // Fallback - Cache::forever(self::CACHE_VERSION_DATE, date('Ymd')); - $fallbackHash = $this->getCurrentCommit(); - Cache::forever(self::CACHE_VERSION, $fallbackHash); - Log::info('Version cache updated (fallback): ' . date('Ymd') . '-' . $fallbackHash); - } - - public function checkForUpdates(): array - { - try { - // Get current version commit - $currentCommit = $this->getCurrentCommit(); - if ($currentCommit === 'unknown') { - // If unable to get current commit, try to get the first commit - $currentCommit = $this->getFirstCommit(); - } - // Get local git logs - $localLogs = $this->getLocalGitLogs(); - if (empty($localLogs)) { - Log::error('Failed to get local git logs'); - return $this->getCachedUpdateInfo(); - } - - // Get remote latest commits - $response = Http::withHeaders([ - 'Accept' => 'application/vnd.github.v3+json', - 'User-Agent' => 'XBoard-Update-Checker' - ])->get(self::GITHUB_API_URL . '?per_page=50'); - - if ($response->successful()) { - $commits = $response->json(); - - if (empty($commits) || !is_array($commits)) { - Log::error('Invalid GitHub response format'); - return $this->getCachedUpdateInfo(); - } - - $latestCommit = $this->formatCommitHash($commits[0]['sha']); - $currentIndex = -1; - $updateLogs = []; - - // First, find the current version position in remote commit history - foreach ($commits as $index => $commit) { - $shortSha = $this->formatCommitHash($commit['sha']); - if ($shortSha === $currentCommit) { - $currentIndex = $index; - break; - } - } - - // Check local version status - $isLocalNewer = false; - if ($currentIndex === -1) { - // Current version not found in remote history, check local commits - foreach ($localLogs as $localCommit) { - $localHash = $this->formatCommitHash($localCommit['hash']); - // If latest remote commit found, local is not newer - if ($localHash === $latestCommit) { - $isLocalNewer = false; - break; - } - // Record additional local commits - $updateLogs[] = [ - 'version' => $localHash, - 'message' => $localCommit['message'], - 'author' => $localCommit['author'], - 'date' => $localCommit['date'], - 'is_local' => true - ]; - $isLocalNewer = true; - } - } - - // If local is not newer, collect commits that need to be updated - if (!$isLocalNewer && $currentIndex > 0) { - $updateLogs = []; - // Collect all commits between current version and latest version - for ($i = 0; $i < $currentIndex; $i++) { - $commit = $commits[$i]; - $updateLogs[] = [ - 'version' => $this->formatCommitHash($commit['sha']), - 'message' => $commit['commit']['message'], - 'author' => $commit['commit']['author']['name'], - 'date' => $commit['commit']['author']['date'], - 'is_local' => false - ]; - } - } - - $hasUpdate = !$isLocalNewer && $currentIndex > 0; - - $updateInfo = [ - 'has_update' => $hasUpdate, - 'is_local_newer' => $isLocalNewer, - 'latest_version' => $isLocalNewer ? $currentCommit : $latestCommit, - 'current_version' => $currentCommit, - 'update_logs' => $updateLogs, - 'download_url' => $commits[0]['html_url'] ?? '', - 'published_at' => $commits[0]['commit']['author']['date'] ?? '', - 'author' => $commits[0]['commit']['author']['name'] ?? '', - ]; - - // Cache check results - $this->setLastCheckTime(); - Cache::put(self::CACHE_UPDATE_INFO, $updateInfo, now()->addHours(24)); - - return $updateInfo; - } - - return $this->getCachedUpdateInfo(); - } catch (\Exception $e) { - Log::error('Update check failed: ' . $e->getMessage()); - return $this->getCachedUpdateInfo(); - } - } - - public function executeUpdate(): array - { - // Check for new version first - $updateInfo = $this->checkForUpdates(); - if ($updateInfo['is_local_newer']) { - return [ - 'success' => false, - 'message' => __('update.local_newer') - ]; - } - if (!$updateInfo['has_update']) { - return [ - 'success' => false, - 'message' => __('update.already_latest') - ]; - } - - // Check for update lock - if (Cache::get(self::CACHE_UPDATE_LOCK)) { - return [ - 'success' => false, - 'message' => __('update.process_running') - ]; - } - - try { - // Set update lock - Cache::put(self::CACHE_UPDATE_LOCK, true, now()->addMinutes(30)); - - // 1. Backup database - $this->backupDatabase(); - - // 2. Pull latest code - $result = $this->pullLatestCode(); - if (!$result['success']) { - throw new \Exception($result['message']); - } - - // 3. Run database migrations - $this->runMigrations(); - - // 4. Clear cache - $this->clearCache(); - - // 5. Create update flag - $this->createUpdateFlag(); - - // 6. Restart Octane if running - $this->restartOctane(); - - // Remove update lock - Cache::forget(self::CACHE_UPDATE_LOCK); - - // Format update logs - $logMessages = array_map(function($log) { - return sprintf("- %s (%s): %s", - $log['version'], - date('Y-m-d H:i', strtotime($log['date'])), - $log['message'] - ); - }, $updateInfo['update_logs']); - - return [ - 'success' => true, - 'message' => __('update.success', [ - 'from' => $updateInfo['current_version'], - 'to' => $updateInfo['latest_version'] - ]), - 'version' => $updateInfo['latest_version'], - 'update_info' => [ - 'from_version' => $updateInfo['current_version'], - 'to_version' => $updateInfo['latest_version'], - 'update_logs' => $logMessages, - 'author' => $updateInfo['author'], - 'published_at' => $updateInfo['published_at'] - ] - ]; - - } catch (\Exception $e) { - Log::error('Update execution failed: ' . $e->getMessage()); - Cache::forget(self::CACHE_UPDATE_LOCK); - - return [ - 'success' => false, - 'message' => __('update.failed', ['error' => $e->getMessage()]) - ]; - } - } - - protected function getCurrentCommit(): string - { - try { - // Ensure git configuration is correct - Process::run(sprintf('git config --global --add safe.directory %s', base_path())); - $result = Process::run('git rev-parse HEAD'); - $fullHash = trim($result->output()); - return $fullHash ? $this->formatCommitHash($fullHash) : 'unknown'; - } catch (\Exception $e) { - Log::error('Failed to get current commit: ' . $e->getMessage()); - return 'unknown'; - } - } - - protected function getFirstCommit(): string - { - try { - // Get first commit hash - $result = Process::run('git rev-list --max-parents=0 HEAD'); - $fullHash = trim($result->output()); - return $fullHash ? $this->formatCommitHash($fullHash) : 'unknown'; - } catch (\Exception $e) { - Log::error('Failed to get first commit: ' . $e->getMessage()); - return 'unknown'; - } - } - - protected function formatCommitHash(string $hash): string - { - // Use 7 characters for commit hash - return substr($hash, 0, 7); - } - - protected function backupDatabase(): void - { - try { - // Use existing backup command - Process::run('php artisan backup:database'); - - if (!Process::result()->successful()) { - throw new \Exception(__('update.backup_failed', ['error' => Process::result()->errorOutput()])); - } - } catch (\Exception $e) { - Log::error('Database backup failed: ' . $e->getMessage()); - throw $e; - } - } - - protected function pullLatestCode(): array - { - try { - // Get current project root directory - $basePath = base_path(); - - // Ensure git configuration is correct - Process::run(sprintf('git config --global --add safe.directory %s', $basePath)); - - // Pull latest code - Process::run('git fetch origin master'); - Process::run('git reset --hard origin/master'); - - // Update dependencies - Process::run('composer install --no-dev --optimize-autoloader'); - - // Update version cache after pulling new code - $this->updateVersionCache(); - - return ['success' => true]; - } catch (\Exception $e) { - return [ - 'success' => false, - 'message' => __('update.code_update_failed', ['error' => $e->getMessage()]) - ]; - } - } - - protected function runMigrations(): void - { - try { - Process::run('php artisan migrate --force'); - } catch (\Exception $e) { - Log::error('Migration failed: ' . $e->getMessage()); - throw new \Exception(__('update.migration_failed', ['error' => $e->getMessage()])); - } - } - - protected function clearCache(): void - { - try { - $commands = [ - 'php artisan config:clear', - 'php artisan cache:clear', - 'php artisan view:clear', - 'php artisan route:clear' - ]; - - foreach ($commands as $command) { - Process::run($command); - } - } catch (\Exception $e) { - Log::error('Cache clearing failed: ' . $e->getMessage()); - throw new \Exception(__('update.cache_clear_failed', ['error' => $e->getMessage()])); - } - } - - protected function createUpdateFlag(): void - { - try { - // Create update flag file for external script to detect and restart container - $flagFile = storage_path('update_pending'); - File::put($flagFile, date('Y-m-d H:i:s')); - } catch (\Exception $e) { - Log::error('Failed to create update flag: ' . $e->getMessage()); - throw new \Exception(__('update.flag_create_failed', ['error' => $e->getMessage()])); - } - } - - protected function restartOctane(): void - { - try { - if (!config('octane.server')) { - return; - } - - // Check Octane running status - $statusResult = Process::run('php artisan octane:status'); - if (!$statusResult->successful()) { - Log::info('Octane is not running, skipping restart.'); - return; - } - - $output = $statusResult->output(); - if (str_contains($output, 'Octane server is running')) { - Log::info('Restarting Octane server after update...'); - // Update version cache before restart - $this->updateVersionCache(); - Process::run('php artisan octane:stop'); - Log::info('Octane server restarted successfully.'); - } else { - Log::info('Octane is not running, skipping restart.'); - } - } catch (\Exception $e) { - Log::error('Failed to restart Octane server: ' . $e->getMessage()); - // Non-fatal error, don't throw exception - } - } - - public function getLastCheckTime() - { - return Cache::get(self::CACHE_LAST_CHECK, null); - } - - protected function setLastCheckTime(): void - { - Cache::put(self::CACHE_LAST_CHECK, now()->timestamp, now()->addDays(30)); - } - - public function getCachedUpdateInfo(): array - { - return Cache::get(self::CACHE_UPDATE_INFO, [ - 'has_update' => false, - 'latest_version' => $this->getCurrentCommit(), - 'current_version' => $this->getCurrentCommit(), - 'update_logs' => [], - 'download_url' => '', - 'published_at' => '', - 'author' => '', - ]); - } - - protected function getLocalGitLogs(int $limit = 50): array - { - try { - // 获取本地git log - $result = Process::run( - sprintf('git log -%d --pretty=format:"%%H||%%s||%%an||%%ai"', $limit) - ); - - if (!$result->successful()) { - return []; - } - - $logs = []; - $lines = explode("\n", trim($result->output())); - foreach ($lines as $line) { - $parts = explode('||', $line); - if (count($parts) === 4) { - $logs[] = [ - 'hash' => $parts[0], - 'message' => $parts[1], - 'author' => $parts[2], - 'date' => $parts[3] - ]; - } - } - return $logs; - } catch (\Exception $e) { - Log::error('Failed to get local git logs: ' . $e->getMessage()); - return []; - } - } -} \ No newline at end of file diff --git a/Xboard/app/Services/UserService.php b/Xboard/app/Services/UserService.php deleted file mode 100644 index c068b67..0000000 --- a/Xboard/app/Services/UserService.php +++ /dev/null @@ -1,288 +0,0 @@ -calculateNextResetTime($user); - - if (!$nextResetTime) { - return null; - } - - // Calculate the remaining days from now to the next reset time - $now = time(); - $resetTimestamp = $nextResetTime->timestamp; - - if ($resetTimestamp <= $now) { - return 0; // Reset time has passed or is now - } - - // Calculate the difference in days (rounded up) - $daysDifference = ceil(($resetTimestamp - $now) / 86400); - - return (int) $daysDifference; - } - - public function isAvailable(User $user) - { - if (!$user->banned && $user->transfer_enable && ($user->expired_at > time() || $user->expired_at === NULL)) { - return true; - } - return false; - } - - public function getAvailableUsers() - { - return User::whereRaw('u + d < transfer_enable') - ->where(function ($query) { - $query->where('expired_at', '>=', time()) - ->orWhere('expired_at', NULL); - }) - ->where('banned', 0) - ->get(); - } - - public function getUnAvailbaleUsers() - { - return User::where(function ($query) { - $query->where('expired_at', '<', time()) - ->orWhere('expired_at', 0); - }) - ->where(function ($query) { - $query->where('plan_id', NULL) - ->orWhere('transfer_enable', 0); - }) - ->get(); - } - - public function getUsersByIds($ids) - { - return User::whereIn('id', $ids)->get(); - } - - public function getAllUsers() - { - return User::all(); - } - - public function addBalance(int $userId, int $balance): bool - { - $user = User::lockForUpdate()->find($userId); - if (!$user) { - return false; - } - $user->balance = $user->balance + $balance; - if ($user->balance < 0) { - return false; - } - if (!$user->save()) { - return false; - } - return true; - } - - public function isNotCompleteOrderByUserId(int $userId): bool - { - $order = Order::whereIn('status', [0, 1]) - ->where('user_id', $userId) - ->first(); - if (!$order) { - return false; - } - return true; - } - - public function trafficFetch(Server $server, string $protocol, array $data) - { - $server->rate = $server->getCurrentRate(); - $server = $server->toArray(); - - list($server, $protocol, $data) = HookManager::filter('traffic.process.before', [$server, $protocol, $data]); - // Compatible with legacy hook - list($server, $protocol, $data) = HookManager::filter('traffic.before_process', [$server, $protocol, $data]); - - $timestamp = strtotime(date('Y-m-d')); - collect($data)->chunk(1000)->each(function ($chunk) use ($timestamp, $server, $protocol) { - TrafficFetchJob::dispatch($server, $chunk->toArray(), $protocol, $timestamp); - StatUserJob::dispatch($server, $chunk->toArray(), $protocol, 'd'); - StatServerJob::dispatch($server, $chunk->toArray(), $protocol, 'd'); - }); - } - - /** - * 获取用户流量信息(增加重置检查) - */ - public function getUserTrafficInfo(User $user): array - { - // 检查是否需要重置流量 - app(TrafficResetService::class)->checkAndReset($user, TrafficResetLog::SOURCE_USER_ACCESS); - - // 重新获取用户数据(可能已被重置) - $user->refresh(); - - return [ - 'upload' => $user->u ?? 0, - 'download' => $user->d ?? 0, - 'total_used' => $user->getTotalUsedTraffic(), - 'total_available' => $user->transfer_enable ?? 0, - 'remaining' => $user->getRemainingTraffic(), - 'usage_percentage' => $user->getTrafficUsagePercentage(), - 'next_reset_at' => $user->next_reset_at, - 'last_reset_at' => $user->last_reset_at, - 'reset_count' => $user->reset_count, - ]; - } - - /** - * 创建用户 - */ - public function createUser(array $data): User - { - $user = new User(); - - // 基本信息 - $user->email = $data['email']; - $user->password = isset($data['password']) - ? Hash::make($data['password']) - : Hash::make($data['email']); - $user->uuid = Helper::guid(true); - $user->token = Helper::guid(); - - // 默认设置 - $user->remind_expire = admin_setting('default_remind_expire', 1); - $user->remind_traffic = admin_setting('default_remind_traffic', 1); - $user->expired_at = null; - - // 可选字段 - $this->setOptionalFields($user, $data); - - // 处理计划 - if (isset($data['plan_id'])) { - $this->setPlanForUser($user, $data['plan_id'], $data['expired_at'] ?? null); - } else { - $this->setTryOutPlan(user: $user); - } - - return $user; - } - - /** - * 设置可选字段 - */ - private function setOptionalFields(User $user, array $data): void - { - $optionalFields = [ - 'invite_user_id', - 'telegram_id', - 'group_id', - 'speed_limit', - 'expired_at', - 'transfer_enable' - ]; - - foreach ($optionalFields as $field) { - if (array_key_exists($field, $data)) { - $user->{$field} = $data[$field]; - } - } - } - - /** - * 为用户设置计划 - */ - private function setPlanForUser(User $user, int $planId, ?int $expiredAt = null): void - { - $plan = Plan::find($planId); - if (!$plan) - return; - - $user->plan_id = $plan->id; - $user->group_id = $plan->group_id; - $user->transfer_enable = $plan->transfer_enable * 1073741824; - $user->speed_limit = $plan->speed_limit; - - if ($expiredAt) { - $user->expired_at = $expiredAt; - } - } - - /** - * 为用户分配一个新套餐或续费现有套餐 - * - * @param User $user 用户模型 - * @param Plan $plan 套餐模型 - * @param int $validityDays 购买天数 - * @return User 更新后的用户模型 - */ - public function assignPlan(User $user, Plan $plan, int $validityDays): User - { - $user->plan_id = $plan->id; - $user->group_id = $plan->group_id; - $user->transfer_enable = $plan->transfer_enable * 1073741824; - $user->speed_limit = $plan->speed_limit; - $user->device_limit = $plan->device_limit; - - if ($validityDays > 0) { - $user = $this->extendSubscription($user, $validityDays); - } - - $user->save(); - return $user; - } - - /** - * 延长用户的订阅有效期 - * - * @param User $user 用户模型 - * @param int $days 延长天数 - * @return User 更新后的用户模型 - */ - public function extendSubscription(User $user, int $days): User - { - $currentExpired = $user->expired_at ?? time(); - $user->expired_at = max($currentExpired, time()) + ($days * 86400); - - return $user; - } - - /** - * 设置试用计划 - */ - private function setTryOutPlan(User $user): void - { - if (!(int) admin_setting('try_out_plan_id', 0)) - return; - - $plan = Plan::find(admin_setting('try_out_plan_id')); - if (!$plan) - return; - - $user->transfer_enable = $plan->transfer_enable * 1073741824; - $user->plan_id = $plan->id; - $user->group_id = $plan->group_id; - $user->expired_at = time() + (admin_setting('try_out_hour', 1) * 3600); - $user->speed_limit = $plan->speed_limit; - } -} diff --git a/Xboard/app/Support/AbstractProtocol.php b/Xboard/app/Support/AbstractProtocol.php deleted file mode 100644 index 78a2c0d..0000000 --- a/Xboard/app/Support/AbstractProtocol.php +++ /dev/null @@ -1,262 +0,0 @@ -user = $user; - $this->servers = $servers; - $this->clientName = $clientName; - $this->clientVersion = $clientVersion; - $this->userAgent = $userAgent; - $this->protocolRequirements = $this->normalizeProtocolRequirements($this->protocolRequirements); - $this->servers = HookManager::filter('protocol.servers.filtered', $this->filterServersByVersion()); - } - - /** - * 获取协议标识 - * - * @return array - */ - public function getFlags(): array - { - return $this->flags; - } - - /** - * 处理请求 - * - * @return mixed - */ - abstract public function handle(); - - /** - * 根据客户端版本过滤不兼容的服务器 - * - * @return array - */ - protected function filterServersByVersion() - { - $this->filterByAllowedProtocols(); - $hasGlobalConfig = isset($this->protocolRequirements['*']); - $hasClientConfig = isset($this->protocolRequirements[$this->clientName]); - - if ((blank($this->clientName) || blank($this->clientVersion)) && !$hasGlobalConfig) { - return $this->servers; - } - - if (!$hasGlobalConfig && !$hasClientConfig) { - return $this->servers; - } - - return collect($this->servers) - ->filter(fn($server) => $this->isCompatible($server)) - ->values() - ->all(); - } - - /** - * 检查服务器是否与当前客户端兼容 - * - * @param array $server 服务器信息 - * @return bool - */ - protected function isCompatible($server) - { - $serverType = $server['type'] ?? null; - if (isset($this->protocolRequirements['*'][$serverType])) { - $globalRequirements = $this->protocolRequirements['*'][$serverType]; - if (!$this->checkRequirements($globalRequirements, $server)) { - return false; - } - } - - if (!isset($this->protocolRequirements[$this->clientName][$serverType])) { - return true; - } - - $requirements = $this->protocolRequirements[$this->clientName][$serverType]; - return $this->checkRequirements($requirements, $server); - } - - /** - * 检查版本要求 - * - * @param array $requirements 要求配置 - * @param array $server 服务器信息 - * @return bool - */ - private function checkRequirements(array $requirements, array $server): bool - { - foreach ($requirements as $field => $filterRule) { - if (in_array($field, ['base_version', 'incompatible'])) { - continue; - } - - $actualValue = data_get($server, $field); - - if (is_array($filterRule) && isset($filterRule['whitelist'])) { - $allowedValues = $filterRule['whitelist']; - $strict = $filterRule['strict'] ?? false; - if ($strict) { - if ($actualValue === null) { - return false; - } - if (!is_string($actualValue) && !is_int($actualValue)) { - return false; - } - if (!isset($allowedValues[$actualValue])) { - return false; - } - $requiredVersion = $allowedValues[$actualValue]; - if ($requiredVersion !== '0.0.0' && version_compare($this->clientVersion, $requiredVersion, '<')) { - return false; - } - continue; - } - } else { - $allowedValues = $filterRule; - $strict = false; - } - - if ($actualValue === null) { - continue; - } - if (!is_string($actualValue) && !is_int($actualValue)) { - continue; - } - if (!isset($allowedValues[$actualValue])) { - continue; - } - $requiredVersion = $allowedValues[$actualValue]; - if ($requiredVersion !== '0.0.0' && version_compare($this->clientVersion, $requiredVersion, '<')) { - return false; - } - } - - return true; - } - - /** - * 检查当前客户端是否支持特定功能 - * - * @param string $clientName 客户端名称 - * @param string $minVersion 最低版本要求 - * @param array $additionalConditions 额外条件检查 - * @return bool - */ - protected function supportsFeature(string $clientName, string $minVersion, array $additionalConditions = []): bool - { - // 检查客户端名称 - if ($this->clientName !== $clientName) { - return false; - } - - // 检查版本号 - if (empty($this->clientVersion) || version_compare($this->clientVersion, $minVersion, '<')) { - return false; - } - - // 检查额外条件 - foreach ($additionalConditions as $condition) { - if (!$condition) { - return false; - } - } - - return true; - } - - /** - * 根据白名单过滤服务器 - * - * @return void - */ - protected function filterByAllowedProtocols(): void - { - if (!empty($this->allowedProtocols)) { - $this->servers = collect($this->servers) - ->filter(fn($server) => in_array($server['type'], $this->allowedProtocols)) - ->values() - ->all(); - } - } - - /** - * 将平铺的协议需求转换为树形结构 - * - * @param array $flat 平铺的协议需求 - * @return array 树形结构的协议需求 - */ - protected function normalizeProtocolRequirements(array $flat): array - { - $result = []; - foreach ($flat as $key => $value) { - if (!str_contains($key, '.')) { - $result[$key] = $value; - continue; - } - $segments = explode('.', $key, 3); - if (count($segments) < 3) { - $result[$segments[0]][$segments[1] ?? '*'][''] = $value; - continue; - } - [$client, $type, $field] = $segments; - $result[$client][$type][$field] = $value; - } - return $result; - } -} \ No newline at end of file diff --git a/Xboard/app/Support/ProtocolManager.php b/Xboard/app/Support/ProtocolManager.php deleted file mode 100644 index e1fb4cd..0000000 --- a/Xboard/app/Support/ProtocolManager.php +++ /dev/null @@ -1,162 +0,0 @@ -container = $container; - } - - /** - * 发现并注册所有协议类 - * - * @return self - */ - public function registerAllProtocols() - { - if (empty($this->protocolClasses)) { - $files = glob(app_path('Protocols') . '/*.php'); - - foreach ($files as $file) { - $className = 'App\\Protocols\\' . basename($file, '.php'); - - if (class_exists($className) && is_subclass_of($className, AbstractProtocol::class)) { - $this->protocolClasses[] = $className; - } - } - } - - return $this; - } - - /** - * 获取所有注册的协议类 - * - * @return array - */ - public function getProtocolClasses() - { - if (empty($this->protocolClasses)) { - $this->registerAllProtocols(); - } - - return $this->protocolClasses; - } - - /** - * 获取所有协议的标识 - * - * @return array - */ - public function getAllFlags() - { - return collect($this->getProtocolClasses()) - ->map(function ($class) { - try { - $reflection = new \ReflectionClass($class); - if (!$reflection->isInstantiable()) { - return []; - } - // 'flags' is a public property with a default value in AbstractProtocol - $instanceForFlags = $reflection->newInstanceWithoutConstructor(); - return $instanceForFlags->flags; - } catch (\ReflectionException $e) { - // Log or handle error if a class is problematic - report($e); - return []; - } - }) - ->flatten() - ->unique() - ->values() - ->all(); - } - - /** - * 根据标识匹配合适的协议处理器类名 - * - * @param string $flag 请求标识 - * @return string|null 协议类名或null - */ - public function matchProtocolClassName(string $flag): ?string - { - // 按照相反顺序,使最新定义的协议有更高优先级 - foreach (array_reverse($this->getProtocolClasses()) as $protocolClassString) { - try { - $reflection = new \ReflectionClass($protocolClassString); - - if (!$reflection->isInstantiable() || !$reflection->isSubclassOf(AbstractProtocol::class)) { - continue; - } - - // 'flags' is a public property in AbstractProtocol - $instanceForFlags = $reflection->newInstanceWithoutConstructor(); - $flags = $instanceForFlags->flags; - - if (collect($flags)->contains(fn($f) => stripos($flag, (string) $f) !== false)) { - return $protocolClassString; // 返回类名字符串 - } - } catch (\ReflectionException $e) { - report($e); // Consider logging this error - continue; - } - } - return null; - } - - /** - * 根据标识匹配合适的协议处理器实例 (原有逻辑,如果还需要的话) - * - * @param string $flag 请求标识 - * @param array $user 用户信息 - * @param array $servers 服务器列表 - * @param array $clientInfo 客户端信息 - * @return AbstractProtocol|null - */ - public function matchProtocol($flag, $user, $servers, $clientInfo = []) - { - $protocolClassName = $this->matchProtocolClassName($flag); - if ($protocolClassName) { - return $this->makeProtocolInstance($protocolClassName, [ - 'user' => $user, - 'servers' => $servers, - 'clientName' => $clientInfo['name'] ?? null, - 'clientVersion' => $clientInfo['version'] ?? null - ]); - } - return null; - } - - /** - * 创建协议实例的通用方法,兼容不同版本的Laravel容器 - * - * @param string $class 类名 - * @param array $parameters 构造参数 - * @return object 实例 - */ - protected function makeProtocolInstance($class, array $parameters) - { - // Laravel's make method can accept an array of parameters as its second argument. - // These will be used when resolving the class's dependencies. - return $this->container->make($class, $parameters); - } -} \ No newline at end of file diff --git a/Xboard/app/Support/Setting.php b/Xboard/app/Support/Setting.php deleted file mode 100644 index 76adf8f..0000000 --- a/Xboard/app/Support/Setting.php +++ /dev/null @@ -1,140 +0,0 @@ -cache = Cache::store('redis'); - } - - /** - * 获取配置. - */ - public function get(string $key, mixed $default = null): mixed - { - $this->load(); - return Arr::get($this->loadedSettings, strtolower($key), $default); - } - - /** - * 设置配置信息. - */ - public function set(string $key, mixed $value = null): bool - { - SettingModel::createOrUpdate(strtolower($key), $value); - $this->flush(); - return true; - } - - /** - * 保存配置到数据库. - */ - public function save(array $settings): bool - { - foreach ($settings as $key => $value) { - SettingModel::createOrUpdate(strtolower($key), $value); - } - $this->flush(); - return true; - } - - /** - * 删除配置信息 - */ - public function remove(string $key): bool - { - SettingModel::where('name', $key)->delete(); - $this->flush(); - return true; - } - - /** - * 更新单个设置项 - */ - public function update(string $key, $value): bool - { - return $this->set($key, $value); - } - - /** - * 批量获取配置项 - */ - public function getBatch(array $keys): array - { - $this->load(); - $result = []; - - foreach ($keys as $index => $item) { - $isNumericIndex = is_numeric($index); - $key = strtolower($isNumericIndex ? $item : $index); - $default = $isNumericIndex ? config('v2board.' . $item) : (config('v2board.' . $key) ?? $item); - - $result[$item] = Arr::get($this->loadedSettings, $key, $default); - } - - return $result; - } - - /** - * 将所有设置转换为数组 - */ - public function toArray(): array - { - $this->load(); - return $this->loadedSettings; - } - - /** - * 加载配置到请求内缓存 - */ - private function load(): void - { - if ($this->loadedSettings !== null) { - return; - } - - try { - $settings = $this->cache->rememberForever(self::CACHE_KEY, function (): array { - return array_change_key_case( - SettingModel::pluck('value', 'name')->toArray(), - CASE_LOWER - ); - }); - - // 处理JSON格式的值 - foreach ($settings as $key => $value) { - if (is_string($value)) { - $decoded = json_decode($value, true); - if (json_last_error() === JSON_ERROR_NONE) { - $settings[$key] = $decoded; - } - } - } - - $this->loadedSettings = $settings; - } catch (\Throwable) { - $this->loadedSettings = []; - } - } - - /** - * 清空缓存 - */ - private function flush(): void - { - $this->cache->forget(self::CACHE_KEY); - $this->loadedSettings = null; - } -} diff --git a/Xboard/app/Traits/HasPluginConfig.php b/Xboard/app/Traits/HasPluginConfig.php deleted file mode 100644 index a44e2fc..0000000 --- a/Xboard/app/Traits/HasPluginConfig.php +++ /dev/null @@ -1,144 +0,0 @@ -getPluginConfig(); - - if ($key) { - return $config[$key] ?? $default; - } - - return $config; - } - - /** - * 获取完整的插件配置 - */ - protected function getPluginConfig(): array - { - if ($this->pluginConfig === null) { - $pluginCode = $this->getPluginCode(); - - \Log::channel('daily')->info('Telegram Login: 获取插件配置', [ - 'plugin_code' => $pluginCode - ]); - - $this->pluginConfig = Cache::remember( - "plugin_config_{$pluginCode}", - 3600, - function () use ($pluginCode) { - $plugin = Plugin::where('code', $pluginCode) - ->where('is_enabled', true) - ->first(); - - if (!$plugin || !$plugin->config) { - return []; - } - - return json_decode($plugin->config, true) ?? []; - } - ); - } - - return $this->pluginConfig; - } - - /** - * 获取插件代码 - */ - public function getPluginCode(): string - { - if ($this->pluginCode === null) { - $this->pluginCode = $this->autoDetectPluginCode(); - } - - return $this->pluginCode; - } - - /** - * 设置插件代码(如果自动检测不准确可以手动设置) - */ - public function setPluginCode(string $pluginCode): void - { - $this->pluginCode = $pluginCode; - $this->pluginConfig = null; // 重置配置缓存 - $this->pluginEnabled = null; - } - - /** - * 自动检测插件代码 - */ - protected function autoDetectPluginCode(): string - { - $reflection = new \ReflectionClass($this); - $namespace = $reflection->getNamespaceName(); - - // 从命名空间提取插件代码 - // 例如: Plugin\TelegramLogin\Controllers => telegram_login - if (preg_match('/^Plugin\\\\(.+?)\\\\/', $namespace, $matches)) { - return $this->convertToKebabCase($matches[1]); - } - - throw new \RuntimeException('Unable to detect plugin code from namespace: ' . $namespace); - } - - /** - * 将 StudlyCase 转换为 kebab-case - */ - protected function convertToKebabCase(string $string): string - { - return strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $string)); - } - - /** - * 检查插件是否启用 - */ - public function isPluginEnabled(): bool - { - if ($this->pluginEnabled !== null) { - return $this->pluginEnabled; - } - - $pluginCode = $this->getPluginCode(); - $isEnabled = Plugin::where('code', $pluginCode)->value('is_enabled'); - $this->pluginEnabled = (bool) $isEnabled; - - return $this->pluginEnabled; - } - - /** - * 清除插件配置缓存 - */ - public function clearConfigCache(): void - { - $pluginCode = $this->getPluginCode(); - Cache::forget("plugin_config_{$pluginCode}"); - $this->pluginConfig = null; - $this->pluginEnabled = null; - } -} \ No newline at end of file diff --git a/Xboard/app/Traits/QueryOperators.php b/Xboard/app/Traits/QueryOperators.php deleted file mode 100644 index 706d5c5..0000000 --- a/Xboard/app/Traits/QueryOperators.php +++ /dev/null @@ -1,68 +0,0 @@ - '=', - 'gt' => '>', - 'gte' => '>=', - 'lt' => '<', - 'lte' => '<=', - 'like' => 'like', - 'notlike' => 'not like', - 'null' => 'null', - 'notnull' => 'notnull', - default => 'like' - }; - } - - /** - * 获取查询值格式化 - * - * @param string $operator - * @param mixed $value - * @return mixed - */ - protected function formatQueryValue(string $operator, mixed $value): mixed - { - return match (strtolower($operator)) { - 'like', 'notlike' => "%{$value}%", - 'null', 'notnull' => null, - default => $value - }; - } - - /** - * 应用查询条件 - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param string $field - * @param string $operator - * @param mixed $value - * @return void - */ - protected function applyQueryCondition($query, array|Expression|string $field, string $operator, mixed $value): void - { - $queryOperator = $this->getQueryOperator($operator); - - if ($queryOperator === 'null') { - $query->whereNull($field); - } elseif ($queryOperator === 'notnull') { - $query->whereNotNull($field); - } else { - $query->where($field, $queryOperator, $this->formatQueryValue($operator, $value)); - } - } -} \ No newline at end of file diff --git a/Xboard/app/Utils/CacheKey.php b/Xboard/app/Utils/CacheKey.php deleted file mode 100644 index 6795c6c..0000000 --- a/Xboard/app/Utils/CacheKey.php +++ /dev/null @@ -1,69 +0,0 @@ - '邮箱验证码', - 'LAST_SEND_EMAIL_VERIFY_TIMESTAMP' => '最后一次发送邮箱验证码时间', - 'TEMP_TOKEN' => '临时令牌', - 'LAST_SEND_EMAIL_REMIND_TRAFFIC' => '最后发送流量邮件提醒', - 'SCHEDULE_LAST_CHECK_AT' => '计划任务最后检查时间', - 'REGISTER_IP_RATE_LIMIT' => '注册频率限制', - 'LAST_SEND_LOGIN_WITH_MAIL_LINK_TIMESTAMP' => '最后一次发送登入链接时间', - 'PASSWORD_ERROR_LIMIT' => '密码错误次数限制', - 'USER_SESSIONS' => '用户session', - 'FORGET_REQUEST_LIMIT' => '找回密码次数限制' - ]; - - // 允许的缓存键模式(支持通配符) - const ALLOWED_PATTERNS = [ - 'SERVER_*_ONLINE_USER', // 节点在线用户 - 'MULTI_SERVER_*_ONLINE_USER', // 多服务器在线用户 - 'SERVER_*_LAST_CHECK_AT', // 节点最后检查时间 - 'SERVER_*_LAST_PUSH_AT', // 节点最后推送时间 - 'SERVER_*_LOAD_STATUS', // 节点负载状态 - 'SERVER_*_LAST_LOAD_AT', // 节点最后负载提交时间 - 'SERVER_*_METRICS', // 节点指标数据 - 'USER_ONLINE_CONN_*_*', // 用户在线连接数 (特定节点类型_ID) - ]; - - /** - * 生成缓存键 - */ - public static function get(string $key, mixed $uniqueValue = null): string - { - // 检查是否为核心键 - if (array_key_exists($key, self::CORE_KEYS)) { - return $uniqueValue ? $key . '_' . $uniqueValue : $key; - } - - // 检查是否匹配允许的模式 - if (self::matchesPattern($key)) { - return $uniqueValue ? $key . '_' . $uniqueValue : $key; - } - - // 开发环境下记录警告,生产环境允许通过 - if (app()->environment('local', 'development')) { - logger()->warning("Unknown cache key used: {$key}"); - } - - return $uniqueValue ? $key . '_' . $uniqueValue : $key; - } - - /** - * 检查键名是否匹配允许的模式 - */ - private static function matchesPattern(string $key): bool - { - foreach (self::ALLOWED_PATTERNS as $pattern) { - $regex = '/^' . str_replace('*', '[A-Za-z0-9_]+', $pattern) . '$/'; - if (preg_match($regex, $key)) { - return true; - } - } - return false; - } -} diff --git a/Xboard/app/Utils/Dict.php b/Xboard/app/Utils/Dict.php deleted file mode 100644 index e0e6fe6..0000000 --- a/Xboard/app/Utils/Dict.php +++ /dev/null @@ -1,23 +0,0 @@ -", "~", "+", "=", ",", "." - )); - } - - $charsLen = count($chars) - 1; - shuffle($chars); - $str = ''; - for ($i = 0; $i < $len; $i++) { - $str .= $chars[mt_rand(0, $charsLen)]; - } - return $str; - } - - public static function wrapIPv6($addr) { - if (filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { - return "[$addr]"; - } else { - return $addr; - } - } - - public static function multiPasswordVerify($algo, $salt, $password, $hash) - { - switch($algo) { - case 'md5': return md5($password) === $hash; - case 'sha256': return hash('sha256', $password) === $hash; - case 'md5salt': return md5($password . $salt) === $hash; - case 'sha256salt': return hash('sha256', $password . $salt) === $hash; - default: return password_verify($password, $hash); - } - } - - public static function emailSuffixVerify($email, $suffixs) - { - $suffix = preg_split('/@/', $email)[1]; - if (!$suffix) return false; - if (!is_array($suffixs)) { - $suffixs = preg_split('/,/', $suffixs); - } - if (!in_array($suffix, $suffixs)) return false; - return true; - } - - public static function trafficConvert(float $byte) - { - $kb = 1024; - $mb = 1048576; - $gb = 1073741824; - if ($byte > $gb) { - return round($byte / $gb, 2) . ' GB'; - } else if ($byte > $mb) { - return round($byte / $mb, 2) . ' MB'; - } else if ($byte > $kb) { - return round($byte / $kb, 2) . ' KB'; - } else if ($byte < 0) { - return 0; - } else { - return round($byte, 2) . ' B'; - } - } - - public static function getSubscribeUrl(string $token, $subscribeUrl = null) - { - $path = route('client.subscribe', ['token' => $token], false); - - if ($subscribeUrl) { - $finalUrl = rtrim($subscribeUrl, '/') . $path; - return HookManager::filter('subscribe.url', $finalUrl); - } - - $urlString = (string)admin_setting('subscribe_url', ''); - $subscribeUrlList = $urlString ? explode(',', $urlString) : []; - - if (empty($subscribeUrlList)) { - return HookManager::filter('subscribe.url', url($path)); - } - - $selectedUrl = self::replaceByPattern(Arr::random($subscribeUrlList)); - $finalUrl = rtrim($selectedUrl, '/') . $path; - - return HookManager::filter('subscribe.url', $finalUrl); - } - - public static function randomPort($range): int { - $portRange = explode('-', (string) $range, 2); - $min = (int) ($portRange[0] ?? 0); - $max = (int) ($portRange[1] ?? $portRange[0] ?? 0); - if ($min > $max) { - [$min, $max] = [$max, $min]; - } - return random_int($min, $max); - } - - public static function base64EncodeUrlSafe($data) - { - $encoded = base64_encode($data); - return str_replace(['+', '/', '='], ['-', '_', ''], $encoded); - } - - /** - * 根据规则替换域名中对应的字符串 - * - * @param string $input 用户输入的字符串 - * @return string 替换后的字符串 - */ - public static function replaceByPattern($input) - { - $patterns = [ - '/\[(\d+)-(\d+)\]/' => function ($matches) { - $min = intval($matches[1]); - $max = intval($matches[2]); - if ($min > $max) { - list($min, $max) = [$max, $min]; - } - $randomNumber = rand($min, $max); - return $randomNumber; - }, - '/\[uuid\]/' => function () { - return self::guid(true); - } - ]; - foreach ($patterns as $pattern => $callback) { - $input = preg_replace_callback($pattern, $callback, $input); - } - return $input; - } - - public static function getIpByDomainName($domain) { - return gethostbynamel($domain) ?: []; - } - - public static function getTlsFingerprint($utls = null) - { - - if (is_array($utls) || is_object($utls)) { - if (!data_get($utls, 'enabled')) { - return null; - } - $fingerprint = data_get($utls, 'fingerprint', 'chrome'); - if ($fingerprint !== 'random') { - return $fingerprint; - } - } - - $fingerprints = ['chrome', 'firefox', 'safari', 'ios', 'edge', 'qq']; - return Arr::random($fingerprints); - } - - public static function encodeURIComponent($str) { - $revert = array('%21'=>'!', '%2A'=>'*', '%27'=>"'", '%28'=>'(', '%29'=>')'); - return strtr(rawurlencode($str), $revert); - } - - public static function getEmailSuffix(): array|bool - { - $suffix = admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT); - if (!is_array($suffix)) { - return preg_split('/,/', $suffix); - } - return $suffix; - } - - /** - * convert the transfer_enable to GB - * @param float $transfer_enable - * @return float - */ - public static function transferToGB(float $transfer_enable): float - { - return $transfer_enable / 1073741824; - } - - /** - * 转义 Telegram Markdown 特殊字符 - * @param string $text - * @return string - */ - public static function escapeMarkdown(string $text): string - { - return str_replace(['_', '*', '`', '['], ['\_', '\*', '\`', '\['], $text); - } -} diff --git a/Xboard/app/WebSocket/NodeEventHandlers.php b/Xboard/app/WebSocket/NodeEventHandlers.php deleted file mode 100644 index 75521f8..0000000 --- a/Xboard/app/WebSocket/NodeEventHandlers.php +++ /dev/null @@ -1,144 +0,0 @@ -type); - Cache::put(\App\Utils\CacheKey::get('SERVER_' . $nodeType . '_LAST_CHECK_AT', $nodeId), time(), 3600); - ServerService::updateMetrics($node, $data); - - Log::debug("[WS] Node#{$nodeId} status updated"); - } - - /** - * Handle device report from node - * - * 数据格式: {"event": "report.devices", "data": {userId: [ip1, ip2, ...], ...}} - */ - public static function handleDeviceReport(TcpConnection $conn, int $nodeId, array $data): void - { - $service = app(DeviceStateService::class); - - // Get old data - $oldDevices = $service->getNodeDevices($nodeId); - - // Calculate diff - $removedUsers = array_diff_key($oldDevices, $data); - $newDevices = []; - - foreach ($data as $userId => $ips) { - if (is_numeric($userId) && is_array($ips)) { - $newDevices[(int) $userId] = $ips; - } - } - - // Handle removed users - foreach ($removedUsers as $userId => $ips) { - $service->removeNodeDevices($nodeId, $userId); - $service->notifyUpdate($userId); - } - - // Handle new/updated users - foreach ($newDevices as $userId => $ips) { - $service->setDevices($userId, $nodeId, $ips); - } - - // Mark for push - Redis::sadd('device:push_pending_nodes', $nodeId); - - Log::debug("[WS] Node#{$nodeId} synced " . count($newDevices) . " users, removed " . count($removedUsers)); - } - - /** - * Handle device state request from node - */ - public static function handleDeviceRequest(TcpConnection $conn, int $nodeId, array $data = []): void - { - $node = Server::find($nodeId); - if (!$node) return; - - $users = ServerService::getAvailableUsers($node); - $userIds = $users->pluck('id')->toArray(); - - $service = app(DeviceStateService::class); - $devices = $service->getUsersDevices($userIds); - - $conn->send(json_encode([ - 'event' => 'sync.devices', - 'data' => ['users' => $devices], - ])); - - Log::debug("[WS] Node#{$nodeId} requested devices, sent " . count($devices) . " users"); - } - - /** - * Push device state to node - */ - public static function pushDeviceStateToNode(int $nodeId, DeviceStateService $service): void - { - $node = Server::find($nodeId); - if (!$node) return; - - $users = ServerService::getAvailableUsers($node); - $userIds = $users->pluck('id')->toArray(); - $devices = $service->getUsersDevices($userIds); - - NodeRegistry::send($nodeId, 'sync.devices', [ - 'users' => $devices - ]); - - Log::debug("[WS] Pushed device state to node#{$nodeId}: " . count($devices) . " users"); - } - - /** - * Push full config + users to newly connected node - */ - public static function pushFullSync(TcpConnection $conn, Server $node): void - { - $nodeId = $conn->nodeId; - - // Push config - $config = ServerService::buildNodeConfig($node); - $conn->send(json_encode([ - 'event' => 'sync.config', - 'data' => ['config' => $config] - ])); - - // Push users - $users = ServerService::getAvailableUsers($node)->toArray(); - $conn->send(json_encode([ - 'event' => 'sync.users', - 'data' => ['users' => $users] - ])); - - Log::info("[WS] Full sync pushed to node#{$nodeId}", [ - 'users' => count($users), - ]); - } -} diff --git a/Xboard/app/WebSocket/NodeWorker.php b/Xboard/app/WebSocket/NodeWorker.php deleted file mode 100644 index d45dd22..0000000 --- a/Xboard/app/WebSocket/NodeWorker.php +++ /dev/null @@ -1,249 +0,0 @@ - [NodeEventHandlers::class, 'handlePong'], - 'node.status' => [NodeEventHandlers::class, 'handleNodeStatus'], - 'report.devices' => [NodeEventHandlers::class, 'handleDeviceReport'], - 'request.devices' => [NodeEventHandlers::class, 'handleDeviceRequest'], - ]; - - public function __construct(string $host, int $port) - { - $this->worker = new Worker("websocket://{$host}:{$port}"); - $this->worker->count = 1; - $this->worker->name = 'xboard-ws-server'; - } - - public function run(): void - { - $this->setupLogging(); - $this->setupCallbacks(); - Worker::runAll(); - } - - private function setupLogging(): void - { - $logPath = storage_path('logs'); - if (!is_dir($logPath)) { - mkdir($logPath, 0777, true); - } - Worker::$logFile = $logPath . '/xboard-ws-server.log'; - Worker::$pidFile = $logPath . '/xboard-ws-server.pid'; - } - - private function setupCallbacks(): void - { - $this->worker->onWorkerStart = [$this, 'onWorkerStart']; - $this->worker->onConnect = [$this, 'onConnect']; - $this->worker->onWebSocketConnect = [$this, 'onWebSocketConnect']; - $this->worker->onMessage = [$this, 'onMessage']; - $this->worker->onClose = [$this, 'onClose']; - } - - public function onWorkerStart(Worker $worker): void - { - Log::info("[WS] Worker started, pid={$worker->id}"); - $this->subscribeRedis(); - $this->setupTimers(); - } - - private function setupTimers(): void - { - // Ping timer - Timer::add(self::PING_INTERVAL, function () { - foreach (NodeRegistry::getConnectedNodeIds() as $nodeId) { - $conn = NodeRegistry::get($nodeId); - if ($conn) { - $conn->send(json_encode(['event' => 'ping'])); - } - } - }); - - // Device state push timer - Timer::add(10, function () { - $pendingNodeIds = Redis::spop('device:push_pending_nodes', 100); - if (empty($pendingNodeIds)) { - return; - } - - $service = app(DeviceStateService::class); - foreach ($pendingNodeIds as $nodeId) { - $nodeId = (int) $nodeId; - if (NodeRegistry::get($nodeId) !== null) { - NodeEventHandlers::pushDeviceStateToNode($nodeId, $service); - } - } - }); - } - - public function onConnect(TcpConnection $conn): void - { - $conn->authTimer = Timer::add(self::AUTH_TIMEOUT, function () use ($conn) { - if (empty($conn->nodeId)) { - $conn->close(json_encode([ - 'event' => 'error', - 'data' => ['message' => 'auth timeout'], - ])); - } - }, [], false); - } - - public function onWebSocketConnect(TcpConnection $conn, $httpMessage): void - { - $queryString = ''; - if (is_string($httpMessage)) { - $queryString = parse_url($httpMessage, PHP_URL_QUERY) ?? ''; - } elseif ($httpMessage instanceof \Workerman\Protocols\Http\Request) { - $queryString = $httpMessage->queryString(); - } - - parse_str($queryString, $params); - $token = $params['token'] ?? ''; - $nodeId = (int) ($params['node_id'] ?? 0); - - // Authenticate - $serverToken = admin_setting('server_token', ''); - if ($token === '' || $serverToken === '' || !hash_equals($serverToken, $token)) { - $conn->close(json_encode([ - 'event' => 'error', - 'data' => ['message' => 'invalid token'], - ])); - return; - } - - $node = ServerService::getServer($nodeId, null); - if (!$node) { - $conn->close(json_encode([ - 'event' => 'error', - 'data' => ['message' => 'node not found'], - ])); - return; - } - - // Auth passed - if (isset($conn->authTimer)) { - Timer::del($conn->authTimer); - } - - $conn->nodeId = $nodeId; - NodeRegistry::add($nodeId, $conn); - Cache::put("node_ws_alive:{$nodeId}", true, 86400); - - // Clear old device data - app(DeviceStateService::class)->clearAllNodeDevices($nodeId); - - Log::debug("[WS] Node#{$nodeId} connected", [ - 'remote' => $conn->getRemoteIp(), - 'total' => NodeRegistry::count(), - ]); - - // Send auth success - $conn->send(json_encode([ - 'event' => 'auth.success', - 'data' => ['node_id' => $nodeId], - ])); - - // Push full sync - NodeEventHandlers::pushFullSync($conn, $node); - } - - public function onMessage(TcpConnection $conn, $data): void - { - $msg = json_decode($data, true); - if (!is_array($msg)) { - return; - } - - $event = $msg['event'] ?? ''; - $nodeId = $conn->nodeId ?? null; - - if (isset($this->handlers[$event]) && $nodeId) { - $handler = $this->handlers[$event]; - $handler($conn, $nodeId, $msg['data'] ?? []); - } - } - - public function onClose(TcpConnection $conn): void - { - if (!empty($conn->nodeId)) { - $nodeId = $conn->nodeId; - NodeRegistry::remove($nodeId); - Cache::forget("node_ws_alive:{$nodeId}"); - - $service = app(DeviceStateService::class); - $affectedUserIds = $service->clearAllNodeDevices($nodeId); - foreach ($affectedUserIds as $userId) { - $service->notifyUpdate($userId); - } - - Log::debug("[WS] Node#{$nodeId} disconnected", [ - 'total' => NodeRegistry::count(), - 'affected_users' => count($affectedUserIds), - ]); - } - } - - private function subscribeRedis(): void - { - $host = config('database.redis.default.host', '127.0.0.1'); - $port = config('database.redis.default.port', 6379); - - if (str_starts_with($host, '/')) { - $redisUri = "unix://{$host}"; - } else { - $redisUri = "redis://{$host}:{$port}"; - } - - $redis = new \Workerman\Redis\Client($redisUri); - - $password = config('database.redis.default.password'); - if ($password) { - $redis->auth($password); - } - - $prefix = config('database.redis.options.prefix', ''); - $channel = $prefix . 'node:push'; - - $redis->subscribe([$channel], function ($chan, $message) { - $payload = json_decode($message, true); - if (!is_array($payload)) { - return; - } - - $nodeId = $payload['node_id'] ?? null; - $event = $payload['event'] ?? ''; - $data = $payload['data'] ?? []; - - if (!$nodeId || !$event) { - return; - } - - $sent = NodeRegistry::send((int) $nodeId, $event, $data); - if ($sent) { - Log::debug("[WS] Pushed {$event} to node#{$nodeId}"); - } - }); - - Log::info("[WS] Subscribed to Redis channel: {$channel}"); - } -} diff --git a/Xboard/artisan b/Xboard/artisan deleted file mode 100644 index 5c23e2e..0000000 --- a/Xboard/artisan +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env php -make(Illuminate\Contracts\Console\Kernel::class); - -$status = $kernel->handle( - $input = new Symfony\Component\Console\Input\ArgvInput, - new Symfony\Component\Console\Output\ConsoleOutput -); - -/* -|-------------------------------------------------------------------------- -| Shutdown The Application -|-------------------------------------------------------------------------- -| -| Once Artisan has finished running, we will fire off the shutdown events -| so that any final work may be done by the application before we shut -| down the process. This is the last thing to happen to the request. -| -*/ - -$kernel->terminate($input, $status); - -exit($status); diff --git a/Xboard/bootstrap/app.php b/Xboard/bootstrap/app.php deleted file mode 100644 index 037e17d..0000000 --- a/Xboard/bootstrap/app.php +++ /dev/null @@ -1,55 +0,0 @@ -singleton( - Illuminate\Contracts\Http\Kernel::class, - App\Http\Kernel::class -); - -$app->singleton( - Illuminate\Contracts\Console\Kernel::class, - App\Console\Kernel::class -); - -$app->singleton( - Illuminate\Contracts\Debug\ExceptionHandler::class, - App\Exceptions\Handler::class -); - -/* -|-------------------------------------------------------------------------- -| Return The Application -|-------------------------------------------------------------------------- -| -| This script returns the application instance. The instance is given to -| the calling script so we can separate the building of the instances -| from the actual running of the application and sending responses. -| -*/ - -return $app; diff --git a/Xboard/bootstrap/cache/.gitignore b/Xboard/bootstrap/cache/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/Xboard/bootstrap/cache/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/Xboard/compose.sample.yaml b/Xboard/compose.sample.yaml deleted file mode 100644 index b955027..0000000 --- a/Xboard/compose.sample.yaml +++ /dev/null @@ -1,44 +0,0 @@ -services: - web: - image: ghcr.io/cedar2025/xboard:new - volumes: - - redis-data:/data - - ./:/www/ - environment: - - docker=true - depends_on: - - redis - network_mode: host - command: php artisan octane:start --port=7001 --host=0.0.0.0 - restart: always - horizon: - image: ghcr.io/cedar2025/xboard:new - volumes: - - redis-data:/data - - ./:/www/ - restart: always - network_mode: host - command: php artisan horizon - depends_on: - - redis - ws-server: - image: ghcr.io/cedar2025/xboard:new - volumes: - - redis-data:/data - - ./:/www/ - restart: always - network_mode: host - command: php artisan ws-server start - depends_on: - - redis - redis: - image: redis:7-alpine - command: redis-server --unixsocket /data/redis.sock --unixsocketperm 777 - restart: unless-stopped - volumes: - - redis-data:/data - sysctls: - net.core.somaxconn: 1024 - -volumes: - redis-data: diff --git a/Xboard/composer.json b/Xboard/composer.json deleted file mode 100644 index ce7762d..0000000 --- a/Xboard/composer.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "name": "xboard/xboard", - "type": "project", - "description": "xboard is a proxy protocol manage.", - "keywords": [ - "xboard", - "v2ray", - "shadowsocks", - "trojan", - "laravel" - ], - "license": "MIT", - "require": { - "php": "^8.2", - "bacon/bacon-qr-code": "^2.0", - "doctrine/dbal": "^4.0", - "google/cloud-storage": "^1.35", - "google/recaptcha": "^1.2", - "guzzlehttp/guzzle": "^7.8", - "laravel/framework": "^12.0", - "laravel/horizon": "^5.30", - "laravel/octane": "2.11.*", - "laravel/prompts": "^0.3", - "laravel/sanctum": "^4.0", - "laravel/tinker": "^2.10", - "linfo/linfo": "^4.0", - "paragonie/sodium_compat": "^1.20", - "php-curl-class/php-curl-class": "^8.6", - "spatie/db-dumper": "^3.4", - "stripe/stripe-php": "^7.36.1", - "symfony/http-client": "^7.0", - "symfony/mailgun-mailer": "^7.0", - "symfony/yaml": "*", - "webmozart/assert": "*", - "workerman/redis": "^2.0", - "workerman/workerman": "^5.1", - "zoujingli/ip2region": "^2.0" - }, - "require-dev": { - "barryvdh/laravel-debugbar": "^3.9", - "fakerphp/faker": "^1.9.1", - "larastan/larastan": "^3.0", - "mockery/mockery": "^1.6", - "nunomaduro/collision": "^8.0", - "orangehill/iseed": "^3.0", - "phpunit/phpunit": "^11.0", - "spatie/laravel-ignition": "^2.4" - }, - "config": { - "optimize-autoloader": true, - "preferred-install": "dist", - "sort-packages": true - }, - "extra": { - "laravel": { - "dont-discover": [] - } - }, - "autoload": { - "psr-4": { - "App\\": "app/", - "Library\\": "library/", - "Plugin\\": "plugins/" - }, - "classmap": [ - "database/seeders", - "database/factories" - ], - "files": [ - "app/Helpers/Functions.php" - ] - }, - "autoload-dev": { - "psr-4": { - "Tests\\": "tests/" - } - }, - "minimum-stability": "stable", - "prefer-stable": true, - "scripts": { - "post-autoload-dump": [ - "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", - "@php artisan package:discover --ansi" - ], - "post-root-package-install": [ - "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" - ], - "post-create-project-cmd": [ - "@php artisan key:generate --ansi" - ] - }, - "repositories": [ - { - "type": "composer", - "url": "https://packagist.org" - } - ] -} diff --git a/Xboard/config/app.php b/Xboard/config/app.php deleted file mode 100644 index 3ed7fe4..0000000 --- a/Xboard/config/app.php +++ /dev/null @@ -1,193 +0,0 @@ - env('APP_NAME', 'Laravel'), - - /* - |-------------------------------------------------------------------------- - | Application Environment - |-------------------------------------------------------------------------- - | - | This value determines the "environment" your application is currently - | running in. This may determine how you prefer to configure various - | services the application utilizes. Set this in your ".env" file. - | - */ - - 'env' => env('APP_ENV', 'production'), - - /* - |-------------------------------------------------------------------------- - | Application Debug Mode - |-------------------------------------------------------------------------- - | - | When your application is in debug mode, detailed error messages with - | stack traces will be shown on every error that occurs within your - | application. If disabled, a simple generic error page is shown. - | - */ - - 'debug' => env('APP_DEBUG', false), - - /* - |-------------------------------------------------------------------------- - | Application URL - |-------------------------------------------------------------------------- - | - | This URL is used by the console to properly generate URLs when using - | the Artisan command line tool. You should set this to the root of - | your application so that it is used when running Artisan tasks. - | - */ - - 'url' => env('APP_URL', 'http://localhost'), - - 'asset_url' => env('ASSET_URL', null), - - /* - |-------------------------------------------------------------------------- - | Application Timezone - |-------------------------------------------------------------------------- - | - | Here you may specify the default timezone for your application, which - | will be used by the PHP date and date-time functions. We have gone - | ahead and set this to a sensible default for you out of the box. - | - */ - - 'timezone' => 'Asia/Shanghai', - - /* - |-------------------------------------------------------------------------- - | Application Locale Configuration - |-------------------------------------------------------------------------- - | - | The application locale determines the default locale that will be used - | by the translation service provider. You are free to set this value - | to any of the locales which will be supported by the application. - | - */ - - 'locale' => 'zh-CN', - - /* - |-------------------------------------------------------------------------- - | Application Fallback Locale - |-------------------------------------------------------------------------- - | - | The fallback locale determines the locale to use when the current one - | is not available. You may change the value to correspond to any of - | the language folders that are provided through your application. - | - */ - - 'fallback_locale' => 'zh-CN', - - /* - |-------------------------------------------------------------------------- - | Faker Locale - |-------------------------------------------------------------------------- - | - | This locale will be used by the Faker PHP library when generating fake - | data for your database seeds. For example, this will be used to get - | localized telephone numbers, street address information and more. - | - */ - - 'faker_locale' => 'zh-CN', - - /* - |-------------------------------------------------------------------------- - | Encryption Key - |-------------------------------------------------------------------------- - | - | This key is used by the Illuminate encrypter service and should be set - | to a random, 32 character string, otherwise these encrypted strings - | will not be safe. Please do this before deploying an application! - | - */ - - 'key' => env('APP_KEY'), - - 'cipher' => 'AES-256-CBC', - - /* - |-------------------------------------------------------------------------- - | Autoloaded Service Providers - |-------------------------------------------------------------------------- - | - | The service providers listed here will be automatically loaded on the - | request to your application. Feel free to add your own services to - | this array to grant expanded functionality to your applications. - | - */ - - 'providers' => [ - - /* - * Laravel Framework Service Providers... - */ - Illuminate\Auth\AuthServiceProvider::class, - Illuminate\Broadcasting\BroadcastServiceProvider::class, - Illuminate\Bus\BusServiceProvider::class, - Illuminate\Cache\CacheServiceProvider::class, - Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class, - Illuminate\Cookie\CookieServiceProvider::class, - Illuminate\Database\DatabaseServiceProvider::class, - Illuminate\Encryption\EncryptionServiceProvider::class, - Illuminate\Filesystem\FilesystemServiceProvider::class, - Illuminate\Foundation\Providers\FoundationServiceProvider::class, - Illuminate\Hashing\HashServiceProvider::class, - Illuminate\Mail\MailServiceProvider::class, - Illuminate\Notifications\NotificationServiceProvider::class, - Illuminate\Pagination\PaginationServiceProvider::class, - Illuminate\Pipeline\PipelineServiceProvider::class, - Illuminate\Queue\QueueServiceProvider::class, - Illuminate\Redis\RedisServiceProvider::class, - Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, - Illuminate\Session\SessionServiceProvider::class, - Illuminate\Translation\TranslationServiceProvider::class, - Illuminate\Validation\ValidationServiceProvider::class, - Illuminate\View\ViewServiceProvider::class, - - /* - * Package Service Providers... - */ - - /* - * Application Service Providers... - */ - App\Providers\AuthServiceProvider::class, - // App\Providers\BroadcastServiceProvider::class, - App\Providers\EventServiceProvider::class, - App\Providers\HorizonServiceProvider::class, - App\Providers\RouteServiceProvider::class, - App\Providers\SettingServiceProvider::class, - App\Providers\OctaneServiceProvider::class, - App\Providers\PluginServiceProvider::class, - App\Providers\ProtocolServiceProvider::class, - - ], - - /* - |-------------------------------------------------------------------------- - | V2board version - |-------------------------------------------------------------------------- - | - | The only modification by laravel config - | - */ - 'version' => '1.0.0' -]; diff --git a/Xboard/config/auth.php b/Xboard/config/auth.php deleted file mode 100644 index c75105f..0000000 --- a/Xboard/config/auth.php +++ /dev/null @@ -1,103 +0,0 @@ - [ - 'guard' => 'api', - 'passwords' => 'users', - ], - - /* - |-------------------------------------------------------------------------- - | Authentication Guards - |-------------------------------------------------------------------------- - | - | Next, you may define every authentication guard for your application. - | Of course, a great default configuration has been defined for you - | here which uses session storage and the Eloquent user provider. - | - | All authentication drivers have a user provider. This defines how the - | users are actually retrieved out of your database or other storage - | mechanisms used by this application to persist your user's data. - | - | Supported: "session", "token" - | - */ - - 'guards' => [ - 'web' => [ - 'driver' => 'session', - 'provider' => 'users', - ], - - 'api' => [ - 'driver' => 'sanctum', - 'provider' => 'users', - 'hash' => false, - ], - ], - - /* - |-------------------------------------------------------------------------- - | User Providers - |-------------------------------------------------------------------------- - | - | All authentication drivers have a user provider. This defines how the - | users are actually retrieved out of your database or other storage - | mechanisms used by this application to persist your user's data. - | - | If you have multiple user tables or models you may configure multiple - | sources which represent each model / table. These sources may then - | be assigned to any extra authentication guards you have defined. - | - | Supported: "database", "eloquent" - | - */ - - 'providers' => [ - 'users' => [ - 'driver' => 'eloquent', - 'model' => App\Models\User::class, - ], - - // 'users' => [ - // 'driver' => 'database', - // 'table' => 'users', - // ], - ], - - /* - |-------------------------------------------------------------------------- - | Resetting Passwords - |-------------------------------------------------------------------------- - | - | You may specify multiple password reset configurations if you have more - | than one user table or model in the application and you want to have - | separate password reset settings based on the specific user types. - | - | The expire time is the number of minutes that the reset token should be - | considered valid. This security feature keeps tokens short-lived so - | they have less time to be guessed. You may change this as needed. - | - */ - - 'passwords' => [ - 'users' => [ - 'provider' => 'users', - 'table' => 'password_resets', - 'expire' => 60, - ], - ], - -]; diff --git a/Xboard/config/broadcasting.php b/Xboard/config/broadcasting.php deleted file mode 100644 index 3bba110..0000000 --- a/Xboard/config/broadcasting.php +++ /dev/null @@ -1,59 +0,0 @@ - env('BROADCAST_DRIVER', 'null'), - - /* - |-------------------------------------------------------------------------- - | Broadcast Connections - |-------------------------------------------------------------------------- - | - | Here you may define all of the broadcast connections that will be used - | to broadcast events to other systems or over websockets. Samples of - | each available type of connection are provided inside this array. - | - */ - - 'connections' => [ - - 'pusher' => [ - 'driver' => 'pusher', - 'key' => env('PUSHER_APP_KEY'), - 'secret' => env('PUSHER_APP_SECRET'), - 'app_id' => env('PUSHER_APP_ID'), - 'options' => [ - 'cluster' => env('PUSHER_APP_CLUSTER'), - 'useTLS' => true, - ], - ], - - 'redis' => [ - 'driver' => 'redis', - 'connection' => 'default', - ], - - 'log' => [ - 'driver' => 'log', - ], - - 'null' => [ - 'driver' => 'null', - ], - - ], - -]; diff --git a/Xboard/config/cache.php b/Xboard/config/cache.php deleted file mode 100644 index b97535c..0000000 --- a/Xboard/config/cache.php +++ /dev/null @@ -1,107 +0,0 @@ - env('CACHE_DRIVER', 'file'), - - /* - |-------------------------------------------------------------------------- - | Cache Stores - |-------------------------------------------------------------------------- - | - | Here you may define all of the cache "stores" for your application as - | well as their drivers. You may even define multiple stores for the - | same cache driver to group types of items stored in your caches. - | - */ - - 'stores' => [ - - 'apc' => [ - 'driver' => 'apc', - ], - - 'array' => [ - 'driver' => 'array', - ], - - 'database' => [ - 'driver' => 'database', - 'table' => 'cache', - 'connection' => null, - ], - - 'file' => [ - 'driver' => 'file', - 'path' => storage_path('framework/cache/data'), - ], - - 'memcached' => [ - 'driver' => 'memcached', - 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), - 'sasl' => [ - env('MEMCACHED_USERNAME'), - env('MEMCACHED_PASSWORD'), - ], - 'options' => [ - // Memcached::OPT_CONNECT_TIMEOUT => 2000, - ], - 'servers' => [ - [ - 'host' => env('MEMCACHED_HOST', '127.0.0.1'), - 'port' => env('MEMCACHED_PORT', 11211), - 'weight' => 100, - ], - ], - ], - - 'redis' => [ - 'driver' => 'redis', - 'connection' => 'cache', - ], - - 'octane' => [ - 'driver' => 'octane' - ], - - 'dynamodb' => [ - 'driver' => 'dynamodb', - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => env('AWS_V2BOARD_REGION', 'us-east-1'), - 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), - 'endpoint' => env('DYNAMODB_ENDPOINT'), - ], - - ], - - /* - |-------------------------------------------------------------------------- - | Cache Key Prefix - |-------------------------------------------------------------------------- - | - | When utilizing a RAM based store such as APC or Memcached, there might - | be other applications utilizing the same cache. So, we'll specify a - | value to get prefixed to all our keys so we can avoid collisions. - | - */ - - 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_cache'), - -]; diff --git a/Xboard/config/cloud_storage.php b/Xboard/config/cloud_storage.php deleted file mode 100644 index 7ba0747..0000000 --- a/Xboard/config/cloud_storage.php +++ /dev/null @@ -1,10 +0,0 @@ - [ - 'key_file' => env('GOOGLE_CLOUD_KEY_FILE') ? base_path(env('GOOGLE_CLOUD_KEY_FILE')) : null, - 'storage_bucket' => env('GOOGLE_CLOUD_STORAGE_BUCKET'), - ], - -]; \ No newline at end of file diff --git a/Xboard/config/cors.php b/Xboard/config/cors.php deleted file mode 100644 index 558369d..0000000 --- a/Xboard/config/cors.php +++ /dev/null @@ -1,34 +0,0 @@ - ['api/*'], - - 'allowed_methods' => ['*'], - - 'allowed_origins' => ['*'], - - 'allowed_origins_patterns' => [], - - 'allowed_headers' => ['*'], - - 'exposed_headers' => [], - - 'max_age' => 0, - - 'supports_credentials' => false, - -]; diff --git a/Xboard/config/database.php b/Xboard/config/database.php deleted file mode 100644 index 1929d5e..0000000 --- a/Xboard/config/database.php +++ /dev/null @@ -1,157 +0,0 @@ - env('DB_CONNECTION', 'mysql'), - - /* - |-------------------------------------------------------------------------- - | Database Connections - |-------------------------------------------------------------------------- - | - | Here are each of the database connections setup for your application. - | Of course, examples of configuring each database platform that is - | supported by Laravel is shown below to make development simple. - | - | - | All database work in Laravel is done through the PHP PDO facilities - | so make sure you have the driver for your particular database of - | choice installed on your machine before you begin development. - | - */ - - 'connections' => [ - - 'sqlite' => [ - 'driver' => 'sqlite', - 'url' => env('DATABASE_URL'), - 'database' => env('DB_DATABASE') ? base_path(env('DB_DATABASE')) : database_path('database.sqlite'), - 'prefix' => '', - 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), - 'busy_timeout' => env('DB_BUSY_TIMEOUT', 30000), - 'journal_mode' => env('DB_JOURNAL_MODE', 'wal'), - 'synchronous' => env('DB_SYNCHRONOUS', 'normal'), - ], - - 'mysql' => [ - 'driver' => 'mysql', - 'url' => env('DATABASE_URL'), - 'host' => env('DB_HOST', '127.0.0.1'), - 'port' => env('DB_PORT', '3306'), - 'database' => env('DB_DATABASE', 'forge'), - 'username' => env('DB_USERNAME', 'forge'), - 'password' => env('DB_PASSWORD', ''), - 'unix_socket' => env('DB_SOCKET', ''), - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => '', - 'prefix_indexes' => true, - 'strict' => true, - 'engine' => null, - 'options' => (extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), - PDO::ATTR_PERSISTENT => false, - ]) : []), - 'pool' => [ - 'min_connections' => 1, - 'max_connections' => 10, - 'idle_timeout' => 60, - ], - ], - - 'pgsql' => [ - 'driver' => 'pgsql', - 'url' => env('DATABASE_URL'), - 'host' => env('DB_HOST', '127.0.0.1'), - 'port' => env('DB_PORT', '5432'), - 'database' => env('DB_DATABASE', 'forge'), - 'username' => env('DB_USERNAME', 'forge'), - 'password' => env('DB_PASSWORD', ''), - 'charset' => 'utf8', - 'prefix' => '', - 'prefix_indexes' => true, - 'search_path' => 'public', - 'sslmode' => 'prefer', - ], - - 'sqlsrv' => [ - 'driver' => 'sqlsrv', - 'url' => env('DATABASE_URL'), - 'host' => env('DB_HOST', 'localhost'), - 'port' => env('DB_PORT', '1433'), - 'database' => env('DB_DATABASE', 'forge'), - 'username' => env('DB_USERNAME', 'forge'), - 'password' => env('DB_PASSWORD', ''), - 'charset' => 'utf8', - 'prefix' => '', - 'prefix_indexes' => true, - ], - - ], - - /* - |-------------------------------------------------------------------------- - | Migration Repository Table - |-------------------------------------------------------------------------- - | - | This table keeps track of all the migrations that have already run for - | your application. Using this information, we can determine which of - | the migrations on disk haven't actually been run in the database. - | - */ - - 'migrations' => 'migrations', - - /* - |-------------------------------------------------------------------------- - | Redis Databases - |-------------------------------------------------------------------------- - | - | Redis is an open source, fast, and advanced key-value store that also - | provides a richer body of commands than a typical key-value system - | such as APC or Memcached. Laravel makes it easy to dig right in. - | - */ - - 'redis' => [ - - 'client' => env('REDIS_CLIENT', 'phpredis'), - - 'options' => [ - 'cluster' => env('REDIS_CLUSTER', 'redis'), - 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_database_'), - ], - - 'default' => [ - 'url' => env('REDIS_URL'), - 'host' => env('REDIS_HOST', '127.0.0.1'), - 'password' => env('REDIS_PASSWORD', null), - 'port' => env('REDIS_PORT', 6379), - 'database' => env('REDIS_DB', 0), - 'persistent' => true, // 开启持久连接 - ], - - 'cache' => [ - 'url' => env('REDIS_URL'), - 'host' => env('REDIS_HOST', '127.0.0.1'), - 'password' => env('REDIS_PASSWORD', null), - 'port' => env('REDIS_PORT', 6379), - 'database' => env('REDIS_CACHE_DB', 1), - ], - - ], - -]; diff --git a/Xboard/config/debugbar.php b/Xboard/config/debugbar.php deleted file mode 100644 index fe3b192..0000000 --- a/Xboard/config/debugbar.php +++ /dev/null @@ -1,275 +0,0 @@ - env('DEBUGBAR_ENABLED', null), - 'except' => [ - 'telescope*', - 'horizon*', - ], - - /* - |-------------------------------------------------------------------------- - | Storage settings - |-------------------------------------------------------------------------- - | - | DebugBar stores data for session/ajax requests. - | You can disable this, so the debugbar stores data in headers/session, - | but this can cause problems with large data collectors. - | By default, file storage (in the storage folder) is used. Redis and PDO - | can also be used. For PDO, run the package migrations first. - | - */ - 'storage' => [ - 'enabled' => true, - 'driver' => 'file', // redis, file, pdo, socket, custom - 'path' => storage_path('debugbar'), // For file driver - 'connection' => null, // Leave null for default connection (Redis/PDO) - 'provider' => '', // Instance of StorageInterface for custom driver - 'hostname' => '127.0.0.1', // Hostname to use with the "socket" driver - 'port' => 2304, // Port to use with the "socket" driver - ], - - /* - |-------------------------------------------------------------------------- - | Editor - |-------------------------------------------------------------------------- - | - | Choose your preferred editor to use when clicking file name. - | - | Supported: "phpstorm", "vscode", "vscode-insiders", "vscode-remote", - | "vscode-insiders-remote", "vscodium", "textmate", "emacs", - | "sublime", "atom", "nova", "macvim", "idea", "netbeans", - | "xdebug", "espresso" - | - */ - - 'editor' => env('DEBUGBAR_EDITOR', 'phpstorm'), - - /* - |-------------------------------------------------------------------------- - | Remote Path Mapping - |-------------------------------------------------------------------------- - | - | If you are using a remote dev server, like Laravel Homestead, Docker, or - | even a remote VPS, it will be necessary to specify your path mapping. - | - | Leaving one, or both of these, empty or null will not trigger the remote - | URL changes and Debugbar will treat your editor links as local files. - | - | "remote_sites_path" is an absolute base path for your sites or projects - | in Homestead, Vagrant, Docker, or another remote development server. - | - | Example value: "/home/vagrant/Code" - | - | "local_sites_path" is an absolute base path for your sites or projects - | on your local computer where your IDE or code editor is running on. - | - | Example values: "/Users//Code", "C:\Users\\Documents\Code" - | - */ - - 'remote_sites_path' => env('DEBUGBAR_REMOTE_SITES_PATH', ''), - 'local_sites_path' => env('DEBUGBAR_LOCAL_SITES_PATH', ''), - - /* - |-------------------------------------------------------------------------- - | Vendors - |-------------------------------------------------------------------------- - | - | Vendor files are included by default, but can be set to false. - | This can also be set to 'js' or 'css', to only include javascript or css vendor files. - | Vendor files are for css: font-awesome (including fonts) and highlight.js (css files) - | and for js: jquery and and highlight.js - | So if you want syntax highlighting, set it to true. - | jQuery is set to not conflict with existing jQuery scripts. - | - */ - - 'include_vendors' => true, - - /* - |-------------------------------------------------------------------------- - | Capture Ajax Requests - |-------------------------------------------------------------------------- - | - | The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors), - | you can use this option to disable sending the data through the headers. - | - | Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools. - | - | Note for your request to be identified as ajax requests they must either send the header - | X-Requested-With with the value XMLHttpRequest (most JS libraries send this), or have application/json as a Accept header. - */ - - 'capture_ajax' => true, - 'add_ajax_timing' => false, - - /* - |-------------------------------------------------------------------------- - | Custom Error Handler for Deprecated warnings - |-------------------------------------------------------------------------- - | - | When enabled, the Debugbar shows deprecated warnings for Symfony components - | in the Messages tab. - | - */ - 'error_handler' => false, - - /* - |-------------------------------------------------------------------------- - | Clockwork integration - |-------------------------------------------------------------------------- - | - | The Debugbar can emulate the Clockwork headers, so you can use the Chrome - | Extension, without the server-side code. It uses Debugbar collectors instead. - | - */ - 'clockwork' => false, - - /* - |-------------------------------------------------------------------------- - | DataCollectors - |-------------------------------------------------------------------------- - | - | Enable/disable DataCollectors - | - */ - - 'collectors' => [ - 'phpinfo' => true, // Php version - 'messages' => true, // Messages - 'time' => true, // Time Datalogger - 'memory' => true, // Memory usage - 'exceptions' => true, // Exception displayer - 'log' => true, // Logs from Monolog (merged in messages if enabled) - 'db' => true, // Show database (PDO) queries and bindings - 'views' => true, // Views with their data - 'route' => true, // Current route information - 'auth' => false, // Display Laravel authentication status - 'gate' => true, // Display Laravel Gate checks - 'session' => true, // Display session data - 'symfony_request' => true, // Only one can be enabled.. - 'mail' => true, // Catch mail messages - 'laravel' => false, // Laravel version and environment - 'events' => false, // All events fired - 'default_request' => false, // Regular or special Symfony request logger - 'logs' => false, // Add the latest log messages - 'files' => false, // Show the included files - 'config' => false, // Display config settings - 'cache' => false, // Display cache events - 'models' => true, // Display models - 'livewire' => true, // Display Livewire (when available) - ], - - /* - |-------------------------------------------------------------------------- - | Extra options - |-------------------------------------------------------------------------- - | - | Configure some DataCollectors - | - */ - - 'options' => [ - 'auth' => [ - 'show_name' => true, // Also show the users name/email in the debugbar - ], - 'db' => [ - 'with_params' => true, // Render SQL with the parameters substituted - 'backtrace' => true, // Use a backtrace to find the origin of the query in your files. - 'backtrace_exclude_paths' => [], // Paths to exclude from backtrace. (in addition to defaults) - 'timeline' => false, // Add the queries to the timeline - 'duration_background' => true, // Show shaded background on each query relative to how long it took to execute. - 'explain' => [ // Show EXPLAIN output on queries - 'enabled' => false, - 'types' => ['SELECT'], // Deprecated setting, is always only SELECT - ], - 'hints' => false, // Show hints for common mistakes - 'show_copy' => false, // Show copy button next to the query - ], - 'mail' => [ - 'full_log' => false, - ], - 'views' => [ - 'timeline' => false, // Add the views to the timeline (Experimental) - 'data' => false, //Note: Can slow down the application, because the data can be quite large.. - ], - 'route' => [ - 'label' => true, // show complete route on bar - ], - 'logs' => [ - 'file' => null, - ], - 'cache' => [ - 'values' => true, // collect cache values - ], - ], - - /* - |-------------------------------------------------------------------------- - | Inject Debugbar in Response - |-------------------------------------------------------------------------- - | - | Usually, the debugbar is added just before , by listening to the - | Response after the App is done. If you disable this, you have to add them - | in your template yourself. See http://phpdebugbar.com/docs/rendering.html - | - */ - - 'inject' => true, - - /* - |-------------------------------------------------------------------------- - | DebugBar route prefix - |-------------------------------------------------------------------------- - | - | Sometimes you want to set route prefix to be used by DebugBar to load - | its resources from. Usually the need comes from misconfigured web server or - | from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97 - | - */ - 'route_prefix' => '_debugbar', - - /* - |-------------------------------------------------------------------------- - | DebugBar route domain - |-------------------------------------------------------------------------- - | - | By default DebugBar route served from the same domain that request served. - | To override default domain, specify it as a non-empty value. - */ - 'route_domain' => null, - - /* - |-------------------------------------------------------------------------- - | DebugBar theme - |-------------------------------------------------------------------------- - | - | Switches between light and dark theme. If set to auto it will respect system preferences - | Possible values: auto, light, dark - */ - 'theme' => env('DEBUGBAR_THEME', 'auto'), - - /* - |-------------------------------------------------------------------------- - | Backtrace stack limit - |-------------------------------------------------------------------------- - | - | By default, the DebugBar limits the number of frames returned by the 'debug_backtrace()' function. - | If you need larger stacktraces, you can increase this number. Setting it to 0 will result in no limit. - */ - 'debug_backtrace_limit' => 50, -]; diff --git a/Xboard/config/filesystems.php b/Xboard/config/filesystems.php deleted file mode 100644 index 925b69d..0000000 --- a/Xboard/config/filesystems.php +++ /dev/null @@ -1,69 +0,0 @@ - env('FILESYSTEM_DISK', 'local'), - - /* - |-------------------------------------------------------------------------- - | Default Cloud Filesystem Disk - |-------------------------------------------------------------------------- - | - | Many applications store files both locally and in the cloud. For this - | reason, you may specify a default "cloud" driver here. This driver - | will be bound as the Cloud disk implementation in the container. - | - */ - - 'cloud' => env('FILESYSTEM_CLOUD', 's3'), - - /* - |-------------------------------------------------------------------------- - | Filesystem Disks - |-------------------------------------------------------------------------- - | - | Here you may configure as many filesystem "disks" as you wish, and you - | may even configure multiple disks of the same driver. Defaults have - | been setup for each driver as an example of the required options. - | - | Supported Drivers: "local", "ftp", "sftp", "s3" - | - */ - - 'disks' => [ - - 'local' => [ - 'driver' => 'local', - 'root' => storage_path('app'), - ], - - 'public' => [ - 'driver' => 'local', - 'root' => storage_path('app/public'), - 'url' => env('APP_URL') . '/storage', - 'visibility' => 'public', - ], - - 's3' => [ - 'driver' => 's3', - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => env('AWS_V2BOARD_REGION'), - 'bucket' => env('AWS_BUCKET'), - 'url' => env('AWS_URL'), - ], - - ], - -]; diff --git a/Xboard/config/hashing.php b/Xboard/config/hashing.php deleted file mode 100644 index 9146bfd..0000000 --- a/Xboard/config/hashing.php +++ /dev/null @@ -1,52 +0,0 @@ - 'bcrypt', - - /* - |-------------------------------------------------------------------------- - | Bcrypt Options - |-------------------------------------------------------------------------- - | - | Here you may specify the configuration options that should be used when - | passwords are hashed using the Bcrypt algorithm. This will allow you - | to control the amount of time it takes to hash the given password. - | - */ - - 'bcrypt' => [ - 'rounds' => env('BCRYPT_ROUNDS', 10), - ], - - /* - |-------------------------------------------------------------------------- - | Argon Options - |-------------------------------------------------------------------------- - | - | Here you may specify the configuration options that should be used when - | passwords are hashed using the Argon algorithm. These will allow you - | to control the amount of time it takes to hash the given password. - | - */ - - 'argon' => [ - 'memory' => 8192, - 'threads' => 2, - 'time' => 2, - ], - -]; diff --git a/Xboard/config/hidden_features.php b/Xboard/config/hidden_features.php deleted file mode 100644 index b676bed..0000000 --- a/Xboard/config/hidden_features.php +++ /dev/null @@ -1,5 +0,0 @@ - (env('ENABLE_EXPOSED_USER_COUNT_FIX') === base64_decode('M2YwNmYxODI=')) -]; \ No newline at end of file diff --git a/Xboard/config/horizon.php b/Xboard/config/horizon.php deleted file mode 100644 index fde7487..0000000 --- a/Xboard/config/horizon.php +++ /dev/null @@ -1,228 +0,0 @@ -getParser(); - -return [ - - /* - |-------------------------------------------------------------------------- - | Horizon Domain - |-------------------------------------------------------------------------- - | - | This is the subdomain where Horizon will be accessible from. If this - | setting is null, Horizon will reside under the same domain as the - | application. Otherwise, this value will serve as the subdomain. - | - */ - - 'domain' => null, - - /* - |-------------------------------------------------------------------------- - | Horizon Path - |-------------------------------------------------------------------------- - | - | This is the URI path where Horizon will be accessible from. Feel free - | to change this path to anything you like. Note that the URI will not - | affect the paths of its internal API that aren't exposed to users. - | - */ - - 'path' => 'monitor', - - /* - |-------------------------------------------------------------------------- - | Horizon Redis Connection - |-------------------------------------------------------------------------- - | - | This is the name of the Redis connection where Horizon will store the - | meta information required for it to function. It includes the list - | of supervisors, failed jobs, job metrics, and other information. - | - */ - - 'use' => 'default', - - /* - |-------------------------------------------------------------------------- - | Horizon Redis Prefix - |-------------------------------------------------------------------------- - | - | This prefix will be used when storing all Horizon data in Redis. You - | may modify the prefix when you are running multiple installations - | of Horizon on the same server so that they don't have problems. - | - */ - - 'prefix' => env( - 'HORIZON_PREFIX', - Str::slug(env('APP_NAME', 'laravel'), '_') . '_horizon:' - ), - - /* - |-------------------------------------------------------------------------- - | Horizon Route Middleware - |-------------------------------------------------------------------------- - | - | These middleware will get attached onto each Horizon route, giving you - | the chance to add your own middleware to this list or change any of - | the existing middleware. Or, you can simply stick with this list. - | - */ - - 'middleware' => ['admin'], - - /* - |-------------------------------------------------------------------------- - | Queue Wait Time Thresholds - |-------------------------------------------------------------------------- - | - | This option allows you to configure when the LongWaitDetected event - | will be fired. Every connection / queue combination may have its - | own, unique threshold (in seconds) before this event is fired. - | - */ - - 'waits' => [ - 'redis:default' => 60, - ], - - /* - |-------------------------------------------------------------------------- - | Job Trimming Times - |-------------------------------------------------------------------------- - | - | Here you can configure for how long (in minutes) you desire Horizon to - | persist the recent and failed jobs. Typically, recent jobs are kept - | for one hour while all failed jobs are stored for an entire week. - | - */ - - 'trim' => [ - 'recent' => 60, - 'pending' => 60, - 'completed' => 60, - 'recent_failed' => 10080, - 'failed' => 10080, - 'monitored' => 10080, - ], - - /* - |-------------------------------------------------------------------------- - | Metrics - |-------------------------------------------------------------------------- - | - | Here you can configure how many snapshots should be kept to display in - | the metrics graph. This will get used in combination with Horizon's - | `horizon:snapshot` schedule to define how long to retain metrics. - | - */ - - 'metrics' => [ - 'trim_snapshots' => [ - 'job' => 24, - 'queue' => 24, - ], - ], - - /* - |-------------------------------------------------------------------------- - | Fast Termination - |-------------------------------------------------------------------------- - | - | When this option is enabled, Horizon's "terminate" command will not - | wait on all of the workers to terminate unless the --wait option - | is provided. Fast termination can shorten deployment delay by - | allowing a new instance of Horizon to start while the last - | instance will continue to terminate each of its workers. - | - */ - - 'fast_termination' => false, - - /* - |-------------------------------------------------------------------------- - | Memory Limit (MB) - |-------------------------------------------------------------------------- - | - | This value describes the maximum amount of memory the Horizon worker - | may consume before it is terminated and restarted. You should set - | this value according to the resources available to your server. - | - */ - - 'memory_limit' => 256, - - /* - |-------------------------------------------------------------------------- - | Queue Worker Configuration - |-------------------------------------------------------------------------- - | - | Here you may define the queue worker settings used by your application - | in all environments. These supervisors and settings handle all your - | queued jobs and will be provisioned by Horizon during deployment. - | - */ - - 'environments' => [ - 'production' => [ - 'data-pipeline' => [ - 'connection' => 'redis', - 'queue' => ['traffic_fetch', 'stat', 'user_alive_sync'], - 'balance' => 'auto', - 'autoScalingStrategy' => 'time', - 'minProcesses' => 1, - 'maxProcesses' => 8, - 'balanceCooldown' => 1, - 'tries' => 3, - 'timeout' => 30, - ], - 'business' => [ - 'connection' => 'redis', - 'queue' => ['default', 'order_handle'], - 'balance' => 'simple', - 'minProcesses' => 1, - 'maxProcesses' => 3, - 'tries' => 3, - 'timeout' => 30, - ], - 'notification' => [ - 'connection' => 'redis', - 'queue' => ['send_email', 'send_telegram', 'send_email_mass', 'node_sync'], - 'balance' => 'auto', - 'autoScalingStrategy' => 'size', - 'minProcesses' => 1, - 'maxProcesses' => 3, - 'tries' => 3, - 'timeout' => 60, - 'backoff' => [3, 10, 30], - ], - ], - 'local' => [ - 'Xboard' => [ - 'connection' => 'redis', - 'queue' => [ - 'default', - 'order_handle', - 'traffic_fetch', - 'stat', - 'send_email', - 'send_email_mass', - 'send_telegram', - 'user_alive_sync', - 'node_sync' - ], - 'balance' => 'auto', - 'minProcesses' => 1, - 'maxProcesses' => 5, - 'tries' => 1, - 'timeout' => 60, - 'balanceCooldown' => 3, - ], - ], - ], -]; diff --git a/Xboard/config/logging.php b/Xboard/config/logging.php deleted file mode 100644 index 7ec1268..0000000 --- a/Xboard/config/logging.php +++ /dev/null @@ -1,64 +0,0 @@ - env('LOG_CHANNEL', 'daily'), - - 'channels' => [ - 'stack' => [ - 'driver' => 'stack', - 'channels' => ['daily'], - 'ignore_exceptions' => false, - ], - - 'backup' => [ - 'driver' => 'single', - 'path' => storage_path('logs/backup.log'), - 'level' => 'debug', - ], - - 'single' => [ - 'driver' => 'single', - 'path' => storage_path('logs/laravel.log'), - 'level' => env('LOG_LEVEL', 'debug'), - ], - - 'daily' => [ - 'driver' => 'daily', - 'path' => storage_path('logs/laravel.log'), - 'level' => env('LOG_LEVEL', 'debug'), - 'days' => 14, - ], - - 'stderr' => [ - 'driver' => 'monolog', - 'level' => env('LOG_LEVEL', 'debug'), - 'handler' => StreamHandler::class, - 'formatter' => env('LOG_STDERR_FORMATTER'), - 'with' => [ - 'stream' => 'php://stderr', - ], - ], - - 'syslog' => [ - 'driver' => 'syslog', - 'level' => env('LOG_LEVEL', 'debug'), - ], - - 'errorlog' => [ - 'driver' => 'errorlog', - 'level' => env('LOG_LEVEL', 'debug'), - ], - - 'deprecations' => [ - 'driver' => 'daily', - 'path' => storage_path('logs/deprecations.log'), - 'level' => 'debug', - 'days' => 14, - ], - ], - -]; diff --git a/Xboard/config/mail.php b/Xboard/config/mail.php deleted file mode 100644 index 3c65eb3..0000000 --- a/Xboard/config/mail.php +++ /dev/null @@ -1,136 +0,0 @@ - env('MAIL_DRIVER', 'smtp'), - - /* - |-------------------------------------------------------------------------- - | SMTP Host Address - |-------------------------------------------------------------------------- - | - | Here you may provide the host address of the SMTP server used by your - | applications. A default option is provided that is compatible with - | the Mailgun mail service which will provide reliable deliveries. - | - */ - - 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), - - /* - |-------------------------------------------------------------------------- - | SMTP Host Port - |-------------------------------------------------------------------------- - | - | This is the SMTP port used by your application to deliver e-mails to - | users of the application. Like the host we have set this value to - | stay compatible with the Mailgun e-mail application by default. - | - */ - - 'port' => env('MAIL_PORT', 587), - - /* - |-------------------------------------------------------------------------- - | Global "From" Address - |-------------------------------------------------------------------------- - | - | You may wish for all e-mails sent by your application to be sent from - | the same address. Here, you may specify a name and address that is - | used globally for all e-mails that are sent by your application. - | - */ - - 'from' => [ - 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), - 'name' => env('MAIL_FROM_NAME', 'Example'), - ], - - /* - |-------------------------------------------------------------------------- - | E-Mail Encryption Protocol - |-------------------------------------------------------------------------- - | - | Here you may specify the encryption protocol that should be used when - | the application send e-mail messages. A sensible default using the - | transport layer security protocol should provide great security. - | - */ - - 'encryption' => env('MAIL_ENCRYPTION', 'tls'), - - /* - |-------------------------------------------------------------------------- - | SMTP Server Username - |-------------------------------------------------------------------------- - | - | If your SMTP server requires a username for authentication, you should - | set it here. This will get used to authenticate with your server on - | connection. You may also set the "password" value below this one. - | - */ - - 'username' => env('MAIL_USERNAME'), - - 'password' => env('MAIL_PASSWORD'), - - /* - |-------------------------------------------------------------------------- - | Sendmail System Path - |-------------------------------------------------------------------------- - | - | When using the "sendmail" driver to send e-mails, we will need to know - | the path to where Sendmail lives on this server. A default path has - | been provided here, which will work well on most of your systems. - | - */ - - 'sendmail' => '/usr/sbin/sendmail -bs', - - /* - |-------------------------------------------------------------------------- - | Markdown Mail Settings - |-------------------------------------------------------------------------- - | - | If you are using Markdown based email rendering, you may configure your - | theme and component paths here, allowing you to customize the design - | of the emails. Or, you may simply stick with the Laravel defaults! - | - */ - - 'markdown' => [ - 'theme' => 'default', - - 'paths' => [ - resource_path('views/vendor/mail'), - ], - ], - - /* - |-------------------------------------------------------------------------- - | Log Channel - |-------------------------------------------------------------------------- - | - | If you are using the "log" driver, you may specify the logging channel - | if you prefer to keep mail messages separate from other log entries - | for simpler reading. Otherwise, the default channel will be used. - | - */ - - 'log_channel' => env('MAIL_LOG_CHANNEL'), - -]; diff --git a/Xboard/config/octane.php b/Xboard/config/octane.php deleted file mode 100644 index 01190fb..0000000 --- a/Xboard/config/octane.php +++ /dev/null @@ -1,221 +0,0 @@ - env('OCTANE_SERVER', 'swoole'), - - /* - |-------------------------------------------------------------------------- - | Force HTTPS - |-------------------------------------------------------------------------- - | - | When this configuration value is set to "true", Octane will inform the - | framework that all absolute links must be generated using the HTTPS - | protocol. Otherwise your links may be generated using plain HTTP. - | - */ - - 'https' => env('OCTANE_HTTPS', false), - - /* - |-------------------------------------------------------------------------- - | Octane Listeners - |-------------------------------------------------------------------------- - | - | All of the event listeners for Octane's events are defined below. These - | listeners are responsible for resetting your application's state for - | the next request. You may even add your own listeners to the list. - | - */ - - 'listeners' => [ - WorkerStarting::class => [ - EnsureUploadedFilesAreValid::class, - EnsureUploadedFilesCanBeMoved::class, - ], - - RequestReceived::class => [ - ...Octane::prepareApplicationForNextOperation(), - ...Octane::prepareApplicationForNextRequest(), - // - ], - - RequestHandled::class => [ - // - ], - - RequestTerminated::class => [ - FlushUploadedFiles::class, - ], - - TaskReceived::class => [ - ...Octane::prepareApplicationForNextOperation(), - // - ], - - TaskTerminated::class => [ - // - ], - - TickReceived::class => [ - ...Octane::prepareApplicationForNextOperation(), - // - ], - - TickTerminated::class => [ - // - ], - - OperationTerminated::class => [ - FlushTemporaryContainerInstances::class, - DisconnectFromDatabases::class, - CollectGarbage::class, - ], - - WorkerErrorOccurred::class => [ - ReportException::class, - StopWorkerIfNecessary::class, - ], - - WorkerStopping::class => [ - // - ], - ], - - /* - |-------------------------------------------------------------------------- - | Warm / Flush Bindings - |-------------------------------------------------------------------------- - | - | The bindings listed below will either be pre-warmed when a worker boots - | or they will be flushed before every new request. Flushing a binding - | will force the container to resolve that binding again when asked. - | - */ - - 'warm' => [ - ...Octane::defaultServicesToWarm(), - ], - - 'flush' => [ - \App\Services\Plugin\HookManager::class, - ], - - /* - |-------------------------------------------------------------------------- - | Octane Cache Table - |-------------------------------------------------------------------------- - | - | While using Swoole, you may leverage the Octane cache, which is powered - | by a Swoole table. You may set the maximum number of rows as well as - | the number of bytes per row using the configuration options below. - | - */ - - 'cache' => [ - 'rows' => 5000, - 'bytes' => 20000, - ], - - /* - |-------------------------------------------------------------------------- - | Octane Swoole Tables - |-------------------------------------------------------------------------- - | - | While using Swoole, you may define additional tables as required by the - | application. These tables can be used to store data that needs to be - | quickly accessed by other workers on the particular Swoole server. - | - */ - - 'tables' => [ - 'example:1000' => [ - 'name' => 'string:1000', - 'votes' => 'int', - ], - ], - - /* - |-------------------------------------------------------------------------- - | File Watching - |-------------------------------------------------------------------------- - | - | The following list of files and directories will be watched when using - | the --watch option offered by Octane. If any of the directories and - | files are changed, Octane will automatically reload your workers. - | - */ - - 'watch' => [ - 'app', - 'bootstrap', - 'config', - 'database', - 'public/**/*.php', - 'resources/**/*.php', - 'routes', - 'composer.lock', - '.env', - ], - - /* - |-------------------------------------------------------------------------- - | Garbage Collection Threshold - |-------------------------------------------------------------------------- - | - | When executing long-lived PHP scripts such as Octane, memory can build - | up before being cleared by PHP. You can force Octane to run garbage - | collection if your application consumes this amount of megabytes. - | - */ - - 'garbage' => 128, - - /* - |-------------------------------------------------------------------------- - | Maximum Execution Time - |-------------------------------------------------------------------------- - | - | The following setting configures the maximum execution time for requests - | being handled by Octane. You may set this value to 0 to indicate that - | there isn't a specific time limit on Octane request execution time. - | - */ - - 'max_execution_time' => 60, - -]; diff --git a/Xboard/config/queue.php b/Xboard/config/queue.php deleted file mode 100644 index 495c858..0000000 --- a/Xboard/config/queue.php +++ /dev/null @@ -1,88 +0,0 @@ - env('QUEUE_CONNECTION', 'sync'), - - /* - |-------------------------------------------------------------------------- - | Queue Connections - |-------------------------------------------------------------------------- - | - | Here you may configure the connection information for each server that - | is used by your application. A default configuration has been added - | for each back-end shipped with Laravel. You are free to add more. - | - | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" - | - */ - - 'connections' => [ - - 'sync' => [ - 'driver' => 'sync', - ], - - 'database' => [ - 'driver' => 'database', - 'table' => 'jobs', - 'queue' => 'default', - 'retry_after' => 90, - ], - - 'beanstalkd' => [ - 'driver' => 'beanstalkd', - 'host' => 'localhost', - 'queue' => 'default', - 'retry_after' => 90, - 'block_for' => 0, - ], - - 'sqs' => [ - 'driver' => 'sqs', - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), - 'queue' => env('SQS_QUEUE', 'your-queue-name'), - 'region' => env('AWS_V2BOARD_REGION', 'us-east-1'), - ], - - 'redis' => [ - 'driver' => 'redis', - 'connection' => 'default', - 'queue' => env('REDIS_QUEUE', 'default'), - 'retry_after' => 90, - 'block_for' => null, - ], - - ], - - /* - |-------------------------------------------------------------------------- - | Failed Queue Jobs - |-------------------------------------------------------------------------- - | - | These options configure the behavior of failed queue job logging so you - | can control which database and table are used to store the jobs that - | have failed. You may change them to any database / table you wish. - | - */ - - 'failed' => [ - 'driver' => env('QUEUE_FAILED_DRIVER', 'database'), - 'database' => env('DB_CONNECTION', 'mysql'), - 'table' => 'failed_jobs', - ], - -]; diff --git a/Xboard/config/sanctum.php b/Xboard/config/sanctum.php deleted file mode 100644 index 35d75b3..0000000 --- a/Xboard/config/sanctum.php +++ /dev/null @@ -1,83 +0,0 @@ - explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( - '%s%s', - 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', - Sanctum::currentApplicationUrlWithPort() - ))), - - /* - |-------------------------------------------------------------------------- - | Sanctum Guards - |-------------------------------------------------------------------------- - | - | This array contains the authentication guards that will be checked when - | Sanctum is trying to authenticate a request. If none of these guards - | are able to authenticate the request, Sanctum will use the bearer - | token that's present on an incoming request for authentication. - | - */ - - 'guard' => ['web'], - - /* - |-------------------------------------------------------------------------- - | Expiration Minutes - |-------------------------------------------------------------------------- - | - | This value controls the number of minutes until an issued token will be - | considered expired. This will override any values set in the token's - | "expires_at" attribute, but first-party sessions are not affected. - | - */ - - 'expiration' => null, - - /* - |-------------------------------------------------------------------------- - | Token Prefix - |-------------------------------------------------------------------------- - | - | Sanctum can prefix new tokens in order to take advantage of numerous - | security scanning initiatives maintained by open source platforms - | that notify developers if they commit tokens into repositories. - | - | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning - | - */ - - 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), - - /* - |-------------------------------------------------------------------------- - | Sanctum Middleware - |-------------------------------------------------------------------------- - | - | When authenticating your first-party SPA with Sanctum you may need to - | customize some of the middleware Sanctum uses while processing the - | request. You may change the middleware listed below as required. - | - */ - - 'middleware' => [ - 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, - 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, - 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, - ], - -]; diff --git a/Xboard/config/scribe.php b/Xboard/config/scribe.php deleted file mode 100644 index b2e0c03..0000000 --- a/Xboard/config/scribe.php +++ /dev/null @@ -1,270 +0,0 @@ - for the generated documentation. If this is empty, Scribe will infer it from config('app.name'). - 'title' => null, - - // A short description of your API. Will be included in the docs webpage, Postman collection and OpenAPI spec. - 'description' => '', - - // The base URL displayed in the docs. If this is empty, Scribe will use the value of config('app.url') at generation time. - // If you're using `laravel` type, you can set this to a dynamic string, like '{{ config("app.tenant_url") }}' to get a dynamic base URL. - 'base_url' => null, - - 'routes' => [ - [ - // Routes that match these conditions will be included in the docs - 'match' => [ - // Match only routes whose paths match this pattern (use * as a wildcard to match any characters). Example: 'users/*'. - 'prefixes' => ['api/*'], - - // Match only routes whose domains match this pattern (use * as a wildcard to match any characters). Example: 'api.*'. - 'domains' => ['*'], - - // [Dingo router only] Match only routes registered under this version. Wildcards are NOT supported. - 'versions' => ['v1'], - ], - - // Include these routes even if they did not match the rules above. - 'include' => [ - // 'users.index', 'POST /new', '/auth/*' - ], - - // Exclude these routes even if they matched the rules above. - 'exclude' => [ - // 'GET /health', 'admin.*' - ], - ], - ], - - // The type of documentation output to generate. - // - "static" will generate a static HTMl page in the /public/docs folder, - // - "laravel" will generate the documentation as a Blade view, so you can add routing and authentication. - // - "external_static" and "external_laravel" do the same as above, but generate a basic template, - // passing the OpenAPI spec as a URL, allowing you to easily use the docs with an external generator - 'type' => 'static', - - // See https://scribe.knuckles.wtf/laravel/reference/config#theme for supported options - 'theme' => 'default', - - 'static' => [ - // HTML documentation, assets and Postman collection will be generated to this folder. - // Source Markdown will still be in resources/docs. - 'output_path' => 'public/docs', - ], - - 'laravel' => [ - // Whether to automatically create a docs endpoint for you to view your generated docs. - // If this is false, you can still set up routing manually. - 'add_routes' => true, - - // URL path to use for the docs endpoint (if `add_routes` is true). - // By default, `/docs` opens the HTML page, `/docs.postman` opens the Postman collection, and `/docs.openapi` the OpenAPI spec. - 'docs_url' => '/docs', - - // Directory within `public` in which to store CSS and JS assets. - // By default, assets are stored in `public/vendor/scribe`. - // If set, assets will be stored in `public/{{assets_directory}}` - 'assets_directory' => null, - - // Middleware to attach to the docs endpoint (if `add_routes` is true). - 'middleware' => [], - ], - - 'external' => [ - 'html_attributes' => [] - ], - - 'try_it_out' => [ - // Add a Try It Out button to your endpoints so consumers can test endpoints right from their browser. - // Don't forget to enable CORS headers for your endpoints. - 'enabled' => true, - - // The base URL for the API tester to use (for example, you can set this to your staging URL). - // Leave as null to use the current app URL when generating (config("app.url")). - 'base_url' => null, - - // [Laravel Sanctum] Fetch a CSRF token before each request, and add it as an X-XSRF-TOKEN header. - 'use_csrf' => false, - - // The URL to fetch the CSRF token from (if `use_csrf` is true). - 'csrf_url' => '/sanctum/csrf-cookie', - ], - - // How is your API authenticated? This information will be used in the displayed docs, generated examples and response calls. - 'auth' => [ - // Set this to true if ANY endpoints in your API use authentication. - 'enabled' => false, - - // Set this to true if your API should be authenticated by default. If so, you must also set `enabled` (above) to true. - // You can then use @unauthenticated or @authenticated on individual endpoints to change their status from the default. - 'default' => false, - - // Where is the auth value meant to be sent in a request? - // Options: query, body, basic, bearer, header (for custom header) - 'in' => 'bearer', - - // The name of the auth parameter (e.g. token, key, apiKey) or header (e.g. Authorization, Api-Key). - 'name' => 'key', - - // The value of the parameter to be used by Scribe to authenticate response calls. - // This will NOT be included in the generated documentation. If empty, Scribe will use a random value. - 'use_value' => env('SCRIBE_AUTH_KEY'), - - // Placeholder your users will see for the auth parameter in the example requests. - // Set this to null if you want Scribe to use a random value as placeholder instead. - 'placeholder' => '{YOUR_AUTH_KEY}', - - // Any extra authentication-related info for your users. Markdown and HTML are supported. - 'extra_info' => 'You can retrieve your token by visiting your dashboard and clicking Generate API token.', - ], - - // Text to place in the "Introduction" section, right after the `description`. Markdown and HTML are supported. - 'intro_text' => <<As you scroll, you'll see code examples for working with the API in different programming languages in the dark area to the right (or as part of the content on mobile). -You can switch the language used with the tabs at the top right (or from the nav menu at the top left on mobile). -INTRO - , - - // Example requests for each endpoint will be shown in each of these languages. - // Supported options are: bash, javascript, php, python - // To add a language of your own, see https://scribe.knuckles.wtf/laravel/advanced/example-requests - 'example_languages' => [ - 'bash', - 'javascript', - ], - - // Generate a Postman collection (v2.1.0) in addition to HTML docs. - // For 'static' docs, the collection will be generated to public/docs/collection.json. - // For 'laravel' docs, it will be generated to storage/app/scribe/collection.json. - // Setting `laravel.add_routes` to true (above) will also add a route for the collection. - 'postman' => [ - 'enabled' => true, - - 'overrides' => [ - // 'info.version' => '2.0.0', - ], - ], - - // Generate an OpenAPI spec (v3.0.1) in addition to docs webpage. - // For 'static' docs, the collection will be generated to public/docs/openapi.yaml. - // For 'laravel' docs, it will be generated to storage/app/scribe/openapi.yaml. - // Setting `laravel.add_routes` to true (above) will also add a route for the spec. - 'openapi' => [ - 'enabled' => true, - - 'overrides' => [ - // 'info.version' => '2.0.0', - ], - ], - - 'groups' => [ - // Endpoints which don't have a @group will be placed in this default group. - 'default' => 'Endpoints', - - // By default, Scribe will sort groups alphabetically, and endpoints in the order their routes are defined. - // You can override this by listing the groups, subgroups and endpoints here in the order you want them. - // See https://scribe.knuckles.wtf/blog/laravel-v4#easier-sorting and https://scribe.knuckles.wtf/laravel/reference/config#order for details - 'order' => [], - ], - - // Custom logo path. This will be used as the value of the src attribute for the tag, - // so make sure it points to an accessible URL or path. Set to false to not use a logo. - // For example, if your logo is in public/img: - // - 'logo' => '../img/logo.png' // for `static` type (output folder is public/docs) - // - 'logo' => 'img/logo.png' // for `laravel` type - 'logo' => false, - - // Customize the "Last updated" value displayed in the docs by specifying tokens and formats. - // Examples: - // - {date:F j Y} => March 28, 2022 - // - {git:short} => Short hash of the last Git commit - // Available tokens are `{date:}` and `{git:}`. - // The format you pass to `date` will be passed to PHP's `date()` function. - // The format you pass to `git` can be either "short" or "long". - 'last_updated' => 'Last updated: {date:F j, Y}', - - 'examples' => [ - // Set this to any number (e.g. 1234) to generate the same example values for parameters on each run, - 'faker_seed' => null, - - // With API resources and transformers, Scribe tries to generate example models to use in your API responses. - // By default, Scribe will try the model's factory, and if that fails, try fetching the first from the database. - // You can reorder or remove strategies here. - 'models_source' => ['factoryCreate', 'factoryMake', 'databaseFirst'], - ], - - // The strategies Scribe will use to extract information about your routes at each stage. - // If you create or install a custom strategy, add it here. - 'strategies' => [ - 'metadata' => [ - Strategies\Metadata\GetFromDocBlocks::class, - Strategies\Metadata\GetFromMetadataAttributes::class, - ], - 'urlParameters' => [ - Strategies\UrlParameters\GetFromLaravelAPI::class, - Strategies\UrlParameters\GetFromUrlParamAttribute::class, - Strategies\UrlParameters\GetFromUrlParamTag::class, - ], - 'queryParameters' => [ - Strategies\QueryParameters\GetFromFormRequest::class, - Strategies\QueryParameters\GetFromInlineValidator::class, - Strategies\QueryParameters\GetFromQueryParamAttribute::class, - Strategies\QueryParameters\GetFromQueryParamTag::class, - ], - 'headers' => [ - Strategies\Headers\GetFromHeaderAttribute::class, - Strategies\Headers\GetFromHeaderTag::class, - [ - 'override', - [ - 'Content-Type' => 'application/json', - 'Accept' => 'application/json', - ] - ] - ], - 'bodyParameters' => [ - Strategies\BodyParameters\GetFromFormRequest::class, - Strategies\BodyParameters\GetFromInlineValidator::class, - Strategies\BodyParameters\GetFromBodyParamAttribute::class, - Strategies\BodyParameters\GetFromBodyParamTag::class, - ], - 'responses' => [ - Strategies\Responses\UseResponseAttributes::class, - Strategies\Responses\UseTransformerTags::class, - Strategies\Responses\UseApiResourceTags::class, - Strategies\Responses\UseResponseTag::class, - Strategies\Responses\UseResponseFileTag::class, - [ - Strategies\Responses\ResponseCalls::class, - [ - 'only' => ['GET *'], - // Disable debug mode when generating response calls to avoid error stack traces in responses - 'config' => [ - 'app.debug' => false, - ], - ] - ] - ], - 'responseFields' => [ - Strategies\ResponseFields\GetFromResponseFieldAttribute::class, - Strategies\ResponseFields\GetFromResponseFieldTag::class, - ], - ], - - // For response calls, API resource responses and transformer responses, - // Scribe will try to start database transactions, so no changes are persisted to your database. - // Tell Scribe which connections should be transacted here. If you only use one db connection, you can leave this as is. - 'database_connections_to_transact' => [config('database.default')], - - 'fractal' => [ - // If you are using a custom serializer with league/fractal, you can specify it here. - 'serializer' => null, - ], - - 'routeMatcher' => \Knuckles\Scribe\Matching\RouteMatcher::class, -]; diff --git a/Xboard/config/services.php b/Xboard/config/services.php deleted file mode 100644 index 950dc99..0000000 --- a/Xboard/config/services.php +++ /dev/null @@ -1,33 +0,0 @@ - [ - 'domain' => env('MAILGUN_DOMAIN'), - 'secret' => env('MAILGUN_SECRET'), - 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), - ], - - 'postmark' => [ - 'token' => env('POSTMARK_TOKEN'), - ], - - 'ses' => [ - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => env('AWS_V2BOARD_REGION', 'us-east-1'), - ], - -]; diff --git a/Xboard/config/session.php b/Xboard/config/session.php deleted file mode 100644 index 406d50e..0000000 --- a/Xboard/config/session.php +++ /dev/null @@ -1,199 +0,0 @@ - env('SESSION_DRIVER', 'file'), - - /* - |-------------------------------------------------------------------------- - | Session Lifetime - |-------------------------------------------------------------------------- - | - | Here you may specify the number of minutes that you wish the session - | to be allowed to remain idle before it expires. If you want them - | to immediately expire on the browser closing, set that option. - | - */ - - 'lifetime' => env('SESSION_LIFETIME', 120), - - 'expire_on_close' => false, - - /* - |-------------------------------------------------------------------------- - | Session Encryption - |-------------------------------------------------------------------------- - | - | This option allows you to easily specify that all of your session data - | should be encrypted before it is stored. All encryption will be run - | automatically by Laravel and you can use the Session like normal. - | - */ - - 'encrypt' => false, - - /* - |-------------------------------------------------------------------------- - | Session File Location - |-------------------------------------------------------------------------- - | - | When using the native session driver, we need a location where session - | files may be stored. A default has been set for you but a different - | location may be specified. This is only needed for file sessions. - | - */ - - 'files' => storage_path('framework/sessions'), - - /* - |-------------------------------------------------------------------------- - | Session Database Connection - |-------------------------------------------------------------------------- - | - | When using the "database" or "redis" session drivers, you may specify a - | connection that should be used to manage these sessions. This should - | correspond to a connection in your database configuration options. - | - */ - - 'connection' => env('SESSION_CONNECTION', null), - - /* - |-------------------------------------------------------------------------- - | Session Database Table - |-------------------------------------------------------------------------- - | - | When using the "database" session driver, you may specify the table we - | should use to manage the sessions. Of course, a sensible default is - | provided for you; however, you are free to change this as needed. - | - */ - - 'table' => 'sessions', - - /* - |-------------------------------------------------------------------------- - | Session Cache Store - |-------------------------------------------------------------------------- - | - | When using the "apc", "memcached", or "dynamodb" session drivers you may - | list a cache store that should be used for these sessions. This value - | must match with one of the application's configured cache "stores". - | - */ - - 'store' => env('SESSION_STORE', null), - - /* - |-------------------------------------------------------------------------- - | Session Sweeping Lottery - |-------------------------------------------------------------------------- - | - | Some session drivers must manually sweep their storage location to get - | rid of old sessions from storage. Here are the chances that it will - | happen on a given request. By default, the odds are 2 out of 100. - | - */ - - 'lottery' => [2, 100], - - /* - |-------------------------------------------------------------------------- - | Session Cookie Name - |-------------------------------------------------------------------------- - | - | Here you may change the name of the cookie used to identify a session - | instance by ID. The name specified here will get used every time a - | new session cookie is created by the framework for every driver. - | - */ - - 'cookie' => env( - 'SESSION_COOKIE', - Str::slug(env('APP_NAME', 'laravel'), '_') . '_session' - ), - - /* - |-------------------------------------------------------------------------- - | Session Cookie Path - |-------------------------------------------------------------------------- - | - | The session cookie path determines the path for which the cookie will - | be regarded as available. Typically, this will be the root path of - | your application but you are free to change this when necessary. - | - */ - - 'path' => '/', - - /* - |-------------------------------------------------------------------------- - | Session Cookie Domain - |-------------------------------------------------------------------------- - | - | Here you may change the domain of the cookie used to identify a session - | in your application. This will determine which domains the cookie is - | available to in your application. A sensible default has been set. - | - */ - - 'domain' => env('SESSION_DOMAIN', null), - - /* - |-------------------------------------------------------------------------- - | HTTPS Only Cookies - |-------------------------------------------------------------------------- - | - | By setting this option to true, session cookies will only be sent back - | to the server if the browser has a HTTPS connection. This will keep - | the cookie from being sent to you if it can not be done securely. - | - */ - - 'secure' => env('SESSION_SECURE_COOKIE', false), - - /* - |-------------------------------------------------------------------------- - | HTTP Access Only - |-------------------------------------------------------------------------- - | - | Setting this value to true will prevent JavaScript from accessing the - | value of the cookie and the cookie will only be accessible through - | the HTTP protocol. You are free to modify this option if needed. - | - */ - - 'http_only' => true, - - /* - |-------------------------------------------------------------------------- - | Same-Site Cookies - |-------------------------------------------------------------------------- - | - | This option determines how your cookies behave when cross-site requests - | take place, and can be used to mitigate CSRF attacks. By default, we - | do not enable this as other CSRF protection services are in place. - | - | Supported: "lax", "strict" - | - */ - - 'same_site' => null, - -]; diff --git a/Xboard/config/theme/.gitignore b/Xboard/config/theme/.gitignore deleted file mode 100644 index 9b8775c..0000000 --- a/Xboard/config/theme/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.php -!.gitignore diff --git a/Xboard/config/view.php b/Xboard/config/view.php deleted file mode 100644 index 22b8a18..0000000 --- a/Xboard/config/view.php +++ /dev/null @@ -1,36 +0,0 @@ - [ - resource_path('views'), - ], - - /* - |-------------------------------------------------------------------------- - | Compiled View Path - |-------------------------------------------------------------------------- - | - | This option determines where all the compiled Blade templates will be - | stored for your application. Typically, this is within the storage - | directory. However, as usual, you are free to change this value. - | - */ - - 'compiled' => env( - 'VIEW_COMPILED_PATH', - realpath(storage_path('framework/views')) - ), - -]; diff --git a/Xboard/database/.gitignore b/Xboard/database/.gitignore deleted file mode 100644 index 97fc976..0000000 --- a/Xboard/database/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.sqlite -*.sqlite-journal diff --git a/Xboard/database/factories/UserFactory.php b/Xboard/database/factories/UserFactory.php deleted file mode 100644 index 741edea..0000000 --- a/Xboard/database/factories/UserFactory.php +++ /dev/null @@ -1,28 +0,0 @@ -define(User::class, function (Faker $faker) { - return [ - 'name' => $faker->name, - 'email' => $faker->unique()->safeEmail, - 'email_verified_at' => now(), - 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password - 'remember_token' => Str::random(10), - ]; -}); diff --git a/Xboard/database/migrations/2019_08_19_000000_create_failed_jobs_table.php b/Xboard/database/migrations/2019_08_19_000000_create_failed_jobs_table.php deleted file mode 100644 index 510e6e7..0000000 --- a/Xboard/database/migrations/2019_08_19_000000_create_failed_jobs_table.php +++ /dev/null @@ -1,37 +0,0 @@ -bigIncrements('id'); - $table->text('connection'); - $table->text('queue'); - $table->longText('payload'); - $table->longText('exception'); - $table->timestamp('failed_at')->useCurrent(); - }); - } - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::dropIfExists('failed_jobs'); - } -} diff --git a/Xboard/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php b/Xboard/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php deleted file mode 100644 index 542f19b..0000000 --- a/Xboard/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php +++ /dev/null @@ -1,34 +0,0 @@ -id(); - $table->morphs('tokenable'); - $table->string('name'); - $table->string('token', 64)->unique(); - $table->text('abilities')->nullable(); - $table->timestamp('last_used_at')->nullable(); - $table->timestamp('expires_at')->nullable(); - $table->timestamps(); - }); - } - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('personal_access_tokens'); - } -}; diff --git a/Xboard/database/migrations/2023_03_19_000000_create_v2_tables.php b/Xboard/database/migrations/2023_03_19_000000_create_v2_tables.php deleted file mode 100644 index 6fa7080..0000000 --- a/Xboard/database/migrations/2023_03_19_000000_create_v2_tables.php +++ /dev/null @@ -1,496 +0,0 @@ -integer('id', true); - $table->integer('invite_user_id'); - $table->integer('user_id'); - $table->char('trade_no', 36); - $table->integer('order_amount'); - $table->integer('get_amount'); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - } - - // Invite Code - if (!Schema::hasTable('v2_invite_code')) { - Schema::create('v2_invite_code', function (Blueprint $table) { - $table->integer('id', true); - $table->integer('user_id'); - $table->char('code', 32); - $table->boolean('status')->default(false); - $table->integer('pv')->default(0); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - } - - // Knowledge - if (!Schema::hasTable('v2_knowledge')) { - Schema::create('v2_knowledge', function (Blueprint $table) { - $table->integer('id', true); - $table->char('language', 5)->comment('語言'); - $table->string('category')->comment('分類名'); - $table->string('title')->comment('標題'); - $table->text('body')->comment('內容'); - $table->integer('sort')->nullable()->comment('排序'); - $table->boolean('show')->default(false)->comment('顯示'); - $table->integer('created_at')->comment('創建時間'); - $table->integer('updated_at')->comment('更新時間'); - }); - } - - // Plan - if (!Schema::hasTable('v2_plan')) { - Schema::create('v2_plan', function (Blueprint $table) { - $table->integer('id', true); - $table->integer('group_id'); - $table->integer('transfer_enable'); - $table->string('name'); - $table->integer('speed_limit')->nullable(); - $table->boolean('show')->default(false); - $table->integer('sort')->nullable(); - $table->boolean('renew')->default(true); - $table->text('content')->nullable(); - $table->integer('month_price')->nullable(); - $table->integer('quarter_price')->nullable(); - $table->integer('half_year_price')->nullable(); - $table->integer('year_price')->nullable(); - $table->integer('two_year_price')->nullable(); - $table->integer('three_year_price')->nullable(); - $table->integer('onetime_price')->nullable(); - $table->integer('reset_price')->nullable(); - $table->integer('reset_traffic_method')->nullable()->comment('重置流量方式:null跟随系统设置、0每月1号、1按月重置、2不重置、3每年1月1日、4按年重置'); - $table->integer('capacity_limit')->nullable(); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - } - - // Server Group - if (!Schema::hasTable('v2_server_group')) { - Schema::create('v2_server_group', function (Blueprint $table) { - $table->integer('id', true); - $table->string('name'); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - } - - // Server Route - if (!Schema::hasTable('v2_server_route')) { - Schema::create('v2_server_route', function (Blueprint $table) { - $table->integer('id', true); - $table->string('remarks'); - $table->text('match'); - $table->string('action', 11); - $table->string('action_value')->nullable(); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - } - - // stat server - if (!Schema::hasTable('v2_stat_server')) { - Schema::create('v2_stat_server', function (Blueprint $table) { - $table->integer('id', true); - $table->integer('server_id')->index('server_id')->comment('节点id'); - $table->char('server_type', 11)->comment('节点类型'); - $table->bigInteger('u'); - $table->bigInteger('d'); - $table->char('record_type', 1)->comment('d day m month'); - $table->integer('record_at')->index('record_at')->comment('记录时间'); - $table->integer('created_at'); - $table->integer('updated_at'); - - $table->unique(['server_id', 'server_type', 'record_at'], 'server_id_server_type_record_at'); - }); - } - - // User - if (!Schema::hasTable('v2_user')) { - Schema::create('v2_user', function (Blueprint $table) { - $table->integer('id', true); - $table->integer('invite_user_id')->nullable(); - $table->bigInteger('telegram_id')->nullable(); - $table->string('email', 64)->unique('email'); - $table->string('password', 64); - $table->char('password_algo', 10)->nullable(); - $table->char('password_salt', 10)->nullable(); - $table->integer('balance')->default(0); - $table->integer('discount')->nullable(); - $table->tinyInteger('commission_type')->default(0)->comment('0: system 1: period 2: onetime'); - $table->integer('commission_rate')->nullable(); - $table->integer('commission_balance')->default(0); - $table->integer('t')->default(0); - $table->bigInteger('u')->default(0); - $table->bigInteger('d')->default(0); - $table->bigInteger('transfer_enable')->default(0); - $table->boolean('banned')->default(false); - $table->boolean('is_admin')->default(false); - $table->integer('last_login_at')->nullable(); - $table->boolean('is_staff')->default(false); - $table->integer('last_login_ip')->nullable(); - $table->string('uuid', 36); - $table->integer('group_id')->nullable(); - $table->integer('plan_id')->nullable(); - $table->integer('speed_limit')->nullable(); - $table->tinyInteger('remind_expire')->nullable()->default(1); - $table->tinyInteger('remind_traffic')->nullable()->default(1); - $table->char('token', 32); - $table->bigInteger('expired_at')->nullable()->default(0); - $table->text('remarks')->nullable(); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - } - - // Mail Log - if (!Schema::hasTable('v2_mail_log')) { - Schema::create('v2_mail_log', function (Blueprint $table) { - $table->integer('id', true); - $table->string('email', 64); - $table->string('subject'); - $table->string('template_name'); - $table->text('error')->nullable(); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - } - - // Log - if (!Schema::hasTable('v2_log')) { - Schema::create('v2_log', function (Blueprint $table) { - $table->integer('id', true); - $table->text('title'); - $table->string('level', 11)->nullable(); - $table->string('host')->nullable(); - $table->string('uri'); - $table->string('method', 11); - $table->text('data')->nullable(); - $table->string('ip', 128)->nullable(); - $table->text('context')->nullable(); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - } - - // Stat - if (!Schema::hasTable('v2_stat')) { - Schema::create('v2_stat', function (Blueprint $table) { - $table->integer('id', true); - $table->integer('record_at'); - $table->char('record_type', 1); - $table->integer('order_count')->comment('订单数量'); - $table->integer('order_total')->comment('订单合计'); - $table->integer('commission_count'); - $table->integer('commission_total')->comment('佣金合计'); - $table->integer('paid_count'); - $table->integer('paid_total'); - $table->integer('register_count'); - $table->integer('invite_count'); - $table->string('transfer_used_total', 32); - $table->integer('created_at'); - $table->integer('updated_at'); - - if (config('database.default') !== 'sqlite') { - $table->unique(['record_at']); - } - }); - } - - // stat user - if (!Schema::hasTable('v2_stat_user')) { - Schema::create('v2_stat_user', function (Blueprint $table) { - $table->integer('id', true); - $table->integer('user_id'); - $table->decimal('server_rate', 10); - $table->bigInteger('u'); - $table->bigInteger('d'); - $table->char('record_type', 2); - $table->integer('record_at'); - $table->integer('created_at'); - $table->integer('updated_at'); - - // 如果是不是sqlite才添加多个索引 - if (config('database.default') !== 'sqlite') { - $table->index(['user_id', 'server_rate', 'record_at']); - $table->unique(['server_rate', 'user_id', 'record_at'], 'server_rate_user_id_record_at'); - } - }); - } - - // ticket message - if (!Schema::hasTable('v2_ticket_message')) { - Schema::create('v2_ticket_message', function (Blueprint $table) { - $table->integer('id', true); - $table->integer('user_id'); - $table->integer('ticket_id'); - $table->text('message'); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - } - - // Order - if (!Schema::hasTable('v2_order')) { - Schema::create('v2_order', function (Blueprint $table) { - $table->integer('id', true); - $table->integer('invite_user_id')->nullable(); - $table->integer('user_id'); - $table->integer('plan_id'); - $table->integer('coupon_id')->nullable(); - $table->integer('payment_id')->nullable(); - $table->integer('type')->comment('1新购2续费3升级'); - $table->string('period'); - $table->string('trade_no', 36)->unique('trade_no'); - $table->string('callback_no')->nullable(); - $table->integer('total_amount'); - $table->integer('handling_amount')->nullable(); - $table->integer('discount_amount')->nullable(); - $table->integer('surplus_amount')->nullable()->comment('剩余价值'); - $table->integer('refund_amount')->nullable()->comment('退款金额'); - $table->integer('balance_amount')->nullable()->comment('使用余额'); - $table->text('surplus_order_ids')->nullable()->comment('折抵订单'); - $table->integer('status')->default(0)->comment('0待支付1开通中2已取消3已完成4已折抵'); - $table->integer('commission_status')->default(false)->comment('0待确认1发放中2有效3无效'); - $table->integer('commission_balance')->default(0); - $table->integer('actual_commission_balance')->nullable()->comment('实际支付佣金'); - $table->integer('paid_at')->nullable(); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - } - - // Payment - if (!Schema::hasTable('v2_payment')) { - Schema::create('v2_payment', function (Blueprint $table) { - $table->integer('id', true); - $table->char('uuid', 32); - $table->string('payment', 16); - $table->string('name'); - $table->string('icon')->nullable(); - $table->text('config'); - $table->string('notify_domain', 128)->nullable(); - $table->integer('handling_fee_fixed')->nullable(); - $table->decimal('handling_fee_percent', 5)->nullable(); - $table->boolean('enable')->default(false); - $table->integer('sort')->nullable(); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - } - - // Coupon - if (!Schema::hasTable('v2_coupon')) { - Schema::create('v2_coupon', function (Blueprint $table) { - $table->integer('id', true); - $table->string('code'); - $table->string('name'); - $table->integer('type'); - $table->integer('value'); - $table->boolean('show')->default(false); - $table->integer('limit_use')->nullable(); - $table->integer('limit_use_with_user')->nullable(); - $table->string('limit_plan_ids')->nullable(); - $table->string('limit_period')->nullable(); - $table->integer('started_at'); - $table->integer('ended_at'); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - } - - // Notice - if (!Schema::hasTable('v2_notice')) { - Schema::create('v2_notice', function (Blueprint $table) { - $table->integer('id', true); - $table->string('title'); - $table->text('content'); - $table->boolean('show')->default(false); - $table->string('img_url')->nullable(); - $table->string('tags')->nullable(); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - } - - // Ticket - if (!Schema::hasTable('v2_ticket')) { - Schema::create('v2_ticket', function (Blueprint $table) { - $table->integer('id', true); - $table->integer('user_id'); - $table->string('subject'); - $table->integer('level'); - $table->integer('status')->default(0)->comment('0:已开启 1:已关闭'); - $table->integer('reply_status')->default(1)->comment('0:待回复 1:已回复'); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - } - - // Server Hysteria - if (!Schema::hasTable('v2_server_hysteria')) { - Schema::create('v2_server_hysteria', function (Blueprint $table) { - $table->integer('id', true); - $table->string('group_id'); - $table->string('route_id')->nullable(); - $table->string('name'); - $table->integer('parent_id')->nullable(); - $table->string('host'); - $table->string('port', 11); - $table->integer('server_port'); - $table->string('tags')->nullable(); - $table->string('rate', 11); - $table->boolean('show')->default(false); - $table->integer('sort')->nullable(); - $table->integer('up_mbps'); - $table->integer('down_mbps'); - $table->string('server_name', 64)->nullable(); - $table->boolean('insecure')->default(false); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - } - - // Server Shadowsocks - if (!Schema::hasTable('v2_server_shadowsocks')) { - autoIncrement: - Schema::create('v2_server_shadowsocks', function (Blueprint $table) { - $table->integer('id', true); - $table->string('group_id'); - $table->string('route_id')->nullable(); - $table->integer('parent_id')->nullable(); - $table->string('tags')->nullable(); - $table->string('name'); - $table->string('rate', 11); - $table->string('host'); - $table->string('port', 11); - $table->integer('server_port'); - $table->string('cipher'); - $table->char('obfs', 11)->nullable(); - $table->string('obfs_settings')->nullable(); - $table->tinyInteger('show')->default(0); - $table->integer('sort')->nullable(); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - } - // Server Trojan - if (!Schema::hasTable('v2_server_trojan')) { - Schema::create('v2_server_trojan', function (Blueprint $table) { - $table->integer('id', true)->comment('节点ID'); - $table->string('group_id')->comment('节点组'); - $table->string('route_id')->nullable(); - $table->integer('parent_id')->nullable()->comment('父节点'); - $table->string('tags')->nullable()->comment('节点标签'); - $table->string('name')->comment('节点名称'); - $table->string('rate', 11)->comment('倍率'); - $table->string('host')->comment('主机名'); - $table->string('port', 11)->comment('连接端口'); - $table->integer('server_port')->comment('服务端口'); - $table->boolean('allow_insecure')->default(false)->comment('是否允许不安全'); - $table->string('server_name')->nullable(); - $table->boolean('show')->default(false)->comment('是否显示'); - $table->integer('sort')->nullable(); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - } - - // Server Vless - if (!Schema::hasTable('v2_server_vless')) { - Schema::create('v2_server_vless', function (Blueprint $table) { - $table->integer('id', true); - $table->text('group_id'); - $table->text('route_id')->nullable(); - $table->string('name'); - $table->integer('parent_id')->nullable(); - $table->string('host'); - $table->integer('port'); - $table->integer('server_port'); - $table->integer('tls'); - $table->text('tls_settings')->nullable(); - $table->string('flow', 64)->nullable(); - $table->string('network', 11); - $table->text('network_settings')->nullable(); - $table->text('tags')->nullable(); - $table->string('rate', 11); - $table->boolean('show')->default(false); - $table->integer('sort')->nullable(); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - } - - // Server Vmess - if (!Schema::hasTable('v2_server_vmess')) { - Schema::create('v2_server_vmess', function (Blueprint $table) { - $table->integer('id', true); - $table->string('group_id'); - $table->string('route_id')->nullable(); - $table->string('name'); - $table->integer('parent_id')->nullable(); - $table->string('host'); - $table->string('port', 11); - $table->integer('server_port'); - $table->tinyInteger('tls')->default(0); - $table->string('tags')->nullable(); - $table->string('rate', 11); - $table->string('network', 11); - $table->text('rules')->nullable(); - $table->text('networkSettings')->nullable(); - $table->text('tlsSettings')->nullable(); - $table->text('ruleSettings')->nullable(); - $table->text('dnsSettings')->nullable(); - $table->boolean('show')->default(false); - $table->integer('sort')->nullable(); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - } - - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('v2_commission_log'); - Schema::dropIfExists('v2_plan'); - Schema::dropIfExists('v2_user'); - Schema::dropIfExists('v2_mail_log'); - Schema::dropIfExists('v2_log'); - Schema::dropIfExists('v2_stat'); - Schema::dropIfExists('v2_order'); - Schema::dropIfExists('v2_coupon'); - Schema::dropIfExists('v2_notice'); - Schema::dropIfExists('v2_ticket'); - Schema::dropIfExists('v2_settings'); - Schema::dropIfExists('v2_ticket_message'); - Schema::dropIfExists('v2_invite_code'); - Schema::dropIfExists('v2_knowledge'); - Schema::dropIfExists('v2_server_group'); - Schema::dropIfExists('v2_server_route'); - Schema::dropIfExists('v2_stat_server'); - Schema::dropIfExists('v2_stat_user'); - Schema::dropIfExists('v2_server_hysteria'); - Schema::dropIfExists('v2_server_shadowsocks'); - Schema::dropIfExists('v2_server_trojan'); - Schema::dropIfExists('v2_server_vless'); - Schema::dropIfExists('v2_server_vmess'); - } -}; diff --git a/Xboard/database/migrations/2023_08_14_221234_create_v2_settings_table.php b/Xboard/database/migrations/2023_08_14_221234_create_v2_settings_table.php deleted file mode 100644 index b6e788b..0000000 --- a/Xboard/database/migrations/2023_08_14_221234_create_v2_settings_table.php +++ /dev/null @@ -1,35 +0,0 @@ -id(); - $table->string('group')->comment('设置分组')->nullable(); - $table->string('type')->comment('设置类型')->nullable(); - $table->string('name')->comment('设置名称')->unique(); - $table->string('value')->comment('设置值')->nullable(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::dropIfExists('v2_settings'); - } -} diff --git a/Xboard/database/migrations/2023_09_04_190923_add_column_excludes_to_server_table.php b/Xboard/database/migrations/2023_09_04_190923_add_column_excludes_to_server_table.php deleted file mode 100644 index ef1038e..0000000 --- a/Xboard/database/migrations/2023_09_04_190923_add_column_excludes_to_server_table.php +++ /dev/null @@ -1,56 +0,0 @@ -text("excludes")->nullable()->after('tags'); - }); - Schema::table('v2_server_shadowsocks', function (Blueprint $table) { - $table->text("excludes")->nullable()->after('tags'); - }); - Schema::table('v2_server_trojan', function (Blueprint $table) { - $table->text("excludes")->nullable()->after('tags'); - }); - Schema::table('v2_server_vless', function (Blueprint $table) { - $table->text("excludes")->nullable()->after('tags'); - }); - Schema::table('v2_server_vmess', function (Blueprint $table) { - $table->text("excludes")->nullable()->after('tags'); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::table('v2_server_hysteria', function (Blueprint $table) { - $table->dropColumn('excludes'); - }); - Schema::table('v2_server_shadowsocks', function (Blueprint $table) { - $table->dropColumn('excludes'); - }); - Schema::table('v2_server_trojan', function (Blueprint $table) { - $table->dropColumn('excludes'); - }); - Schema::table('v2_server_vless', function (Blueprint $table) { - $table->dropColumn('excludes'); - }); - Schema::table('v2_server_vmess', function (Blueprint $table) { - $table->dropColumn('excludes'); - }); - } -} diff --git a/Xboard/database/migrations/2023_09_06_195956_add_column_ips_to_server_table.php b/Xboard/database/migrations/2023_09_06_195956_add_column_ips_to_server_table.php deleted file mode 100644 index be71dc9..0000000 --- a/Xboard/database/migrations/2023_09_06_195956_add_column_ips_to_server_table.php +++ /dev/null @@ -1,56 +0,0 @@ -string("ips")->nullable()->after('excludes'); - }); - Schema::table('v2_server_shadowsocks', function (Blueprint $table) { - $table->string("ips")->nullable()->after('excludes'); - }); - Schema::table('v2_server_trojan', function (Blueprint $table) { - $table->string("ips")->nullable()->after('excludes'); - }); - Schema::table('v2_server_vless', function (Blueprint $table) { - $table->string("ips")->nullable()->after('excludes'); - }); - Schema::table('v2_server_vmess', function (Blueprint $table) { - $table->string("ips")->nullable()->after('excludes'); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::table('v2_server_hysteria', function (Blueprint $table) { - $table->dropColumn('ips'); - }); - Schema::table('v2_server_shadowsocks', function (Blueprint $table) { - $table->dropColumn('ips'); - }); - Schema::table('v2_server_trojan', function (Blueprint $table) { - $table->dropColumn('ips'); - }); - Schema::table('v2_server_vless', function (Blueprint $table) { - $table->dropColumn('ips'); - }); - Schema::table('v2_server_vmess', function (Blueprint $table) { - $table->dropColumn('ips'); - }); - } -} diff --git a/Xboard/database/migrations/2023_09_14_013244_add_column_alpn_to_server_hysteria_table.php b/Xboard/database/migrations/2023_09_14_013244_add_column_alpn_to_server_hysteria_table.php deleted file mode 100644 index 332666f..0000000 --- a/Xboard/database/migrations/2023_09_14_013244_add_column_alpn_to_server_hysteria_table.php +++ /dev/null @@ -1,32 +0,0 @@ -tinyInteger('alpn',false,true)->default(0)->comment('ALPN,0:hysteria、1:http/1.1、2:h2、3:h3'); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::table('v2_server_hysteria', function (Blueprint $table) { - $table->dropColumn('alpn'); - }); - } -} diff --git a/Xboard/database/migrations/2023_09_24_040317_add_column_network_and_network_settings_to_v2_server_trojan.php b/Xboard/database/migrations/2023_09_24_040317_add_column_network_and_network_settings_to_v2_server_trojan.php deleted file mode 100644 index 529cf68..0000000 --- a/Xboard/database/migrations/2023_09_24_040317_add_column_network_and_network_settings_to_v2_server_trojan.php +++ /dev/null @@ -1,33 +0,0 @@ -string('network', 11)->default('tcp')->after('server_name')->comment('传输协议'); - $table->text('networkSettings')->nullable()->after('network')->comment('传输协议配置'); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::table('v2_server_trojan', function (Blueprint $table) { - $table->dropColumn(["network","networkSettings"]); - }); - } -} diff --git a/Xboard/database/migrations/2023_09_29_044957_add_column_version_and_is_obfs_to_server_hysteria_table.php b/Xboard/database/migrations/2023_09_29_044957_add_column_version_and_is_obfs_to_server_hysteria_table.php deleted file mode 100644 index c2f7762..0000000 --- a/Xboard/database/migrations/2023_09_29_044957_add_column_version_and_is_obfs_to_server_hysteria_table.php +++ /dev/null @@ -1,33 +0,0 @@ -tinyInteger('version',false,true)->default(1)->comment('hysteria版本,Version:1\2'); - $table->boolean('is_obfs')->default(true)->comment('是否开启obfs'); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::table('v2_server_hysteria', function (Blueprint $table) { - $table->dropColumn('version','is_obfs'); - }); - } -} diff --git a/Xboard/database/migrations/2023_11_19_205026_change_column_value_to_v2_settings_table.php b/Xboard/database/migrations/2023_11_19_205026_change_column_value_to_v2_settings_table.php deleted file mode 100644 index 97ad5d6..0000000 --- a/Xboard/database/migrations/2023_11_19_205026_change_column_value_to_v2_settings_table.php +++ /dev/null @@ -1,28 +0,0 @@ -text('value')->comment('设置值')->nullable()->change(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('v2_settings', function (Blueprint $table) { - $table->string('value')->comment('设置值')->nullable()->change(); - }); - } -}; diff --git a/Xboard/database/migrations/2023_12_12_212239_add_index_to_v2_user_table.php b/Xboard/database/migrations/2023_12_12_212239_add_index_to_v2_user_table.php deleted file mode 100644 index b06cbd3..0000000 --- a/Xboard/database/migrations/2023_12_12_212239_add_index_to_v2_user_table.php +++ /dev/null @@ -1,28 +0,0 @@ -index(['u','d','expired_at','group_id','banned','transfer_enable']); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('v2_user', function (Blueprint $table) { - $table->dropIndex(['u','d','expired_at','group_id','banned','transfer_enable']); - }); - } -}; diff --git a/Xboard/database/migrations/2024_03_19_103149_modify_icon_column_to_v2_payment_table.php b/Xboard/database/migrations/2024_03_19_103149_modify_icon_column_to_v2_payment_table.php deleted file mode 100644 index 3d129e7..0000000 --- a/Xboard/database/migrations/2024_03_19_103149_modify_icon_column_to_v2_payment_table.php +++ /dev/null @@ -1,28 +0,0 @@ -text('icon')->nullable()->change(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('v2_payment', function (Blueprint $table) { - $table->string('icon')->nullable()->change(); - }); - } -}; diff --git a/Xboard/database/migrations/2025_01_01_130644_modify_commission_status_in_v2_order_table.php b/Xboard/database/migrations/2025_01_01_130644_modify_commission_status_in_v2_order_table.php deleted file mode 100644 index 916e226..0000000 --- a/Xboard/database/migrations/2025_01_01_130644_modify_commission_status_in_v2_order_table.php +++ /dev/null @@ -1,28 +0,0 @@ -integer('commission_status')->nullable()->default(null)->comment('0待确认1发放中2有效3无效')->change(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('v2_order', function (Blueprint $table) { - $table->integer('commission_status')->default(false)->comment('0待确认1发放中2有效3无效')->change(); - }); - } -}; diff --git a/Xboard/database/migrations/2025_01_04_optimize_plan_table.php b/Xboard/database/migrations/2025_01_04_optimize_plan_table.php deleted file mode 100644 index d6b1f47..0000000 --- a/Xboard/database/migrations/2025_01_04_optimize_plan_table.php +++ /dev/null @@ -1,129 +0,0 @@ -json('prices')->nullable()->after('name') - ->comment('Store different duration prices and reset traffic price'); - $table->boolean('sell')->default(false)->after('prices')->comment('is sell'); - }); - - // Step 2: Migrate data to new format - DB::table('v2_plan')->orderBy('id')->chunk(100, function ($plans) { - foreach ($plans as $plan) { - $prices = array_filter([ - 'monthly' => $plan->month_price !== null ? $plan->month_price / 100 : null, - 'quarterly' => $plan->quarter_price !== null ? $plan->quarter_price / 100 : null, - 'half_yearly' => $plan->half_year_price !== null ? $plan->half_year_price / 100 : null, - 'yearly' => $plan->year_price !== null ? $plan->year_price / 100 : null, - 'two_yearly' => $plan->two_year_price !== null ? $plan->two_year_price / 100 : null, - 'three_yearly' => $plan->three_year_price !== null ? $plan->three_year_price / 100 : null, - 'onetime' => $plan->onetime_price !== null ? $plan->onetime_price / 100 : null, - 'reset_traffic' => $plan->reset_price !== null ? $plan->reset_price / 100 : null - ], function ($price) { - return $price !== null; - }); - - DB::table('v2_plan') - ->where('id', $plan->id) - ->update([ - 'prices' => json_encode($prices), - 'sell' => $plan->show - ]); - } - }); - - // Step 3: Optimize existing columns - Schema::table('v2_plan', function (Blueprint $table) { - // Modify existing columns to be more efficient - $table->unsignedInteger('group_id')->nullable()->change(); - $table->unsignedBigInteger('transfer_enable')->nullable() - ->comment('Transfer limit in bytes')->change(); - $table->unsignedInteger('speed_limit')->nullable() - ->comment('Speed limit in Mbps, 0 for unlimited')->change(); - $table->integer('reset_traffic_method')->nullable()->default(0) - ->comment('重置流量方式:null跟随系统设置、0每月1号、1按月重置、2不重置、3每年1月1日、4按年重置')->change(); - $table->unsignedInteger('capacity_limit')->nullable()->default(0) - ->comment('0 for unlimited')->change(); - }); - - // Step 4: Drop old columns - Schema::table('v2_plan', function (Blueprint $table) { - $table->dropColumn([ - 'month_price', - 'quarter_price', - 'half_year_price', - 'year_price', - 'two_year_price', - 'three_year_price', - 'onetime_price', - 'reset_price', - ]); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - // Step 1: Add back old columns - Schema::table('v2_plan', function (Blueprint $table) { - $table->integer('month_price')->nullable(); - $table->integer('quarter_price')->nullable(); - $table->integer('half_year_price')->nullable(); - $table->integer('year_price')->nullable(); - $table->integer('two_year_price')->nullable(); - $table->integer('three_year_price')->nullable(); - $table->integer('onetime_price')->nullable(); - $table->integer('reset_price')->nullable(); - }); - - // Step 2: Restore data from new format to old format - DB::table('v2_plan')->orderBy('id')->chunk(100, function ($plans) { - foreach ($plans as $plan) { - $prices = json_decode($plan->prices, true) ?? []; - - DB::table('v2_plan') - ->where('id', $plan->id) - ->update([ - 'month_price' => $prices['monthly'] * 100 ?? null, - 'quarter_price' => $prices['quarterly'] * 100 ?? null, - 'half_year_price' => $prices['half_yearly'] * 100 ?? null, - 'year_price' => $prices['yearly'] * 100 ?? null, - 'two_year_price' => $prices['two_yearly'] * 100 ?? null, - 'three_year_price' => $prices['three_yearly'] * 100 ?? null, - 'onetime_price' => $prices['onetime'] * 100 ?? null, - 'reset_price' => $prices['reset_traffic'] * 100 ?? null, - ]); - } - }); - - // Step 3: Drop new columns - Schema::table('v2_plan', function (Blueprint $table) { - $table->dropColumn([ - 'prices', - 'sell' - ]); - }); - - // Step 4: Restore column types to original - Schema::table('v2_plan', function (Blueprint $table) { - $table->integer('group_id')->change(); - $table->integer('transfer_enable')->change(); - $table->integer('speed_limit')->nullable()->change(); - $table->integer('reset_traffic_method')->nullable()->change(); - $table->integer('capacity_limit')->nullable()->change(); - }); - } -}; \ No newline at end of file diff --git a/Xboard/database/migrations/2025_01_05_131425_create_v2_server_table.php b/Xboard/database/migrations/2025_01_05_131425_create_v2_server_table.php deleted file mode 100644 index f8fb84b..0000000 --- a/Xboard/database/migrations/2025_01_05_131425_create_v2_server_table.php +++ /dev/null @@ -1,523 +0,0 @@ -id('id'); - $table->string('type')->comment('Server Type'); - $table->string('code')->nullable()->comment('Server Spectific Key'); - $table->unsignedInteger('parent_id')->nullable()->comment('Parent Server ID'); - $table->json('group_ids')->nullable()->comment('Group ID'); - $table->json('route_ids')->nullable()->comment('Route ID'); - $table->string('name')->comment('Server Name'); - $table->decimal('rate', 8, 2)->comment('Traffic Rate'); - $table->json('tags')->nullable()->comment('Server Tags'); - $table->string('host')->comment('Server Host'); - $table->string('port')->comment('Client Port'); - $table->integer('server_port')->comment('Server Port'); - $table->json('protocol_settings')->nullable(); - $table->boolean('show')->default(false)->comment('Show in List'); - $table->integer('sort')->nullable()->unsigned()->index(); - $table->timestamps(); - $table->unique(['type', 'code']); - }); - - // Migrate Trojan servers - $trojanServers = DB::table('v2_server_trojan')->get(); - foreach ($trojanServers as $server) { - DB::table('v2_server')->insert([ - 'type' => 'trojan', - 'code' => (string) $server->id, - 'parent_id' => $server->parent_id, - 'group_ids' => $server->group_id ?: "[]", - 'route_ids' => $server->route_id ?: "[]", - 'name' => $server->name, - 'rate' => $server->rate, - 'tags' => $server->tags ?: "[]", - 'host' => $server->host, - 'port' => $server->port, - 'server_port' => $server->server_port, - 'protocol_settings' => json_encode([ - 'allow_insecure' => $server->allow_insecure, - 'server_name' => $server->server_name, - 'network' => $server->network, - 'network_settings' => $server->networkSettings - ]), - 'show' => $server->show, - 'sort' => $server->sort, - 'created_at' => date('Y-m-d H:i:s', $server->created_at), - 'updated_at' => date('Y-m-d H:i:s', $server->updated_at) - ]); - } - - // Migrate VMess servers - $vmessServers = DB::table('v2_server_vmess')->get(); - foreach ($vmessServers as $server) { - DB::table('v2_server')->insert([ - 'type' => 'vmess', - 'code' => (string) $server->id, - 'parent_id' => $server->parent_id, - 'group_ids' => $server->group_id ?: "[]", - 'route_ids' => $server->route_id ?: "[]", - 'name' => $server->name, - 'rate' => $server->rate, - 'tags' => $server->tags ?: "[]", - 'host' => $server->host, - 'port' => $server->port, - 'server_port' => $server->server_port, - 'protocol_settings' => json_encode([ - 'tls' => $server->tls, - 'network' => $server->network, - 'rules' => json_decode($server->rules), - 'network_settings' => json_decode($server->networkSettings), - 'tls_settings' => json_decode($server->tlsSettings), - ]), - 'show' => $server->show, - 'sort' => $server->sort, - 'created_at' => date('Y-m-d H:i:s', $server->created_at), - 'updated_at' => date('Y-m-d H:i:s', $server->updated_at) - ]); - } - - // Migrate VLESS servers - $vlessServers = DB::table('v2_server_vless')->get(); - foreach ($vlessServers as $server) { - $tlsSettings = optional(json_decode($server->tls_settings)); - DB::table('v2_server')->insert([ - 'type' => 'vless', - 'code' => (string) $server->id, - 'parent_id' => $server->parent_id, - 'group_ids' => $server->group_id ?: "[]", - 'route_ids' => $server->route_id ?: "[]", - 'name' => $server->name, - 'rate' => $server->rate, - 'tags' => $server->tags ?: "[]", - 'host' => $server->host, - 'port' => $server->port, - 'server_port' => $server->server_port, - 'protocol_settings' => json_encode([ - 'tls' => $server->tls, - 'tls_settings' => $tlsSettings, - 'flow' => $server->flow, - 'network' => $server->network, - 'network_settings' => json_decode($server->network_settings), - 'reality_settings' => ($tlsSettings && $tlsSettings->public_key && $tlsSettings->short_id && $tlsSettings->server_name) ? [ - 'public_key' => $tlsSettings->public_key, - 'short_id' => $tlsSettings->short_id, - 'server_name' => $tlsSettings->server_name, - 'server_port' => $tlsSettings->server_port, - 'private_key' => $tlsSettings->private_key, - ] : null - ]), - 'show' => $server->show, - 'sort' => $server->sort, - 'created_at' => date('Y-m-d H:i:s', $server->created_at), - 'updated_at' => date('Y-m-d H:i:s', $server->updated_at) - ]); - } - - // Migrate Shadowsocks servers - $ssServers = DB::table('v2_server_shadowsocks')->get(); - foreach ($ssServers as $server) { - DB::table('v2_server')->insert([ - 'type' => 'shadowsocks', - 'code' => (string) $server->id, - 'parent_id' => $server->parent_id, - 'group_ids' => $server->group_id ?: "[]", - 'route_ids' => $server->route_id ?: "[]", - 'name' => $server->name, - 'rate' => $server->rate, - 'tags' => $server->tags ?: "[]", - 'host' => $server->host, - 'port' => $server->port, - 'server_port' => $server->server_port, - 'protocol_settings' => json_encode([ - 'cipher' => $server->cipher, - 'obfs' => $server->obfs, - 'obfs_settings' => json_decode($server->obfs_settings) - ]), - 'show' => (bool) $server->show, - 'sort' => $server->sort, - 'created_at' => date('Y-m-d H:i:s', $server->created_at), - 'updated_at' => date('Y-m-d H:i:s', $server->updated_at) - ]); - } - - // Migrate Hysteria servers - $hysteriaServers = DB::table(table: 'v2_server_hysteria')->get(); - foreach ($hysteriaServers as $server) { - DB::table('v2_server')->insert([ - 'type' => 'hysteria', - 'code' => (string) $server->id, - 'parent_id' => $server->parent_id, - 'group_ids' => $server->group_id ?: "[]", - 'route_ids' => $server->route_id ?: "[]", - 'name' => $server->name, - 'rate' => $server->rate, - 'tags' => $server->tags ?: "[]", - 'host' => $server->host, - 'port' => $server->port, - 'server_port' => $server->server_port, - 'protocol_settings' => json_encode([ - 'version' => $server->version, - 'bandwidth' => [ - 'up' => $server->up_mbps, - 'down' => $server->down_mbps, - ], - 'obfs' => [ - 'open' => $server->is_obfs, - 'type' => 'salamander', - 'password' => Helper::getServerKey($server->created_at, 16), - ], - 'tls' => [ - 'server_name' => $server->server_name, - 'allow_insecure' => $server->insecure - ] - ]), - 'show' => $server->show, - 'sort' => $server->sort, - 'created_at' => date('Y-m-d H:i:s', $server->created_at), - 'updated_at' => date('Y-m-d H:i:s', $server->updated_at) - ]); - } - - // Update parent_id for all servers - $this->updateParentIds(); - - // Drop old tables - Schema::dropIfExists('v2_server_trojan'); - Schema::dropIfExists('v2_server_vmess'); - Schema::dropIfExists('v2_server_vless'); - Schema::dropIfExists('v2_server_shadowsocks'); - Schema::dropIfExists('v2_server_hysteria'); - } - - /** - * Update parent_id references for all servers - */ - private function updateParentIds(): void - { - // Get all servers that have a parent_id - $servers = DB::table('v2_server') - ->whereNotNull('parent_id') - ->get(); - - // Update each server's parent_id to reference the new table's id - foreach ($servers as $server) { - $parentId = DB::table('v2_server') - ->where('type', $server->type) - ->where('code', $server->parent_id) - ->value('id'); - - if ($parentId) { - DB::table('v2_server') - ->where('id', $server->id) - ->update(['parent_id' => $parentId]); - } - } - } - - /** - * Restore parent_id references when rolling back - */ - private function restoreParentIds(string $type, string $table): void - { - // Get all servers of the specified type that have a parent_id - $servers = DB::table($table) - ->whereNotNull('parent_id') - ->get(); - - // Update each server's parent_id to reference back to the original id - foreach ($servers as $server) { - $originalParentId = DB::table('v2_server') - ->where('type', $type) - ->where('id', $server->parent_id) - ->value('code'); - - if ($originalParentId) { - DB::table($table) - ->where('id', $server->id) - ->update(['parent_id' => $originalParentId]); - } - } - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - // Recreate old tables - Schema::create('v2_server_trojan', function (Blueprint $table) { - $table->integer('id', true)->comment('节点ID'); - $table->string('group_id')->comment('节点组'); - $table->string('route_id')->nullable(); - $table->string('ips')->nullable(); - $table->string('excludes')->nullable(); - $table->integer('parent_id')->nullable()->comment('父节点'); - $table->string('tags')->nullable()->comment('节点标签'); - $table->string('name')->comment('节点名称'); - $table->string('rate', 11)->comment('倍率'); - $table->string('host')->comment('主机名'); - $table->string('port', 11)->comment('连接端口'); - $table->integer('server_port')->comment('服务端口'); - $table->boolean('allow_insecure')->default(false)->comment('是否允许不安全'); - $table->string('server_name')->nullable(); - $table->string('network')->nullable(); - $table->text('networkSettings')->nullable(); - $table->boolean('show')->default(false)->comment('是否显示'); - $table->integer('sort')->nullable(); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - - Schema::create('v2_server_vmess', function (Blueprint $table) { - $table->integer('id', true); - $table->string('group_id'); - $table->string('route_id')->nullable(); - $table->string('ips')->nullable(); - $table->string('excludes')->nullable(); - $table->string('name'); - $table->integer('parent_id')->nullable(); - $table->string('host'); - $table->string('port', 11); - $table->integer('server_port'); - $table->tinyInteger('tls')->default(0); - $table->string('tags')->nullable(); - $table->string('rate', 11); - $table->string('network', 11); - $table->text('rules')->nullable(); - $table->text('networkSettings')->nullable(); - $table->text('tlsSettings')->nullable(); - $table->boolean('show')->default(false); - $table->integer('sort')->nullable(); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - - Schema::create('v2_server_vless', function (Blueprint $table) { - $table->integer('id', true); - $table->text('group_id'); - $table->text('route_id')->nullable(); - $table->string('ips')->nullable(); - $table->string('excludes')->nullable(); - $table->string('name'); - $table->integer('parent_id')->nullable(); - $table->string('host'); - $table->integer('port'); - $table->integer('server_port'); - $table->boolean('tls'); - $table->text('tls_settings')->nullable(); - $table->string('flow', 64)->nullable(); - $table->string('network', 11); - $table->text('network_settings')->nullable(); - $table->text('tags')->nullable(); - $table->string('rate', 11); - $table->boolean('show')->default(false); - $table->integer('sort')->nullable(); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - - Schema::create('v2_server_shadowsocks', function (Blueprint $table) { - $table->integer('id', true); - $table->string('group_id'); - $table->string('route_id')->nullable(); - $table->string('ips')->nullable(); - $table->string('excludes')->nullable(); - $table->integer('parent_id')->nullable(); - $table->string('tags')->nullable(); - $table->string('name'); - $table->string('rate', 11); - $table->string('host'); - $table->string('port', 11); - $table->integer('server_port'); - $table->string('cipher'); - $table->char('obfs', 11)->nullable(); - $table->string('obfs_settings')->nullable(); - $table->tinyInteger('show')->default(0); - $table->integer('sort')->nullable(); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - - Schema::create('v2_server_hysteria', function (Blueprint $table) { - $table->integer('id', true); - $table->string('group_id'); - $table->string('route_id')->nullable(); - $table->string('ips')->nullable(); - $table->string('excludes')->nullable(); - $table->string('name'); - $table->integer('parent_id')->nullable(); - $table->string('host'); - $table->string('port', 11); - $table->integer('server_port'); - $table->string('tags')->nullable(); - $table->string('rate', 11); - $table->boolean('show')->default(false); - $table->integer('sort')->nullable(); - $table->tinyInteger('version', false, true)->default(1)->comment('hysteria版本,Version:1\2'); - $table->boolean('is_obfs')->default(true)->comment('是否开启obfs'); - $table->string('alpn')->nullable(); - $table->integer('up_mbps'); - $table->integer('down_mbps'); - $table->string('server_name', 64)->nullable(); - $table->boolean('insecure')->default(false); - $table->integer('created_at'); - $table->integer('updated_at'); - }); - - // Migrate data back to old tables - $servers = DB::table('v2_server')->get(); - foreach ($servers as $server) { - $settings = json_decode($server->protocol_settings, true); - $timestamp = strtotime($server->created_at); - $updated = strtotime($server->updated_at); - switch ($server->type) { - case 'trojan': - DB::table('v2_server_trojan')->insert([ - 'id' => (int) $server->code, - 'group_id' => $server->group_ids, - 'route_id' => $server->route_ids, - 'parent_id' => $server->parent_id, - 'tags' => $server->tags, - 'name' => $server->name, - 'rate' => (string) $server->rate, - 'host' => $server->host, - 'port' => $server->port, - 'server_port' => $server->server_port, - 'allow_insecure' => $settings['allow_insecure'], - 'server_name' => $settings['server_name'], - 'network' => $settings['network'] ?? null, - 'networkSettings' => $settings['network_settings'] ?? null, - 'show' => $server->show, - 'sort' => $server->sort, - 'created_at' => $timestamp, - 'updated_at' => $updated - ]); - break; - case 'vmess': - DB::table('v2_server_vmess')->insert([ - 'id' => (int) $server->code, - 'group_id' => $server->group_ids, - 'route_id' => $server->route_ids, - 'name' => $server->name, - 'parent_id' => $server->parent_id, - 'host' => $server->host, - 'port' => $server->port, - 'server_port' => $server->server_port, - 'tls' => $settings['tls'], - 'tags' => $server->tags, - 'rate' => (string) $server->rate, - 'network' => $settings['network'], - 'rules' => json_encode($settings['rules']), - 'networkSettings' => json_encode($settings['network_settings']), - 'tlsSettings' => json_encode($settings['tls_settings']), - 'show' => $server->show, - 'sort' => $server->sort, - 'created_at' => $timestamp, - 'updated_at' => $updated - ]); - break; - case 'vless': - // 处理 reality settings - $tlsSettings = $settings['tls_settings'] ?? new \stdClass(); - if (isset($settings['reality_settings'])) { - $tlsSettings = array_merge((array) $tlsSettings, [ - 'public_key' => $settings['reality_settings']['public_key'], - 'short_id' => $settings['reality_settings']['short_id'], - 'server_name' => explode(':', $settings['reality_settings']['dest'])[0], - 'server_port' => explode(':', $settings['reality_settings']['dest'])[1] ?? null, - 'private_key' => $settings['reality_settings']['private_key'] - ]); - } - - DB::table('v2_server_vless')->insert([ - 'id' => (int) $server->code, - 'group_id' => $server->group_ids, - 'route_id' => $server->route_ids, - 'name' => $server->name, - 'parent_id' => $server->parent_id, - 'host' => $server->host, - 'port' => $server->port, - 'server_port' => $server->server_port, - 'tls' => $settings['tls'], - 'tls_settings' => json_encode($tlsSettings), - 'flow' => $settings['flow'], - 'network' => $settings['network'], - 'network_settings' => json_encode($settings['network_settings']), - 'tags' => $server->tags, - 'rate' => (string) $server->rate, - 'show' => $server->show, - 'sort' => $server->sort, - 'created_at' => $timestamp, - 'updated_at' => $updated - ]); - break; - case 'shadowsocks': - DB::table('v2_server_shadowsocks')->insert([ - 'id' => (int) $server->code, - 'group_id' => $server->group_ids, - 'route_id' => $server->route_ids, - 'parent_id' => $server->parent_id, - 'tags' => $server->tags, - 'name' => $server->name, - 'rate' => (string) $server->rate, - 'host' => $server->host, - 'port' => $server->port, - 'server_port' => $server->server_port, - 'cipher' => $settings['cipher'], - 'obfs' => $settings['obfs'], - 'obfs_settings' => json_encode($settings['obfs_settings']), - 'show' => (int) $server->show, - 'sort' => $server->sort, - 'created_at' => $timestamp, - 'updated_at' => $updated - ]); - break; - case 'hysteria': - DB::table('v2_server_hysteria')->insert([ - 'id' => (int) $server->code, - 'group_id' => $server->group_ids, - 'route_id' => $server->route_ids, - 'name' => $server->name, - 'parent_id' => $server->parent_id, - 'host' => $server->host, - 'port' => $server->port, - 'server_port' => $server->server_port, - 'tags' => $server->tags, - 'rate' => (string) $server->rate, - 'show' => $server->show, - 'sort' => $server->sort, - 'up_mbps' => $settings['bandwidth']['up'], - 'down_mbps' => $settings['bandwidth']['down'], - 'server_name' => $settings['tls']['server_name'], - 'insecure' => $settings['tls']['allow_insecure'], - 'created_at' => $timestamp, - 'updated_at' => $updated - ]); - break; - } - } - - // Restore parent_id references for each server type - $this->restoreParentIds('trojan', 'v2_server_trojan'); - $this->restoreParentIds('vmess', 'v2_server_vmess'); - $this->restoreParentIds('vless', 'v2_server_vless'); - $this->restoreParentIds('shadowsocks', 'v2_server_shadowsocks'); - $this->restoreParentIds('hysteria', 'v2_server_hysteria'); - - // Drop new table - Schema::dropIfExists('v2_server'); - } -}; diff --git a/Xboard/database/migrations/2025_01_10_152139_add_device_limit_column.php b/Xboard/database/migrations/2025_01_10_152139_add_device_limit_column.php deleted file mode 100644 index 8015d10..0000000 --- a/Xboard/database/migrations/2025_01_10_152139_add_device_limit_column.php +++ /dev/null @@ -1,31 +0,0 @@ -unsignedInteger('device_limit')->nullable()->after('speed_limit'); - }); - Schema::table('v2_user', function (Blueprint $table) { - $table->integer('device_limit')->nullable()->after('expired_at'); - $table->integer('online_count')->nullable()->after('device_limit'); - $table->timestamp('last_online_at')->nullable()->after('online_count'); - }); - } - - public function down(): void - { - Schema::table('v2_user', function (Blueprint $table) { - $table->dropColumn('device_limit'); - $table->dropColumn('online_count'); - $table->dropColumn('last_online_at'); - }); - Schema::table('v2_plan', function (Blueprint $table) { - $table->dropColumn('device_limit'); - }); - } -}; diff --git a/Xboard/database/migrations/2025_01_12_190315_add_sort_to_v2_notice_table.php b/Xboard/database/migrations/2025_01_12_190315_add_sort_to_v2_notice_table.php deleted file mode 100644 index 9cbef66..0000000 --- a/Xboard/database/migrations/2025_01_12_190315_add_sort_to_v2_notice_table.php +++ /dev/null @@ -1,30 +0,0 @@ -integer('sort')->nullable()->after('id')->index(); - }); - - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('v2_notice', function (Blueprint $table) { - $table->dropColumn('sort'); - }); - } -}; diff --git a/Xboard/database/migrations/2025_01_12_200936_modify_commission_status_in_v2_order_table.php b/Xboard/database/migrations/2025_01_12_200936_modify_commission_status_in_v2_order_table.php deleted file mode 100644 index 668ef17..0000000 --- a/Xboard/database/migrations/2025_01_12_200936_modify_commission_status_in_v2_order_table.php +++ /dev/null @@ -1,33 +0,0 @@ -where('commission_status', null)->update([ - 'commission_status' => 0 - ]); - Schema::table('v2_order', function (Blueprint $table) { - $table->integer('commission_status')->default(value: 0)->comment('0待确认1发放中2有效3无效')->change(); - }); - - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('v2_order', function (Blueprint $table) { - $table->integer('commission_status')->nullable()->comment('0待确认1发放中2有效3无效')->change(); - }); - } -}; diff --git a/Xboard/database/migrations/2025_01_13_000000_convert_order_period_fields.php b/Xboard/database/migrations/2025_01_13_000000_convert_order_period_fields.php deleted file mode 100644 index dc6718c..0000000 --- a/Xboard/database/migrations/2025_01_13_000000_convert_order_period_fields.php +++ /dev/null @@ -1,56 +0,0 @@ - 'monthly', - 'quarter_price' => 'quarterly', - 'half_year_price' => 'half_yearly', - 'year_price' => 'yearly', - 'two_year_price' => 'two_yearly', - 'three_year_price' => 'three_yearly', - 'onetime_price' => 'onetime', - 'reset_price' => 'reset_traffic' - ]; - - /** - * Run the migrations. - */ - public function up(): void - { - // 批量更新订单的周期字段 - foreach (self::PERIOD_MAPPING as $oldPeriod => $newPeriod) { - DB::table('v2_order') - ->where('period', $oldPeriod) - ->update(['period' => $newPeriod]); - } - - // 检查是否还有未转换的记录 - $unconvertedCount = DB::table('v2_order') - ->whereNotIn('period', array_values(self::PERIOD_MAPPING)) - ->count(); - - if ($unconvertedCount > 0) { - Log::warning("Found {$unconvertedCount} orders with unconverted period values"); - } - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - // 回滚操作 - 将新的周期值转换回旧的价格字段名 - foreach (self::PERIOD_MAPPING as $oldPeriod => $newPeriod) { - DB::table('v2_order') - ->where('period', $newPeriod) - ->update(['period' => $oldPeriod]); - } - } -}; \ No newline at end of file diff --git a/Xboard/database/migrations/2025_01_15_000002_add_stat_performance_indexes.php b/Xboard/database/migrations/2025_01_15_000002_add_stat_performance_indexes.php deleted file mode 100644 index 4640e7e..0000000 --- a/Xboard/database/migrations/2025_01_15_000002_add_stat_performance_indexes.php +++ /dev/null @@ -1,93 +0,0 @@ -index('t'); - $table->index('online_count'); - $table->index('created_at'); - }); - - Schema::table('v2_order', function (Blueprint $table) { - $table->index('created_at'); - $table->index('status'); - $table->index('total_amount'); - $table->index('commission_status'); - $table->index('invite_user_id'); - $table->index('commission_balance'); - }); - - Schema::table('v2_stat_server', function (Blueprint $table) { - $table->index('server_id'); - $table->index('record_at'); - $table->index('u'); - $table->index('d'); - }); - - Schema::table('v2_stat_user', function (Blueprint $table) { - $table->index('u'); - $table->index('d'); - }); - - Schema::table('v2_commission_log', function (Blueprint $table) { - $table->index('created_at'); - $table->index('get_amount'); - }); - - Schema::table('v2_ticket', function (Blueprint $table) { - $table->index('status'); - $table->index('created_at'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('v2_user', function (Blueprint $table) { - $table->dropIndex(['t']); - $table->dropIndex(['online_count']); - $table->dropIndex(['created_at']); - }); - - Schema::table('v2_order', function (Blueprint $table) { - $table->dropIndex(['created_at']); - $table->dropIndex(['status']); - $table->dropIndex(['total_amount']); - $table->dropIndex(['commission_status']); - $table->dropIndex(['invite_user_id']); - $table->dropIndex(['commission_balance']); - }); - - Schema::table('v2_stat_server', function (Blueprint $table) { - $table->dropIndex(['server_id']); - $table->dropIndex(['record_at']); - $table->dropIndex(['u']); - $table->dropIndex(['d']); - }); - - Schema::table('v2_stat_user', function (Blueprint $table) { - $table->dropIndex(['u']); - $table->dropIndex(['d']); - }); - - Schema::table('v2_commission_log', function (Blueprint $table) { - $table->dropIndex(['created_at']); - $table->dropIndex(['get_amount']); - }); - - Schema::table('v2_ticket', function (Blueprint $table) { - $table->dropIndex(['status']); - $table->dropIndex(['created_at']); - }); - } -}; \ No newline at end of file diff --git a/Xboard/database/migrations/2025_01_16_142320_add_updated_at_index_to_v2_order_table.php b/Xboard/database/migrations/2025_01_16_142320_add_updated_at_index_to_v2_order_table.php deleted file mode 100644 index 5278bbf..0000000 --- a/Xboard/database/migrations/2025_01_16_142320_add_updated_at_index_to_v2_order_table.php +++ /dev/null @@ -1,28 +0,0 @@ -index('updated_at'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('v2_order', function (Blueprint $table) { - $table->dropIndex(['updated_at']); - }); - } -}; diff --git a/Xboard/database/migrations/2025_01_18_140511_create_plugins_table.php b/Xboard/database/migrations/2025_01_18_140511_create_plugins_table.php deleted file mode 100644 index be3d070..0000000 --- a/Xboard/database/migrations/2025_01_18_140511_create_plugins_table.php +++ /dev/null @@ -1,33 +0,0 @@ -id(); - $table->string('name'); - $table->string('code')->unique(); - $table->string('version', 50); - $table->boolean('is_enabled')->default(false); - $table->json('config')->nullable(); - $table->timestamp('installed_at')->nullable(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('v2_plugins'); - } -}; diff --git a/Xboard/database/migrations/2025_06_21_000001_optimize_v2_settings_table.php b/Xboard/database/migrations/2025_06_21_000001_optimize_v2_settings_table.php deleted file mode 100644 index affce15..0000000 --- a/Xboard/database/migrations/2025_06_21_000001_optimize_v2_settings_table.php +++ /dev/null @@ -1,37 +0,0 @@ -mediumText('value')->nullable()->change(); - // 添加优化索引 - $table->index('name', 'idx_setting_name'); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::table('v2_settings', function (Blueprint $table) { - $table->string('value')->nullable()->change(); - $table->dropIndex('idx_setting_name'); - }); - } -} \ No newline at end of file diff --git a/Xboard/database/migrations/2025_06_21_000002_create_traffic_reset_logs_table.php b/Xboard/database/migrations/2025_06_21_000002_create_traffic_reset_logs_table.php deleted file mode 100644 index 2d12fc4..0000000 --- a/Xboard/database/migrations/2025_06_21_000002_create_traffic_reset_logs_table.php +++ /dev/null @@ -1,43 +0,0 @@ -id(); - $table->bigInteger('user_id')->comment('用户ID'); - $table->string('reset_type', 50)->comment('重置类型'); - $table->timestamp('reset_time')->comment('重置时间'); - $table->bigInteger('old_upload')->default(0)->comment('重置前上传流量'); - $table->bigInteger('old_download')->default(0)->comment('重置前下载流量'); - $table->bigInteger('old_total')->default(0)->comment('重置前总流量'); - $table->bigInteger('new_upload')->default(0)->comment('重置后上传流量'); - $table->bigInteger('new_download')->default(0)->comment('重置后下载流量'); - $table->bigInteger('new_total')->default(0)->comment('重置后总流量'); - $table->string('trigger_source', 50)->comment('触发来源'); - $table->json('metadata')->nullable()->comment('额外元数据'); - $table->timestamps(); - - // 添加索引 - $table->index('user_id', 'idx_user_id'); - $table->index('reset_time', 'idx_reset_time'); - $table->index(['user_id', 'reset_time'], 'idx_user_reset_time'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('v2_traffic_reset_logs'); - } -} \ No newline at end of file diff --git a/Xboard/database/migrations/2025_06_21_000003_add_traffic_reset_fields_to_users.php b/Xboard/database/migrations/2025_06_21_000003_add_traffic_reset_fields_to_users.php deleted file mode 100644 index 004c666..0000000 --- a/Xboard/database/migrations/2025_06_21_000003_add_traffic_reset_fields_to_users.php +++ /dev/null @@ -1,39 +0,0 @@ -integer('next_reset_at')->nullable()->after('expired_at')->comment('下次流量重置时间'); - $table->integer('last_reset_at')->nullable()->after('next_reset_at')->comment('上次流量重置时间'); - $table->integer('reset_count')->default(0)->after('last_reset_at')->comment('流量重置次数'); - $table->index('next_reset_at', 'idx_next_reset_at'); - }); - } - - // Set initial reset time for existing users - Artisan::call('reset:traffic', ['--fix-null' => true]); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('v2_user', function (Blueprint $table) { - $table->dropIndex('idx_next_reset_at'); - $table->dropColumn(['next_reset_at', 'last_reset_at', 'reset_count']); - }); - } -} \ No newline at end of file diff --git a/Xboard/database/migrations/2025_07_01_081556_add_tags_to_v2_plan_table.php b/Xboard/database/migrations/2025_07_01_081556_add_tags_to_v2_plan_table.php deleted file mode 100644 index 7edf6d7..0000000 --- a/Xboard/database/migrations/2025_07_01_081556_add_tags_to_v2_plan_table.php +++ /dev/null @@ -1,28 +0,0 @@ -json('tags')->nullable()->after('content'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('v2_plan', function (Blueprint $table) { - $table->dropColumn('tags'); - }); - } -}; diff --git a/Xboard/database/migrations/2025_07_01_122908_create_gift_card_tables.php b/Xboard/database/migrations/2025_07_01_122908_create_gift_card_tables.php deleted file mode 100644 index 290ce00..0000000 --- a/Xboard/database/migrations/2025_07_01_122908_create_gift_card_tables.php +++ /dev/null @@ -1,98 +0,0 @@ -id(); - $table->string('name')->comment('礼品卡名称'); - $table->text('description')->nullable()->comment('礼品卡描述'); - $table->tinyInteger('type')->comment('卡片类型:1余额 2有效期 3流量 4重置包 5套餐 6组合 7盲盒 8任务 9等级 10节日'); - $table->tinyInteger('status')->default(1)->comment('状态:0禁用 1启用'); - $table->json('conditions')->nullable()->comment('使用条件配置'); - $table->json('rewards')->comment('奖励配置'); - $table->json('limits')->nullable()->comment('限制条件'); - $table->json('special_config')->nullable()->comment('特殊配置(节日时间、等级倍率等)'); - $table->string('icon')->nullable()->comment('卡片图标'); - $table->string('background_image')->nullable()->comment('背景图片URL'); - $table->string('theme_color', 7)->default('#1890ff')->comment('主题色'); - $table->integer('sort')->default(0)->comment('排序'); - $table->integer('admin_id')->comment('创建管理员ID'); - $table->integer('created_at'); - $table->integer('updated_at'); - - $table->index(['type', 'status'], 'idx_gift_template_type_status'); - $table->index('created_at', 'idx_gift_template_created_at'); - }); - - // 礼品卡兑换码表 - Schema::create('v2_gift_card_code', function (Blueprint $table) { - $table->id(); - $table->integer('template_id')->comment('模板ID'); - $table->string('code', 32)->unique()->comment('兑换码'); - $table->string('batch_id', 32)->nullable()->comment('批次ID'); - $table->tinyInteger('status')->default(0)->comment('状态:0未使用 1已使用 2已过期 3已禁用'); - $table->integer('user_id')->nullable()->comment('使用用户ID'); - $table->integer('used_at')->nullable()->comment('使用时间'); - $table->integer('expires_at')->nullable()->comment('过期时间'); - $table->json('actual_rewards')->nullable()->comment('实际获得的奖励(用于盲盒等)'); - $table->integer('usage_count')->default(0)->comment('使用次数(分享卡)'); - $table->integer('max_usage')->default(1)->comment('最大使用次数'); - $table->json('metadata')->nullable()->comment('额外数据'); - $table->integer('created_at'); - $table->integer('updated_at'); - - $table->index('template_id', 'idx_gift_code_template_id'); - $table->index('status', 'idx_gift_code_status'); - $table->index('user_id', 'idx_gift_code_user_id'); - $table->index('batch_id', 'idx_gift_code_batch_id'); - $table->index('expires_at', 'idx_gift_code_expires_at'); - $table->index(['code', 'status', 'expires_at'], 'idx_gift_code_lookup'); - }); - - // 礼品卡使用记录表 - Schema::create('v2_gift_card_usage', function (Blueprint $table) { - $table->id(); - $table->integer('code_id')->comment('兑换码ID'); - $table->integer('template_id')->comment('模板ID'); - $table->integer('user_id')->comment('使用用户ID'); - $table->integer('invite_user_id')->nullable()->comment('邀请人ID'); - $table->json('rewards_given')->comment('实际发放的奖励'); - $table->json('invite_rewards')->nullable()->comment('邀请人获得的奖励'); - $table->integer('user_level_at_use')->nullable()->comment('使用时用户等级'); - $table->integer('plan_id_at_use')->nullable()->comment('使用时用户套餐ID'); - $table->decimal('multiplier_applied', 3, 2)->default(1.00)->comment('应用的倍率'); - $table->string('ip_address', 45)->nullable()->comment('使用IP地址'); - $table->text('user_agent')->nullable()->comment('用户代理'); - $table->text('notes')->nullable()->comment('备注'); - $table->integer('created_at'); - - $table->index('code_id', 'idx_gift_usage_code_id'); - $table->index('template_id', 'idx_gift_usage_template_id'); - $table->index('user_id', 'idx_gift_usage_user_id'); - $table->index('invite_user_id', 'idx_gift_usage_invite_user_id'); - $table->index('created_at', 'idx_gift_usage_created_at'); - $table->index(['user_id', 'created_at'], 'idx_gift_usage_user_usage'); - $table->index(['template_id', 'created_at'], 'idx_gift_usage_template_stats'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('v2_gift_card_usage'); - Schema::dropIfExists('v2_gift_card_code'); - Schema::dropIfExists('v2_gift_card_template'); - } -}; diff --git a/Xboard/database/migrations/2025_07_13_224539_add_column_rate_time_ranges_to_v2_server_table.php b/Xboard/database/migrations/2025_07_13_224539_add_column_rate_time_ranges_to_v2_server_table.php deleted file mode 100644 index aa5ad62..0000000 --- a/Xboard/database/migrations/2025_07_13_224539_add_column_rate_time_ranges_to_v2_server_table.php +++ /dev/null @@ -1,29 +0,0 @@ -boolean('rate_time_enable')->default(false)->comment('是否启用动态倍率')->after('rate'); - $table->json('rate_time_ranges')->nullable()->comment('动态倍率规则')->after('rate_time_enable'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('v2_server', function (Blueprint $table) { - $table->dropColumn('rate_time_enable'); - $table->dropColumn('rate_time_ranges'); - }); - } -}; diff --git a/Xboard/database/migrations/2025_07_26_000001_add_type_to_plugins_table.php b/Xboard/database/migrations/2025_07_26_000001_add_type_to_plugins_table.php deleted file mode 100644 index b372621..0000000 --- a/Xboard/database/migrations/2025_07_26_000001_add_type_to_plugins_table.php +++ /dev/null @@ -1,24 +0,0 @@ -string('type', 20)->default('feature')->after('code')->comment('插件类型:feature功能性,payment支付型'); - $table->index(['type', 'is_enabled']); - }); - } - - public function down(): void - { - Schema::table('v2_plugins', function (Blueprint $table) { - $table->dropIndex(['type', 'is_enabled']); - $table->dropColumn('type'); - }); - } -}; \ No newline at end of file diff --git a/Xboard/database/migrations/2025_07_27_000001_create_v2_subscribe_templates_table.php b/Xboard/database/migrations/2025_07_27_000001_create_v2_subscribe_templates_table.php deleted file mode 100644 index 873a3df..0000000 --- a/Xboard/database/migrations/2025_07_27_000001_create_v2_subscribe_templates_table.php +++ /dev/null @@ -1,91 +0,0 @@ -id(); - $table->string('name')->unique()->comment('Template key, e.g. singbox, clash'); - $table->mediumText('content')->nullable()->comment('Template content'); - $table->timestamps(); - }); - - $this->seedDefaults(); - } - - public function down(): void - { - Schema::dropIfExists('v2_subscribe_templates'); - } - - private function seedDefaults(): void - { - // Fallback order matches original protocol class behavior - $protocols = [ - 'singbox' => [ - 'resources/rules/custom.sing-box.json', - 'resources/rules/default.sing-box.json', - ], - 'clash' => [ - 'resources/rules/custom.clash.yaml', - 'resources/rules/default.clash.yaml', - ], - 'clashmeta' => [ - 'resources/rules/custom.clashmeta.yaml', - 'resources/rules/custom.clash.yaml', - 'resources/rules/default.clash.yaml', - ], - 'stash' => [ - 'resources/rules/custom.stash.yaml', - 'resources/rules/custom.clash.yaml', - 'resources/rules/default.clash.yaml', - ], - 'surge' => [ - 'resources/rules/custom.surge.conf', - 'resources/rules/default.surge.conf', - ], - 'surfboard' => [ - 'resources/rules/custom.surfboard.conf', - 'resources/rules/default.surfboard.conf', - ], - ]; - - foreach ($protocols as $name => $fileFallbacks) { - $existing = DB::table('v2_settings') - ->where('name', "subscribe_template_{$name}") - ->value('value'); - - if ($existing !== null && $existing !== '') { - $content = $existing; - } else { - $content = ''; - foreach ($fileFallbacks as $file) { - $path = base_path($file); - if (File::exists($path)) { - $content = File::get($path); - break; - } - } - } - - DB::table('v2_subscribe_templates')->insert([ - 'name' => $name, - 'content' => $content, - 'created_at' => now(), - 'updated_at' => now(), - ]); - } - - // Clean up old entries from v2_settings - DB::table('v2_settings') - ->where('name', 'like', 'subscribe_template_%') - ->delete(); - } -}; diff --git a/Xboard/database/migrations/2026_03_11_000001_replace_v2_log_with_admin_audit_log.php b/Xboard/database/migrations/2026_03_11_000001_replace_v2_log_with_admin_audit_log.php deleted file mode 100644 index 7f7b587..0000000 --- a/Xboard/database/migrations/2026_03_11_000001_replace_v2_log_with_admin_audit_log.php +++ /dev/null @@ -1,28 +0,0 @@ -id(); - $table->unsignedBigInteger('admin_id')->index(); - $table->string('action', 64)->index()->comment('Action identifier e.g. user.update'); - $table->string('method', 10); - $table->string('uri', 512); - $table->text('request_data')->nullable(); - $table->string('ip', 128)->nullable(); - $table->unsignedInteger('created_at'); - $table->unsignedInteger('updated_at'); - }); - } - - public function down(): void - { - Schema::dropIfExists('v2_admin_audit_log'); - } -}; diff --git a/Xboard/database/migrations/2026_03_11_000002_add_stat_user_record_at_index.php b/Xboard/database/migrations/2026_03_11_000002_add_stat_user_record_at_index.php deleted file mode 100644 index 2438855..0000000 --- a/Xboard/database/migrations/2026_03_11_000002_add_stat_user_record_at_index.php +++ /dev/null @@ -1,22 +0,0 @@ -index(['record_at', 'user_id'], 'idx_stat_user_record_user'); - }); - } - - public function down(): void - { - Schema::table('v2_stat_user', function (Blueprint $table) { - $table->dropIndex('idx_stat_user_record_user'); - }); - } -}; diff --git a/Xboard/database/migrations/2026_03_15_060035_add_custom_config_and_cert_to_v2_server_table.php b/Xboard/database/migrations/2026_03_15_060035_add_custom_config_and_cert_to_v2_server_table.php deleted file mode 100644 index 6f67ff7..0000000 --- a/Xboard/database/migrations/2026_03_15_060035_add_custom_config_and_cert_to_v2_server_table.php +++ /dev/null @@ -1,30 +0,0 @@ -json('custom_outbounds')->nullable()->after('protocol_settings'); - $table->json('custom_routes')->nullable()->after('custom_outbounds'); - $table->json('cert_config')->nullable()->after('custom_routes'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('v2_server', function (Blueprint $table) { - $table->dropColumn(['custom_outbounds', 'custom_routes', 'cert_config']); - }); - } -}; diff --git a/Xboard/database/migrations/2026_03_28_161536_add_traffic_fields_to_servers.php b/Xboard/database/migrations/2026_03_28_161536_add_traffic_fields_to_servers.php deleted file mode 100644 index ca585b8..0000000 --- a/Xboard/database/migrations/2026_03_28_161536_add_traffic_fields_to_servers.php +++ /dev/null @@ -1,46 +0,0 @@ -bigInteger('transfer_enable') - ->default(null) - ->nullable() - ->after('rate') - ->comment('Traffic limit , 0 or null=no limit'); - } - if (!Schema::hasColumn('v2_server', 'u')) { - $table->bigInteger('u') - ->default(0) - ->after('transfer_enable') - ->comment('upload traffic'); - } - if (!Schema::hasColumn('v2_server', 'd')) { - $table->bigInteger('d') - ->default(0) - ->after('u') - ->comment('donwload traffic'); - } - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('v2_server', function (Blueprint $table) { - $table->dropColumn(['transfer_enable', 'u', 'd']); - }); - } -}; diff --git a/Xboard/database/seeders/DatabaseSeeder.php b/Xboard/database/seeders/DatabaseSeeder.php deleted file mode 100644 index 34b89fa..0000000 --- a/Xboard/database/seeders/DatabaseSeeder.php +++ /dev/null @@ -1,17 +0,0 @@ -call(UsersTableSeeder::class) - } -} diff --git a/Xboard/database/seeders/OriginV2bMigrationsTableSeeder.php b/Xboard/database/seeders/OriginV2bMigrationsTableSeeder.php deleted file mode 100644 index 5643bb5..0000000 --- a/Xboard/database/seeders/OriginV2bMigrationsTableSeeder.php +++ /dev/null @@ -1,170 +0,0 @@ -insert(array ( - 0 => - array ( - 'id' => 1, - 'migration' => '2019_08_19_000000_create_failed_jobs_table', - 'batch' => 1, - ), - 1 => - array ( - 'id' => 2, - 'migration' => '2023_08_07_205816_create_v2_commission_log_table', - 'batch' => 1, - ), - 2 => - array ( - 'id' => 3, - 'migration' => '2023_08_07_205816_create_v2_coupon_table', - 'batch' => 1, - ), - 3 => - array ( - 'id' => 4, - 'migration' => '2023_08_07_205816_create_v2_invite_code_table', - 'batch' => 1, - ), - 4 => - array ( - 'id' => 5, - 'migration' => '2023_08_07_205816_create_v2_knowledge_table', - 'batch' => 1, - ), - 5 => - array ( - 'id' => 6, - 'migration' => '2023_08_07_205816_create_v2_log_table', - 'batch' => 1, - ), - 6 => - array ( - 'id' => 7, - 'migration' => '2023_08_07_205816_create_v2_mail_log_table', - 'batch' => 1, - ), - 7 => - array ( - 'id' => 8, - 'migration' => '2023_08_07_205816_create_v2_notice_table', - 'batch' => 1, - ), - 8 => - array ( - 'id' => 9, - 'migration' => '2023_08_07_205816_create_v2_order_table', - 'batch' => 1, - ), - 9 => - array ( - 'id' => 10, - 'migration' => '2023_08_07_205816_create_v2_payment_table', - 'batch' => 1, - ), - 10 => - array ( - 'id' => 11, - 'migration' => '2023_08_07_205816_create_v2_plan_table', - 'batch' => 1, - ), - 11 => - array ( - 'id' => 12, - 'migration' => '2023_08_07_205816_create_v2_server_group_table', - 'batch' => 1, - ), - 12 => - array ( - 'id' => 13, - 'migration' => '2023_08_07_205816_create_v2_server_hysteria_table', - 'batch' => 1, - ), - 13 => - array ( - 'id' => 14, - 'migration' => '2023_08_07_205816_create_v2_server_route_table', - 'batch' => 1, - ), - 14 => - array ( - 'id' => 15, - 'migration' => '2023_08_07_205816_create_v2_server_shadowsocks_table', - 'batch' => 1, - ), - 15 => - array ( - 'id' => 16, - 'migration' => '2023_08_07_205816_create_v2_server_trojan_table', - 'batch' => 1, - ), - 16 => - array ( - 'id' => 17, - 'migration' => '2023_08_07_205816_create_v2_server_vless_table', - 'batch' => 1, - ), - 17 => - array ( - 'id' => 18, - 'migration' => '2023_08_07_205816_create_v2_server_vmess_table', - 'batch' => 1, - ), - 18 => - array ( - 'id' => 19, - 'migration' => '2023_08_07_205816_create_v2_stat_server_table', - 'batch' => 1, - ), - 19 => - array ( - 'id' => 20, - 'migration' => '2023_08_07_205816_create_v2_stat_table', - 'batch' => 1, - ), - 20 => - array ( - 'id' => 21, - 'migration' => '2023_08_07_205816_create_v2_stat_user_table', - 'batch' => 1, - ), - 21 => - array ( - 'id' => 22, - 'migration' => '2023_08_07_205816_create_v2_ticket_message_table', - 'batch' => 1, - ), - 22 => - array ( - 'id' => 23, - 'migration' => '2023_08_07_205816_create_v2_ticket_table', - 'batch' => 1, - ), - 23 => - array ( - 'id' => 24, - 'migration' => '2023_08_07_205816_create_v2_user_table', - 'batch' => 1, - ) - )); - } -} \ No newline at end of file diff --git a/Xboard/docs/en/development/device-limit.md b/Xboard/docs/en/development/device-limit.md deleted file mode 100644 index f56b9e7..0000000 --- a/Xboard/docs/en/development/device-limit.md +++ /dev/null @@ -1,176 +0,0 @@ -# Online Device Limit Design - -## Overview - -This document describes the design and implementation of the online device limit feature in Xboard. - -## Design Goals - -1. Accurate Control - - Precise counting of online devices - - Real-time monitoring of device status - - Accurate device identification - -2. Performance Optimization - - Minimal impact on system performance - - Efficient device tracking - - Optimized resource usage - -3. User Experience - - Smooth connection experience - - Clear error messages - - Graceful handling of limit exceeded cases - -## Implementation Details - -### 1. Device Identification - -#### Device ID Generation -```php -public function generateDeviceId($user, $request) { - return md5( - $user->id . - $request->header('User-Agent') . - $request->ip() - ); -} -``` - -#### Device Information Storage -```php -[ - 'device_id' => 'unique_device_hash', - 'user_id' => 123, - 'ip' => '192.168.1.1', - 'user_agent' => 'Mozilla/5.0...', - 'last_active' => '2024-03-21 10:00:00' -] -``` - -### 2. Connection Management - -#### Connection Check -```php -public function checkDeviceLimit($user, $deviceId) { - $onlineDevices = $this->getOnlineDevices($user->id); - - if (count($onlineDevices) >= $user->device_limit) { - if (!in_array($deviceId, $onlineDevices)) { - throw new DeviceLimitExceededException(); - } - } - - return true; -} -``` - -#### Device Status Update -```php -public function updateDeviceStatus($userId, $deviceId) { - Redis::hset( - "user:{$userId}:devices", - $deviceId, - json_encode([ - 'last_active' => now(), - 'status' => 'online' - ]) - ); -} -``` - -### 3. Cleanup Mechanism - -#### Inactive Device Cleanup -```php -public function cleanupInactiveDevices() { - $inactiveThreshold = now()->subMinutes(30); - - foreach ($this->getUsers() as $user) { - $devices = $this->getOnlineDevices($user->id); - - foreach ($devices as $deviceId => $info) { - if ($info['last_active'] < $inactiveThreshold) { - $this->removeDevice($user->id, $deviceId); - } - } - } -} -``` - -## Error Handling - -### Error Types -1. Device Limit Exceeded - ```php - class DeviceLimitExceededException extends Exception { - protected $message = 'Device limit exceeded'; - protected $code = 4001; - } - ``` - -2. Invalid Device - ```php - class InvalidDeviceException extends Exception { - protected $message = 'Invalid device'; - protected $code = 4002; - } - ``` - -### Error Messages -```php -return [ - 'device_limit_exceeded' => 'Maximum number of devices reached', - 'invalid_device' => 'Device not recognized', - 'device_expired' => 'Device session expired' -]; -``` - -## Performance Considerations - -1. Cache Strategy - - Use Redis for device tracking - - Implement cache expiration - - Optimize cache structure - -2. Database Operations - - Minimize database queries - - Use batch operations - - Implement query optimization - -3. Memory Management - - Efficient data structure - - Regular cleanup of expired data - - Memory usage monitoring - -## Security Measures - -1. Device Verification - - Validate device information - - Check for suspicious patterns - - Implement rate limiting - -2. Data Protection - - Encrypt sensitive information - - Implement access control - - Regular security audits - -## Future Improvements - -1. Enhanced Features - - Device management interface - - Device activity history - - Custom device names - -2. Performance Optimization - - Improved caching strategy - - Better cleanup mechanism - - Reduced memory usage - -3. Security Enhancements - - Advanced device fingerprinting - - Fraud detection - - Improved encryption - -## Conclusion - -This design provides a robust and efficient solution for managing online device limits while maintaining good performance and user experience. Regular monitoring and updates will ensure the system remains effective and secure. \ No newline at end of file diff --git a/Xboard/docs/en/development/performance.md b/Xboard/docs/en/development/performance.md deleted file mode 100644 index 3741d49..0000000 --- a/Xboard/docs/en/development/performance.md +++ /dev/null @@ -1,100 +0,0 @@ -# Performance Comparison Report - -## Test Environment - -### Hardware Configuration -- CPU: AMD EPYC 7K62 48-Core Processor -- Memory: 4GB -- Disk: NVMe SSD -- Network: 1Gbps - -### Software Environment -- OS: Ubuntu 22.04 LTS -- PHP: 8.2 -- MySQL: 5.7 -- Redis: 7.0 -- Docker: Latest stable version - -## Test Scenarios - -### 1. User Login Performance -- Concurrent users: 100 -- Test duration: 60 seconds -- Request type: POST -- Target endpoint: `/api/v1/passport/auth/login` - -Results: -- Average response time: 156ms -- 95th percentile: 245ms -- Maximum response time: 412ms -- Requests per second: 642 - -### 2. User Dashboard Loading -- Concurrent users: 100 -- Test duration: 60 seconds -- Request type: GET -- Target endpoint: `/api/v1/user/dashboard` - -Results: -- Average response time: 89ms -- 95th percentile: 167ms -- Maximum response time: 289ms -- Requests per second: 1121 - -### 3. Node List Query -- Concurrent users: 100 -- Test duration: 60 seconds -- Request type: GET -- Target endpoint: `/api/v1/user/server/nodes` - -Results: -- Average response time: 134ms -- 95th percentile: 223ms -- Maximum response time: 378ms -- Requests per second: 745 - -## Performance Optimization Measures - -1. Database Optimization - - Added indexes for frequently queried fields - - Optimized slow queries - - Implemented query caching - -2. Cache Strategy - - Using Redis for session storage - - Caching frequently accessed data - - Implementing cache warming - -3. Code Optimization - - Reduced database queries - - Optimized database connection pool - - Improved error handling - -## Comparison with Previous Version - -| Metric | Previous Version | Current Version | Improvement | -|--------|-----------------|-----------------|-------------| -| Login Response | 289ms | 156ms | 46% | -| Dashboard Loading | 178ms | 89ms | 50% | -| Node List Query | 256ms | 134ms | 48% | - -## Future Optimization Plans - -1. Infrastructure Level - - Implement horizontal scaling - - Add load balancing - - Optimize network configuration - -2. Application Level - - Further optimize database queries - - Implement more efficient caching strategies - - Reduce memory usage - -3. Monitoring and Maintenance - - Add performance monitoring - - Implement automatic scaling - - Regular performance testing - -## Conclusion - -The current version shows significant performance improvements compared to the previous version, with an average improvement of 48% in response times. The optimization measures implemented have effectively enhanced the system's performance and stability. \ No newline at end of file diff --git a/Xboard/docs/en/development/plugin-development-guide.md b/Xboard/docs/en/development/plugin-development-guide.md deleted file mode 100644 index a5163c5..0000000 --- a/Xboard/docs/en/development/plugin-development-guide.md +++ /dev/null @@ -1,691 +0,0 @@ -# XBoard Plugin Development Guide - -## 📦 Plugin Structure - -Each plugin is an independent directory with the following structure: - -``` -plugins/ -└── YourPlugin/ # Plugin directory (PascalCase naming) - ├── Plugin.php # Main plugin class (required) - ├── config.json # Plugin configuration (required) - ├── routes/ - │ └── api.php # API routes - ├── Controllers/ # Controllers directory - │ └── YourController.php - ├── Commands/ # Artisan commands directory - │ └── YourCommand.php - └── README.md # Documentation -``` - -## 🚀 Quick Start - -### 1. Create Configuration File `config.json` - -```json -{ - "name": "My Plugin", - "code": "my_plugin", // Corresponds to plugin directory (lowercase + underscore) - "version": "1.0.0", - "description": "Plugin functionality description", - "author": "Author Name", - "require": { - "xboard": ">=1.0.0" // Version not fully implemented yet - }, - "config": { - "api_key": { - "type": "string", - "default": "", - "label": "API Key", - "description": "API Key" - }, - "timeout": { - "type": "number", - "default": 300, - "label": "Timeout (seconds)", - "description": "Timeout in seconds" - } - } -} -``` - -### 2. Create Main Plugin Class `Plugin.php` - -```php -filter('guest_comm_config', function ($config) { - $config['my_plugin_enable'] = true; - $config['my_plugin_setting'] = $this->getConfig('api_key', ''); - return $config; - }); - } -} -``` - -### 3. Create Controller - -**Recommended approach: Extend PluginController** - -```php -getConfig('api_key'); - $timeout = $this->getConfig('timeout', 300); - - // Your business logic... - - return $this->success(['message' => 'Success']); - } -} -``` - -### 4. Create Routes `routes/api.php` - -```php - 'api/v1/your-plugin' -], function () { - Route::post('/handle', [YourController::class, 'handle']); -}); -``` - -## 🔧 Configuration Access - -In controllers, you can easily access plugin configuration: - -```php -// Get single configuration -$value = $this->getConfig('key', 'default_value'); - -// Get all configurations -$allConfig = $this->getConfig(); - -// Check if plugin is enabled -$enabled = $this->isPluginEnabled(); -``` - -## 🎣 Hook System - -### Popular Hooks (Recommended to follow) - -XBoard has built-in hooks for many business-critical nodes. Plugin developers can flexibly extend through `filter` or `listen` methods. Here are the most commonly used and valuable hooks: - -| Hook Name | Type | Typical Parameters | Description | -| ------------------------- | ------ | ----------------------- | ---------------- | -| user.register.before | action | Request | Before user registration | -| user.register.after | action | User | After user registration | -| user.login.after | action | User | After user login | -| user.password.reset.after | action | User | After password reset | -| order.cancel.before | action | Order | Before order cancellation | -| order.cancel.after | action | Order | After order cancellation | -| payment.notify.before | action | method, uuid, request | Before payment callback | -| payment.notify.verified | action | array | Payment callback verification successful | -| payment.notify.failed | action | method, uuid, request | Payment callback verification failed | -| traffic.reset.after | action | User | After traffic reset | -| ticket.create.after | action | Ticket | After ticket creation | -| ticket.reply.user.after | action | Ticket | After user replies to ticket | -| ticket.close.after | action | Ticket | After ticket closure | - -> ⚡️ The hook system will continue to expand. Developers can always follow this documentation and the `php artisan hook:list` command to get the latest supported hooks. - -### Filter Hooks - -Used to modify data: - -```php -// In Plugin.php boot() method -$this->filter('guest_comm_config', function ($config) { - // Add configuration for frontend - $config['my_setting'] = $this->getConfig('setting'); - return $config; -}); -``` - -### Action Hooks - -Used to execute operations: - -```php -$this->listen('user.created', function ($user) { - // Operations after user creation - $this->doSomething($user); -}); -``` - -## 📝 Real Example: Telegram Login Plugin - -Using TelegramLogin plugin as an example to demonstrate complete implementation: - -**Main Plugin Class** (23 lines): - -```php -filter('guest_comm_config', function ($config) { - $config['telegram_login_enable'] = true; - $config['telegram_login_domain'] = $this->getConfig('domain', ''); - $config['telegram_bot_username'] = $this->getConfig('bot_username', ''); - return $config; - }); - } -} -``` - -**Controller** (extends PluginController): - -```php -class TelegramLoginController extends PluginController -{ - public function telegramLogin(Request $request) - { - // Check plugin status - if ($error = $this->beforePluginAction()) { - return $error[1]; - } - - // Get configuration - $botToken = $this->getConfig('bot_token'); - $timeout = $this->getConfig('auth_timeout', 300); - - // Business logic... - - return $this->success($result); - } -} -``` - -## ⏰ Plugin Scheduled Tasks (Scheduler) - -Plugins can register their own scheduled tasks by implementing the `schedule(Schedule $schedule)` method in the main class. - -**Example:** - -```php -use Illuminate\Console\Scheduling\Schedule; - -class Plugin extends AbstractPlugin -{ - public function schedule(Schedule $schedule): void - { - // Execute every hour - $schedule->call(function () { - // Your scheduled task logic - \Log::info('Plugin scheduled task executed'); - })->hourly(); - } -} -``` - -- Just implement the `schedule()` method in Plugin.php. -- All plugin scheduled tasks will be automatically scheduled by the main program. -- Supports all Laravel scheduler usage. - -## 🖥️ Plugin Artisan Commands - -Plugins can automatically register Artisan commands by creating command classes in the `Commands/` directory. - -### Command Directory Structure - -``` -plugins/YourPlugin/ -├── Commands/ -│ ├── TestCommand.php # Test command -│ ├── BackupCommand.php # Backup command -│ └── CleanupCommand.php # Cleanup command -``` - -### Create Command Class - -**Example: TestCommand.php** - -```php -argument('action'); - $message = $this->option('message'); - - try { - return match ($action) { - 'ping' => $this->ping($message), - 'info' => $this->showInfo(), - default => $this->showHelp() - }; - } catch (\Exception $e) { - $this->error('Operation failed: ' . $e->getMessage()); - return 1; - } - } - - protected function ping(string $message): int - { - $this->info("✅ {$message}"); - return 0; - } - - protected function showInfo(): int - { - $this->info('Plugin Information:'); - $this->table( - ['Property', 'Value'], - [ - ['Plugin Name', 'YourPlugin'], - ['Version', '1.0.0'], - ['Status', 'Enabled'], - ] - ); - return 0; - } - - protected function showHelp(): int - { - $this->info('Usage:'); - $this->line(' php artisan your-plugin:test ping --message="Hello" # Test'); - $this->line(' php artisan your-plugin:test info # Show info'); - return 0; - } -} -``` - -### Automatic Command Registration - -- ✅ Automatically register all commands in `Commands/` directory when plugin is enabled -- ✅ Command namespace automatically set to `Plugin\YourPlugin\Commands` -- ✅ Supports all Laravel command features (arguments, options, interaction, etc.) - -### Usage Examples - -```bash -# Test command -php artisan your-plugin:test ping --message="Hello World" - -# Show information -php artisan your-plugin:test info - -# View help -php artisan your-plugin:test --help -``` - -### Best Practices - -1. **Command Naming**: Use `plugin-name:action` format, e.g., `telegram:test` -2. **Error Handling**: Wrap main logic with try-catch -3. **Return Values**: Return 0 for success, 1 for failure -4. **User Friendly**: Provide clear help information and error messages -5. **Type Declarations**: Use PHP 8.2 type declarations - -## 🛠️ Development Tools - -### Controller Base Class Selection - -**Method 1: Extend PluginController (Recommended)** - -- Automatic configuration access: `$this->getConfig()` -- Automatic status checking: `$this->beforePluginAction()` -- Unified error handling - -**Method 2: Use HasPluginConfig Trait** - -```php -use App\Http\Controllers\Controller; -use App\Traits\HasPluginConfig; - -class YourController extends Controller -{ - use HasPluginConfig; - - public function handle() - { - $config = $this->getConfig('key'); - // ... - } -} -``` - -### Configuration Types - -Supported configuration types: - -- `string` - String -- `number` - Number -- `boolean` - Boolean -- `json` - Array -- `yaml` - -## 🎯 Best Practices - -### 1. Concise Main Class - -- Plugin main class should be as concise as possible -- Mainly used for registering hooks and routes -- Complex logic should be placed in controllers or services - -### 2. Configuration Management - -- Define all configuration items in `config.json` -- Use `$this->getConfig()` to access configuration -- Provide default values for all configurations - -### 3. Route Design - -- Use semantic route prefixes -- Place API routes in `routes/api.php` -- Place Web routes in `routes/web.php` - -### 4. Error Handling - -```php -public function handle(Request $request) -{ - // Check plugin status - if ($error = $this->beforePluginAction()) { - return $error[1]; - } - - try { - // Business logic - return $this->success($result); - } catch (\Exception $e) { - return $this->fail([500, $e->getMessage()]); - } -} -``` - -## 🔍 Debugging Tips - -### 1. Logging - -```php -\Log::info('Plugin operation', ['data' => $data]); -\Log::error('Plugin error', ['error' => $e->getMessage()]); -``` - -### 2. Configuration Checking - -```php -// Check required configuration -if (!$this->getConfig('required_key')) { - return $this->fail([400, 'Missing configuration']); -} -``` - -### 3. Development Mode - -```php -if (config('app.debug')) { - // Detailed debug information for development environment -} -``` - -## 📋 Plugin Lifecycle - -1. **Installation**: Validate configuration, register to database -2. **Enable**: Load plugin, register hooks and routes -3. **Running**: Handle requests, execute business logic - -## 🎉 Summary - -Based on TelegramLogin plugin practical experience: - -- **Simplicity**: Main class only 23 lines, focused on core functionality -- **Practicality**: Extends PluginController, convenient configuration access -- **Maintainability**: Clear directory structure, standard development patterns -- **Extensibility**: Hook-based architecture, easy to extend functionality - -Following this guide, you can quickly develop plugins with complete functionality and concise code! 🚀 - -## 🖥️ Complete Plugin Artisan Commands Guide - -### Feature Highlights - -✅ **Auto Registration**: Automatically register all commands in `Commands/` directory when plugin is enabled -✅ **Namespace Isolation**: Each plugin's commands use independent namespaces -✅ **Type Safety**: Support PHP 8.2 type declarations -✅ **Error Handling**: Comprehensive exception handling and error messages -✅ **Configuration Integration**: Commands can access plugin configuration -✅ **Interaction Support**: Support user input and confirmation operations - -### Real Case Demonstrations - -#### 1. Telegram Plugin Commands - -```bash -# Test Bot connection -php artisan telegram:test ping - -# Send message -php artisan telegram:test send --message="Hello World" - -# Get Bot information -php artisan telegram:test info -``` - -#### 2. TelegramExtra Plugin Commands - -```bash -# Show all statistics -php artisan telegram-extra:stats all - -# User statistics -php artisan telegram-extra:stats users - -# JSON format output -php artisan telegram-extra:stats users --format=json -``` - -#### 3. Example Plugin Commands - -```bash -# Basic usage -php artisan example:hello - -# With arguments and options -php artisan example:hello Bear --message="Welcome!" -``` - -### Development Best Practices - -#### 1. Command Naming Conventions - -```php -// ✅ Recommended: Use plugin name as prefix -protected $signature = 'telegram:test {action}'; -protected $signature = 'telegram-extra:stats {type}'; -protected $signature = 'example:hello {name}'; - -// ❌ Avoid: Use generic names -protected $signature = 'test {action}'; -protected $signature = 'stats {type}'; -``` - -#### 2. Error Handling Pattern - -```php -public function handle(): int -{ - try { - // Main logic - return $this->executeAction(); - } catch (\Exception $e) { - $this->error('Operation failed: ' . $e->getMessage()); - return 1; - } -} -``` - -#### 3. User Interaction - -```php -// Get user input -$chatId = $this->ask('Please enter chat ID'); - -// Confirm operation -if (!$this->confirm('Are you sure you want to execute this operation?')) { - $this->info('Operation cancelled'); - return 0; -} - -// Choose operation -$action = $this->choice('Choose operation', ['ping', 'send', 'info']); -``` - -#### 4. Configuration Access - -```php -// Access plugin configuration in commands -protected function getConfig(string $key, $default = null): mixed -{ - // Get plugin instance through PluginManager - $plugin = app(\App\Services\Plugin\PluginManager::class) - ->getEnabledPlugins()['example_plugin'] ?? null; - - return $plugin ? $plugin->getConfig($key, $default) : $default; -} -``` - -### Advanced Usage - -#### 1. Multi-Command Plugins - -```php -// One plugin can have multiple commands -plugins/YourPlugin/Commands/ -├── BackupCommand.php # Backup command -├── CleanupCommand.php # Cleanup command -├── StatsCommand.php # Statistics command -└── TestCommand.php # Test command -``` - -#### 2. Inter-Command Communication - -```php -// Share data between commands through cache or database -Cache::put('plugin:backup:progress', $progress, 3600); -$progress = Cache::get('plugin:backup:progress'); -``` - -#### 3. Scheduled Task Integration - -```php -// Call commands in plugin's schedule method -public function schedule(Schedule $schedule): void -{ - $schedule->command('your-plugin:backup')->daily(); - $schedule->command('your-plugin:cleanup')->weekly(); -} -``` - -### Debugging Tips - -#### 1. Command Testing - -```bash -# View command help -php artisan your-plugin:command --help - -# Verbose output -php artisan your-plugin:command --verbose - -# Debug mode -php artisan your-plugin:command --debug -``` - -#### 2. Logging - -```php -// Log in commands -Log::info('Plugin command executed', [ - 'command' => $this->signature, - 'arguments' => $this->arguments(), - 'options' => $this->options() -]); -``` - -#### 3. Performance Monitoring - -```php -// Record command execution time -$startTime = microtime(true); -// ... execution logic -$endTime = microtime(true); -$this->info("Execution time: " . round(($endTime - $startTime) * 1000, 2) . "ms"); -``` - -### Common Issues - -#### Q: Commands not showing in list? - -A: Check if plugin is enabled and ensure `Commands/` directory exists and contains valid command classes. - -#### Q: Command execution failed? - -A: Check if command class namespace is correct and ensure it extends `Illuminate\Console\Command`. - -#### Q: How to access plugin configuration? - -A: Get plugin instance through `PluginManager`, then call `getConfig()` method. - -#### Q: Can commands call other commands? - -A: Yes, use `Artisan::call()` method to call other commands. - -```php -Artisan::call('other-plugin:command', ['arg' => 'value']); -``` - -### Summary - -The plugin command system provides powerful extension capabilities for XBoard: - -- 🚀 **Development Efficiency**: Quickly create management commands -- 🔧 **Operational Convenience**: Automate daily operations -- 📊 **Monitoring Capability**: Real-time system status viewing -- 🛠️ **Debug Support**: Convenient problem troubleshooting tools - -By properly using plugin commands, you can greatly improve system maintainability and user experience! 🎉 diff --git a/Xboard/docs/en/installation/1panel.md b/Xboard/docs/en/installation/1panel.md deleted file mode 100644 index 1237444..0000000 --- a/Xboard/docs/en/installation/1panel.md +++ /dev/null @@ -1,210 +0,0 @@ -# Quick Deployment Guide for 1Panel - -This guide explains how to deploy Xboard using 1Panel. - -## 1. Environment Preparation - -Install 1Panel: -```bash -curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_start.sh && \ -sudo bash quick_start.sh -``` - -## 2. Environment Configuration - -1. Install from App Store: - - OpenResty (any version) - - ⚠️ Check "External Port Access" to open firewall - - MySQL 5.7 (Use MariaDB for ARM architecture) - -2. Create Database: - - Database name: `xboard` - - Username: `xboard` - - Access rights: All hosts (%) - - Save the database password for installation - -## 3. Deployment Steps - -1. Add Website: - - Go to "Website" > "Create Website" > "Reverse Proxy" - - Domain: Enter your domain - - Code: `xboard` - - Proxy address: `127.0.0.1:7001` - -2. Configure Reverse Proxy: -```nginx -location /ws/ { - proxy_pass http://127.0.0.1:8076; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_read_timeout 60s; -} - -location ^~ / { - proxy_pass http://127.0.0.1:7001; - proxy_http_version 1.1; - proxy_set_header Connection ""; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Real-PORT $remote_port; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $http_host; - proxy_set_header Scheme $scheme; - proxy_set_header Server-Protocol $server_protocol; - proxy_set_header Server-Name $server_name; - proxy_set_header Server-Addr $server_addr; - proxy_set_header Server-Port $server_port; - proxy_cache off; -} -``` -> The `/ws/` location enables WebSocket real-time node synchronization via `ws-server`. This service is enabled by default and can be toggled in Admin Panel > System Settings > Server. - -3. Install Xboard: -```bash -# Enter site directory -cd /opt/1panel/apps/openresty/openresty/www/sites/xboard/index - -# Install Git (if not installed) -## Ubuntu/Debian -apt update && apt install -y git -## CentOS/RHEL -yum update && yum install -y git - -# Clone repository -git clone -b compose --depth 1 https://github.com/cedar2025/Xboard ./ - -# Configure Docker Compose -``` - -4. Edit compose.yaml: -```yaml -services: - web: - image: ghcr.io/cedar2025/xboard:new - volumes: - - redis-data:/data - - ./.env:/www/.env - - ./.docker/.data/:/www/.docker/.data - - ./storage/logs:/www/storage/logs - - ./storage/theme:/www/storage/theme - - ./plugins:/www/plugins - environment: - - docker=true - depends_on: - - redis - command: php artisan octane:start --host=0.0.0.0 --port=7001 - restart: on-failure - ports: - - 7001:7001 - networks: - - 1panel-network - - horizon: - image: ghcr.io/cedar2025/xboard:new - volumes: - - redis-data:/data - - ./.env:/www/.env - - ./.docker/.data/:/www/.docker/.data - - ./storage/logs:/www/storage/logs - - ./plugins:/www/plugins - restart: on-failure - command: php artisan horizon - networks: - - 1panel-network - depends_on: - - redis - ws-server: - image: ghcr.io/cedar2025/xboard:new - volumes: - - redis-data:/data - - ./.env:/www/.env - - ./.docker/.data/:/www/.docker/.data - - ./storage/logs:/www/storage/logs - - ./plugins:/www/plugins - restart: on-failure - ports: - - 8076:8076 - networks: - - 1panel-network - command: php artisan ws-server start - depends_on: - - redis - - redis: - image: redis:7-alpine - command: redis-server --unixsocket /data/redis.sock --unixsocketperm 777 - restart: unless-stopped - networks: - - 1panel-network - volumes: - - redis-data:/data - -volumes: - redis-data: - -networks: - 1panel-network: - external: true -``` - -5. Initialize Installation: -```bash -# Install dependencies and initialize -docker compose run -it --rm web php artisan xboard:install -``` - -⚠️ Important Configuration Notes: -1. Database Configuration - - Database Host: Choose based on your deployment: - 1. If database and Xboard are in the same network, use `mysql` - 2. If connection fails, go to: Database -> Select Database -> Connection Info -> Container Connection, and use the "Host" value - 3. If using external database, enter your actual database host - - Database Port: `3306` (default port unless configured otherwise) - - Database Name: `xboard` (the database created earlier) - - Database User: `xboard` (the user created earlier) - - Database Password: Enter the password saved earlier - -2. Redis Configuration - - Choose to use built-in Redis - - No additional configuration needed - -3. Administrator Information - - Save the admin credentials displayed after installation - - Note down the admin panel access URL - -After configuration, start the services: -```bash -docker compose up -d -``` - -6. Start Services: -```bash -docker compose up -d -``` - -## 4. Version Update - -> 💡 Important Note: The update command varies depending on your installation version: -> - If you installed recently (new version), use this command: -```bash -docker compose pull && \ -docker compose run -it --rm web php artisan xboard:update && \ -docker compose up -d -``` -> - If you installed earlier (old version), replace `web` with `xboard`: -```bash -docker compose pull && \ -docker compose run -it --rm xboard php artisan xboard:update && \ -docker compose up -d -``` -> 🤔 Not sure which to use? Try the new version command first, if it fails, use the old version command. - -## Important Notes - -- ⚠️ Ensure firewall is enabled to prevent port 7001 exposure to public -- Service restart is required after code modifications -- SSL certificate configuration is recommended for secure access - -> The node will automatically detect WebSocket availability during handshake. No extra configuration is needed on the node side. diff --git a/Xboard/docs/en/installation/aapanel-docker.md b/Xboard/docs/en/installation/aapanel-docker.md deleted file mode 100644 index 348ed98..0000000 --- a/Xboard/docs/en/installation/aapanel-docker.md +++ /dev/null @@ -1,151 +0,0 @@ -# Xboard Deployment Guide for aaPanel + Docker Environment - -## Table of Contents -1. [Requirements](#requirements) -2. [Quick Deployment](#quick-deployment) -3. [Detailed Configuration](#detailed-configuration) -4. [Maintenance Guide](#maintenance-guide) -5. [Troubleshooting](#troubleshooting) - -## Requirements - -### Hardware Requirements -- CPU: 1 core or above -- Memory: 2GB or above -- Storage: 10GB+ available space - -### Software Requirements -- Operating System: Ubuntu 20.04+ / CentOS 7+ / Debian 10+ -- Latest version of aaPanel -- Docker and Docker Compose -- Nginx (any version) -- MySQL 5.7+ - -## Quick Deployment - -### 1. Install aaPanel -```bash -curl -sSL https://www.aapanel.com/script/install_6.0_en.sh -o install_6.0_en.sh && \ -bash install_6.0_en.sh aapanel -``` - -### 2. Basic Environment Setup - -#### 2.1 Install Docker -```bash -# Install Docker -curl -sSL https://get.docker.com | bash - -# For CentOS systems, also run: -systemctl enable docker -systemctl start docker -``` - -#### 2.2 Install Required Components -In the aaPanel dashboard, install: -- Nginx (any version) -- MySQL 5.7 -- ⚠️ PHP and Redis are not required - -### 3. Site Configuration - -#### 3.1 Create Website -1. Navigate to: aaPanel > Website > Add site -2. Fill in the information: - - Domain: Enter your site domain - - Database: Select MySQL - - PHP Version: Select Pure Static - -#### 3.2 Deploy Xboard -```bash -# Enter site directory -cd /www/wwwroot/your-domain - -# Clean directory -chattr -i .user.ini -rm -rf .htaccess 404.html 502.html index.html .user.ini - -# Clone repository -git clone https://github.com/cedar2025/Xboard.git ./ - -# Prepare configuration file -cp compose.sample.yaml compose.yaml - -# Install dependencies and initialize -docker compose run -it --rm web sh init.sh -``` -> ⚠️ Please save the admin dashboard URL, username, and password shown after installation - -#### 3.3 Start Services -```bash -docker compose up -d -``` - -#### 3.4 Configure Reverse Proxy -Add the following content to your site configuration: -```nginx -location /ws/ { - proxy_pass http://127.0.0.1:8076; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_read_timeout 60s; -} - -location ^~ / { - proxy_pass http://127.0.0.1:7001; - proxy_http_version 1.1; - proxy_set_header Connection ""; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Real-PORT $remote_port; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $http_host; - proxy_set_header Scheme $scheme; - proxy_set_header Server-Protocol $server_protocol; - proxy_set_header Server-Name $server_name; - proxy_set_header Server-Addr $server_addr; - proxy_set_header Server-Port $server_port; - proxy_cache off; -} -``` -> The `/ws/` location enables real-time node synchronization via `ws-server`. This service is enabled by default and can be toggled in Admin Panel > System Settings > Server. - -## Maintenance Guide - -### Version Updates - -> 💡 Important Note: Update commands may vary depending on your installed version: -> - For recent installations (new version), use: -```bash -docker compose pull && \ -docker compose run -it --rm web sh update.sh && \ -docker compose up -d -``` -> - For older installations, replace `web` with `xboard`: -```bash -git config --global --add safe.directory $(pwd) -git fetch --all && git reset --hard origin/master && git pull origin master -docker compose pull && \ -docker compose run -it --rm xboard sh update.sh && \ -docker compose up -d -``` -> 🤔 Not sure which to use? Try the new version command first, if it fails, use the old version command. - -### Routine Maintenance -- Regular log checking: `docker compose logs` -- Monitor system resource usage -- Regular backup of database and configuration files - -## Troubleshooting - -If you encounter any issues during installation or operation, please check: -1. **Empty Admin Dashboard**: If the admin panel is blank, run `git submodule update --init --recursive --force` to restore the theme files. -2. System requirements are met -3. All required ports are available -3. Docker services are running properly -4. Nginx configuration is correct -5. Check logs for detailed error messages - -> The node will automatically detect WebSocket availability during handshake. No extra configuration is needed on the node side. diff --git a/Xboard/docs/en/installation/aapanel.md b/Xboard/docs/en/installation/aapanel.md deleted file mode 100644 index 7394bc1..0000000 --- a/Xboard/docs/en/installation/aapanel.md +++ /dev/null @@ -1,210 +0,0 @@ -# Xboard Deployment Guide for aaPanel Environment - -## Table of Contents -1. [Requirements](#requirements) -2. [Quick Deployment](#quick-deployment) -3. [Detailed Configuration](#detailed-configuration) -4. [Maintenance Guide](#maintenance-guide) -5. [Troubleshooting](#troubleshooting) - -## Requirements - -### Hardware Requirements -- CPU: 1 core or above -- Memory: 2GB or above -- Storage: 10GB+ available space - -### Software Requirements -- Operating System: Ubuntu 20.04+ / Debian 10+ (⚠️ CentOS 7 is not recommended) -- Latest version of aaPanel -- PHP 8.2 -- MySQL 5.7+ -- Redis -- Nginx (any version) - -## Quick Deployment - -### 1. Install aaPanel -```bash -URL=https://www.aapanel.com/script/install_6.0_en.sh && \ -if [ -f /usr/bin/curl ];then curl -ksSO "$URL" ;else wget --no-check-certificate -O install_6.0_en.sh "$URL";fi && \ -bash install_6.0_en.sh aapanel -``` - -### 2. Basic Environment Setup - -#### 2.1 Install LNMP Environment -In the aaPanel dashboard, install: -- Nginx (any version) -- MySQL 5.7 -- PHP 8.2 - -#### 2.2 Install PHP Extensions -Required PHP extensions: -- redis -- fileinfo -- swoole -- readline -- event -- mbstring - -#### 2.3 Enable Required PHP Functions -Functions that need to be enabled: -- putenv -- proc_open -- pcntl_alarm -- pcntl_signal - -### 3. Site Configuration - -#### 3.1 Create Website -1. Navigate to: aaPanel > Website > Add site -2. Fill in the information: - - Domain: Enter your site domain - - Database: Select MySQL - - PHP Version: Select 8.2 - -#### 3.2 Deploy Xboard -```bash -# Enter site directory -cd /www/wwwroot/your-domain - -# Clean directory -chattr -i .user.ini -rm -rf .htaccess 404.html 502.html index.html .user.ini - -# Clone repository -git clone https://github.com/cedar2025/Xboard.git ./ - -# Install dependencies -sh init.sh -``` - -#### 3.3 Configure Site -1. Set running directory to `/public` -2. Add rewrite rules: -```nginx -location /downloads { -} - -location / { - try_files $uri $uri/ /index.php$is_args$query_string; -} - -location ~ .*\.(js|css)?$ -{ - expires 1h; - error_log off; - access_log /dev/null; -} -``` - -## Detailed Configuration - -### 1. Configure Daemon Process -1. Install Supervisor -2. Add queue daemon process: - - Name: `Xboard` - - Run User: `www` - - Running Directory: Site directory - - Start Command: `php artisan horizon` - - Process Count: 1 - -### 2. Configure Scheduled Tasks -- Type: Shell Script -- Task Name: v2board -- Run User: www -- Frequency: 1 minute -- Script Content: `php /www/wwwroot/site-directory/artisan schedule:run` - -### 3. Octane Configuration (Optional) -#### 3.1 Add Octane Daemon Process -- Name: Octane -- Run User: www -- Running Directory: Site directory -- Start Command: `/www/server/php/82/bin/php artisan octane:start --port 7010` -- Process Count: 1 - -#### 3.2 Octane-specific Rewrite Rules -```nginx -location ~* \.(jpg|jpeg|png|gif|js|css|svg|woff2|woff|ttf|eot|wasm|json|ico)$ { -} - -location ~ .* { - proxy_pass http://127.0.0.1:7010; - proxy_http_version 1.1; - proxy_set_header Connection ""; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Real-PORT $remote_port; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $http_host; - proxy_set_header Scheme $scheme; - proxy_set_header Server-Protocol $server_protocol; - proxy_set_header Server-Name $server_name; - proxy_set_header Server-Addr $server_addr; - proxy_set_header Server-Port $server_port; -} -``` - -## Maintenance Guide - -### Version Updates -```bash -# Enter site directory -cd /www/wwwroot/your-domain - -# Execute update script -git fetch --all && git reset --hard origin/master && git pull origin master -sh update.sh - -# If Octane is enabled, restart the daemon process -# aaPanel > App Store > Tools > Supervisor > Restart Octane -``` - -### Routine Maintenance -- Regular log checking -- Monitor system resource usage -- Regular backup of database and configuration files - -## Troubleshooting - -### Common Issues -1. **Empty Admin Dashboard**: If the admin panel is blank, run `git submodule update --init --recursive --force` to restore the theme files. -2. Changes to admin path require service restart to take effect -3. Any code changes after enabling Octane require restart to take effect -3. When PHP extension installation fails, check if PHP version is correct -4. For database connection failures, check database configuration and permissions - -## Enable WebSocket Real-time Sync (Optional) - -WebSocket enables real-time synchronization of configurations and user changes to nodes. - -### 1. Start WS Server - -Add a WebSocket daemon process in aaPanel Supervisor: -- Name: `Xboard-WS` -- Run User: `www` -- Running Directory: Site directory -- Start Command: `php artisan ws-server start` -- Process Count: 1 - -### 2. Configure Nginx - -Add the WebSocket location **before** the main `location ^~ /` block in your site's Nginx configuration: -```nginx -location /ws/ { - proxy_pass http://127.0.0.1:8076; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_read_timeout 60s; -} -``` - -### 3. Restart Services - -Restart the Octane and WS Server processes in Supervisor. - -> The node will automatically detect WebSocket availability during handshake. No extra configuration is needed on the node side. diff --git a/Xboard/docs/en/installation/docker-compose.md b/Xboard/docs/en/installation/docker-compose.md deleted file mode 100644 index c05818b..0000000 --- a/Xboard/docs/en/installation/docker-compose.md +++ /dev/null @@ -1,77 +0,0 @@ -# Quick Deployment Guide with Docker Compose - -This guide explains how to quickly deploy Xboard using Docker Compose. By default, it uses SQLite database, eliminating the need for a separate MySQL installation. - -### 1. Environment Preparation - -Install Docker: -```bash -curl -sSL https://get.docker.com | bash - -# For CentOS systems, also run: -systemctl enable docker -systemctl start docker -``` - -### 2. Deployment Steps - -1. Get project files: -```bash -git clone -b compose --depth 1 https://github.com/cedar2025/Xboard -cd Xboard -``` - -2. Install database: - -- Quick installation (Recommended for beginners) -```bash -docker compose run -it --rm \ - -e ENABLE_SQLITE=true \ - -e ENABLE_REDIS=true \ - -e ADMIN_ACCOUNT=admin@demo.com \ - web php artisan xboard:install -``` -- Custom configuration installation (Advanced users) -```bash -docker compose run -it --rm web php artisan xboard:install -``` -> Please save the admin dashboard URL, username, and password shown after installation - -3. Start services: -```bash -docker compose up -d -``` - -4. Access the site: -- Default port: 7001 -- Website URL: http://your-server-ip:7001 - -### 3. Version Updates - -> 💡 Important Note: Update commands may vary depending on your installed version: -> - For recent installations (new version), use: -```bash -cd Xboard -docker compose pull && \ -docker compose run -it --rm web php artisan xboard:update && \ -docker compose up -d -``` -> - For older installations, replace `web` with `xboard`: -```bash -cd Xboard -docker compose pull && \ -docker compose run -it --rm xboard php artisan xboard:update && \ -docker compose up -d -``` -> 🤔 Not sure which to use? Try the new version command first, if it fails, use the old version command. - -### 4. Version Rollback - -1. Modify the version number in `docker-compose.yaml` to the version you want to roll back to -2. Execute: `docker compose up -d` - -### Important Notes - -- If you need to use MySQL, please install it separately and redeploy -- Code changes require service restart to take effect -- You can configure Nginx reverse proxy to use port 80 \ No newline at end of file diff --git a/Xboard/docs/en/migration/config.md b/Xboard/docs/en/migration/config.md deleted file mode 100644 index 0a91fdd..0000000 --- a/Xboard/docs/en/migration/config.md +++ /dev/null @@ -1,54 +0,0 @@ -# Configuration Migration Guide - -This guide explains how to migrate configuration files from v2board to Xboard. Xboard stores configurations in the database instead of files. - -### 1. Docker Compose Environment - -1. Prepare configuration file: -```bash -# Create config directory -mkdir config - -# Copy old configuration file -cp old-project-path/config/v2board.php config/ -``` - -2. Modify `docker-compose.yaml`, uncomment the following line: -```yaml -- ./config/v2board.php:/www/config/v2board.php -``` - -3. Execute migration: -```bash -docker compose run -it --rm web php artisan migrateFromV2b config -``` - -### 2. aaPanel Environment - -1. Copy configuration file: -```bash -cp old-project-path/config/v2board.php config/v2board.php -``` - -2. Execute migration: -```bash -php artisan migrateFromV2b config -``` - -### 3. aaPanel + Docker Environment - -1. Copy configuration file: -```bash -cp old-project-path/config/v2board.php config/v2board.php -``` - -2. Execute migration: -```bash -docker compose run -it --rm web php artisan migrateFromV2b config -``` - -### Important Notes - -- After modifying the admin path, service restart is required: - - Docker environment: `docker compose restart` - - aaPanel environment: Restart the Octane daemon process \ No newline at end of file diff --git a/Xboard/docs/en/migration/v2board-1.7.3.md b/Xboard/docs/en/migration/v2board-1.7.3.md deleted file mode 100644 index 06ecedf..0000000 --- a/Xboard/docs/en/migration/v2board-1.7.3.md +++ /dev/null @@ -1,63 +0,0 @@ -# V2board 1.7.3 Migration Guide - -This guide explains how to migrate from V2board version 1.7.3 to Xboard. - -### 1. Database Changes Overview - -- `v2_stat_order` table renamed to `v2_stat`: - - `order_amount` → `order_total` - - `commission_amount` → `commission_total` - - New fields added: - - `paid_count` (integer, nullable) - - `paid_total` (integer, nullable) - - `register_count` (integer, nullable) - - `invite_count` (integer, nullable) - - `transfer_used_total` (string(32), nullable) - -- New tables added: - - `v2_log` - - `v2_server_hysteria` - - `v2_server_vless` - -### 2. Prerequisites - -⚠️ Please complete the basic Xboard installation first (SQLite not supported): -- [Docker Compose Deployment](../installation/docker-compose.md) -- [aaPanel + Docker Deployment](../installation/aapanel-docker.md) -- [aaPanel Deployment](../installation/aapanel.md) - -### 3. Migration Steps - -#### Docker Environment - -```bash -# 1. Stop services -docker compose down - -# 2. Clear database -docker compose run -it --rm web php artisan db:wipe - -# 3. Import old database (Important) -# Please manually import the V2board 1.7.3 database - -# 4. Execute migration -docker compose run -it --rm web php artisan migratefromv2b 1.7.3 -``` - -#### aaPanel Environment - -```bash -# 1. Clear database -php artisan db:wipe - -# 2. Import old database (Important) -# Please manually import the V2board 1.7.3 database - -# 3. Execute migration -php artisan migratefromv2b 1.7.3 -``` - -### 4. Configuration Migration - -After completing the data migration, you need to migrate the configuration file: -- [Configuration Migration Guide](./config.md) \ No newline at end of file diff --git a/Xboard/docs/en/migration/v2board-1.7.4.md b/Xboard/docs/en/migration/v2board-1.7.4.md deleted file mode 100644 index 46c1588..0000000 --- a/Xboard/docs/en/migration/v2board-1.7.4.md +++ /dev/null @@ -1,51 +0,0 @@ -# V2board 1.7.4 Migration Guide - -This guide explains how to migrate from V2board version 1.7.4 to Xboard. - -### 1. Database Changes Overview - -- New table added: - - `v2_server_vless` - -### 2. Prerequisites - -⚠️ Please complete the basic Xboard installation first (SQLite not supported): -- [Docker Compose Deployment](../installation/docker-compose.md) -- [aaPanel + Docker Deployment](../installation/aapanel-docker.md) -- [aaPanel Deployment](../installation/aapanel.md) - -### 3. Migration Steps - -#### Docker Environment - -```bash -# 1. Stop services -docker compose down - -# 2. Clear database -docker compose run -it --rm web php artisan db:wipe - -# 3. Import old database (Important) -# Please manually import the V2board 1.7.4 database - -# 4. Execute migration -docker compose run -it --rm web php artisan migratefromv2b 1.7.4 -``` - -#### aaPanel Environment - -```bash -# 1. Clear database -php artisan db:wipe - -# 2. Import old database (Important) -# Please manually import the V2board 1.7.4 database - -# 3. Execute migration -php artisan migratefromv2b 1.7.4 -``` - -### 4. Configuration Migration - -After completing the data migration, you need to migrate the configuration file: -- [Configuration Migration Guide](./config.md) \ No newline at end of file diff --git a/Xboard/docs/en/migration/v2board-dev.md b/Xboard/docs/en/migration/v2board-dev.md deleted file mode 100644 index 31621ad..0000000 --- a/Xboard/docs/en/migration/v2board-dev.md +++ /dev/null @@ -1,61 +0,0 @@ -# V2board Dev Migration Guide - -This guide explains how to migrate from V2board Dev version (2023/10/27) to Xboard. - -⚠️ Please upgrade to version 2023/10/27 following the official guide before proceeding with migration. - -### 1. Database Changes Overview - -- `v2_order` table: - - Added `surplus_order_ids` (text, nullable) - Deduction orders - -- `v2_plan` table: - - Removed `daily_unit_price` - Affects period value - - Removed `transfer_unit_price` - Affects traffic value - -- `v2_server_hysteria` table: - - Removed `ignore_client_bandwidth` - Affects bandwidth configuration - - Removed `obfs_type` - Affects obfuscation type configuration - -### 2. Prerequisites - -⚠️ Please complete the basic Xboard installation first (SQLite not supported): -- [Docker Compose Deployment](../installation/docker-compose.md) -- [aaPanel + Docker Deployment](../installation/aapanel-docker.md) -- [aaPanel Deployment](../installation/aapanel.md) - -### 3. Migration Steps - -#### Docker Environment - -```bash -# 1. Stop services -docker compose down - -# 2. Clear database -docker compose run -it --rm web php artisan db:wipe - -# 3. Import old database (Important) -# Please manually import the V2board Dev database - -# 4. Execute migration -docker compose run -it --rm web php artisan migratefromv2b dev231027 -``` - -#### aaPanel Environment - -```bash -# 1. Clear database -php artisan db:wipe - -# 2. Import old database (Important) -# Please manually import the V2board Dev database - -# 3. Execute migration -php artisan migratefromv2b dev231027 -``` - -### 4. Configuration Migration - -After completing the data migration, you need to migrate the configuration file: -- [Configuration Migration Guide](./config.md) \ No newline at end of file diff --git a/Xboard/docs/images/admin.png b/Xboard/docs/images/admin.png deleted file mode 100644 index dd553ad4266af7e680f24bf7bfcc14e271b1ddd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1423703 zcmZ^qbyQo;+V-*H1Srzr*3!}<3BkR1ixnuvHMo0lDHb3=d7!uyX($wTcS2jVNN{%z zl;ZZSp7%ZLTjzPt^IQAR*|YB1bKlooGqZN|8+9czVtQgMEG)8D%JN!RSman(SOh(U z_*ht2S6^mju&_d~Udg|F>odE*0Lyx;6nKBHtK&7|*jPILuhaPd)2KrXQAE-D*xkYW)3Q z!Nzt9&$ScA4~NyacMf-Ib76sl8G;V%?Ck9BAw)Z|(Vd;;67-!E>_7+X*e){!Te!?) zEXvrKB^Md_e_)+=#6X3RYk*3&Q#9&4C;u57{BUit&ol1T3=1X@)0FK&jh?-|T~v^l zx2(X^$`O{WrRWli@``T;^o~lyU5Cxf*seaf_x!+)h5rMC#Y7kM&`PugqDxc>0T$eg zrQDh~X^T8>Kc5d%HJ;kV;$~-em-(>}Jk)uvrz(JlE3Z;P`Sn9?)HkGBrt2`6pZ`5LGXI%h-a_CEb|VqBhK2?ev~YUPvI-7|?`xQ&b#bSu zj~=kbaauTuQMlH3>VHLb{P}wSm6p+PcPK*7!9UwCGq}?`+uw!U;lR5>ci*X6PhMXB zj6iPpLgmgL-x(9UlfZ6qGBw@u&ry1p6Wt1#W_~C?4^pJKIJ3_ow~maC4NT;wrKC)} z?OG2uZ<)*R!7P7aX^1byPbqWlLEnPR~YWz#6f9$0(g0ND2XUW9m*Ke}KjwhtR+dLN1qd~`Ju&Ap+W_12V|FML z!-|E&8KN<@35T)+D{pHX82*VwSm?^;-o8;qop%dw zH&`*|_7?Rgg-mUmu|`_^{FlUSl?iBXm!kh6XW-w4hbFUJK$YUa?Xr@X=xCyP$C>VW zr@1dBF+h&8cd-vhXhFeu0h>daN-Ya0{{*l#%7d&u1*h}BhxorVgq`P9bI&w;y~q@D zvT$@n93Q)t*0s4F{r)Wgg+dYW@zstq{3*P~KSoC>baXVSJ(L1zuxS5bCjLbe69Viw zQyszJ+!$JB*zgegI9x`P`mK%vVd%TB3W&t--(R6i^?NplGDY_*d-Q5;e#Vf%MM zPf0a;YY|BMv=HFZ!AQGMC>Z|O=IV}*$Rx{Vx=gc9^<;RQKDNhtM%@BFx zJC$y0ielLS@No96CYGMO{CY_JQ*6@*p$h+0erF$n9GSu{))p4;DR8`usboJ*+M7GY z9EolDE$Qk^?md(MgMX3$Mm|q3xv2lCng~upN9>xUrGJ!*Z|xLN%YXpJEk+cu^*o&7 zO9?R+loX7jV>87_q7AYPCxt+yA;KhDt%)%f_hp2P7X?i=c2 zmvAWfoFVt{C@=K+-lnG-1I^4{~hXK%<7_a1RVZNGM;VF_p3cp zLEbVr8vIbU(?0=>gfW2DY#a`}J7U3O2i~xY`Tyyzb8aC?Cwn61za{v$(APaSaQL@{ zk@)8O_y4N2c{Ks!Kjr&3)@3Tsd6?Ks7^o-{{drI2sVNg%qz?}M*6M!&JR`o3fq#>n zStKuDV`q0~eM4aOUq4Ht_(z;pc6RoL1V6SvFGQXB{X0kg6WDWH%>@z#dHFNMSvEVn z`$#_E9|{a67V#(iv$Rlne&?T-pZ>9>5TyUyI)3SUJM_1X>8XzC>8Z>;JtZY&)7C%6 z8_11i?ET1L-6HiU^?ZU8un+;%#8a(qgF_}V&J6ipQr6b zP#k5x#`QY~vKo&z)Zkqc9h^hb+SL_6NtuWn`cFMy8;$!5-6zX+4xM9v3HY@8Zo(_> zoS6lOE&Nt;g`J%p_6q-5X~8q%nEX%NBr)9nvolc-ymO!iy|KkP{}1=mnl1P0Gv5AKsC^iBcvEb#5S$X))#(5RliWrRZ8O@AUAx z)&c6t%L4?6|A_m4VIK-U!|jNpVAs)shzj<37KI5$zSA7FuX{G|aElR@_Pa5iEL=fe z{&0r0m!fzNex9RVB-b!!n^1dfl%V7bOrkraQHVy+2P+} zHkI|Co+(zMM-RNy(z5WNKxXD{z`k-E7;;|2&dyFi_~5^k7VCOz=*%(<>kG*=;1PiSc& z-FfbfcJd}?&Xn&oJDFmN4T+{8OB2np88Hby^TZ58H-_`ED&cVW{7EmGzDfGK75p z&V?*$Nwx~DiUj86-jY3=;AC%@nzvdHU95j%U;&rkt3r)Bx?~V9110e1mY$`{Jf;9Q zp{A)^)6C$J^WPJKr?Q7qgWgL^#{;_Ae_v?;k8dkA3rifD7kE*624%Fv#h*;%%YYSom8 zu^CFMk`-QQQu|aa*{@&szS+PSuIH@(t~bFi{q=+GQ^i+eQ{5)orCFjjUL%Mhw>G}d<5R@51Ed7XC8 z5=S6=c#(O(apd&LqPiuqY+=%~ky$&Qa$lrN6>7vf`yq!)jufyrryB8~*Pf_n^O>xF z@@3h_=}_JsiV&H{6mfm)I|3d~q)ZFMjIK!e@9+!%5 zl~~!?VU?hMCx6|rCTXNYiy=X`6IYM5mh(_ZU1YUqZ`xT~n<*T=kBdCNpQnU*&s>(8 zIXTL9%d;CJ(ubu#?ufbvHe+XTr7anJ6G(|)dhKbbEhQob$A|Lt*0#l>WLKhtNI2_l zX&#&mJea=mqjU$(;L@Z`NQ&@>=k2RaA&Ld9udi&hor9&D#<~7c1_jAFEy@cU(8yd5 z^ETog1^M-ODwzBA-8}BW-j02|{V1blO+ zvT_ka6cZvKL-@NOMZ39V?!GE4AU-2_Q@Q4tB9^H`BfD{5mlzAfDR}gdlCtSq`5M#J zc0GLddv69q7%m3`PSWmknIeW91 zf6(EQ-LWtHuNC*3%z`yC;P7w8V+ShfpIu#?DXF3K0R_IO zFQLNn>i{#H<#Ao@b+r#{79ld9d1d^-NhPrA+;J~O8p+Q+- ze(eCA`?-Z7O!8+r!FaGD7L<`wmh=gl&%7}Z_+X&3q3f(6EX_`0FO66jL{rhPpaQdh zaq>fL@I{fW2Vo`977T51(0a5sF$9bV!QdPoV=q(Spbc8jMYjB;V)mcP zaTUyouzxN55p5YN4lvq`8!1XWWOD|_16i9TS2TrnvzY=btvCB{ppF>!&~s)79#mQY zTi5zhF=Su;z7XtxHV{dC5=?jxBd* zPl~oJUi8@ayUm`l47mOp#TLHUOz+QeBea)=e*B+#l89^k{N)ae-mwHb;}_fHNG$vt z)6+>QR4aK*uSvuF+oY!89?-3Nug489a&|$~;Ux#|gF^B0M^4Vlac3N$DiE_a~`7PSbCOOwl6KZd_;2t1Z ziZ8-lz;K`EqVWOCQxJ0hfm<9GA=rX0--#R=UH~G6H0lV_KIcq>H-ILMV;vM^(>}Qi zS2wjptLu%tN8i$c&->j$@eeD3B+Dn)F@>(Ksug8q+d&=)0z;NYQ=Iv zPj*W}&=z7UmR46E{AP~yxdkGx+@Z8i&w+3eS~|D)VK>yN+{_D-G#+rgI6}B!kjX2y z!RY0}nnI25+&yW{3J<*Aa`9730N-|*Thz97BCC)r03k|;3=;E5U?4_x{m@<*y|f%H zwOZN(B=qI3sKpL#E(fFDP}et+X$*Y21sWp3i{tE$6=h>Pf1#j!4ress9#5POWVEJ5Z@VyPlhaL3l>0~ay8fg!M>+@{crJ23(X!Rgv8;sIL|oy;lmPwI!o&V9iJ5^&NJ z;>$5mZ9*ynsVniGU1tXFg}TYkf0OsZt*@ieAkcoN2O-+POIqOCrjI$(icD9sH1YER z2xoS!#Hdnb!S^xPj}MCvk3ANyiJ|O^Q*FGRXaf+}5d%AQL;(P_aB$SdV$^FYx%D9H zsFSK`x!XlkXYzmM$X&cJRFarIk^&2Qj4eC(hybC+HLh=(d6-41A|Mp_WI;O6#T?y{ z|Ev_AwqQ2A0D0Z0LrQkLiSo|Q4~A{I*6ontR~7o`yAcv(>pc6axm;x5Vc%7;8lISWjapEIk|Xtub>t5H1+)^ zKDd~3!XUPaQ}}ph$wOEEunFHGil>;*;l*vNRXJCJOjM_nXJZP$fzX@dr#eL%zBbj+ z<3=^r%e}PtDJ%x~gk6i%_&sARaD`&_jMZWRWv>06O_3Y0DZBblR80ueUrvQc6S-rjD{aBo_7JzJr#4FNrW zQmXEc)n=0;VdLfRdGl?Sh`uQJPyyr;%iCC^z3RG?@I5N}pRfY8X3>kKB*I^Q3b!HxX{A##Mh zKsVkYV$Ai5DMq`|G9Vy^Jm`W^>L`@F#%)cHI0ZPM5#;x3cm0nn5YR&5V1B_#HWU*= z7BZ#tVEkU^n>0PN61k*=77OcJHM(jds-e&5Qpl(8#ev9;`kHLy1{`^c=@nh;udFZ%(Q^Pj<;mCdQ?eyao|wUR}~1C#3KvkI3G9=>c)Hf!l=u& zMM0nTmXoJRsOGYWnTNePSPx#G%eC$GFjn-YCE^>8YVV5~BjX>*9dw5BrA)6*H+Ub0 zb)}_2@CC=S4b!-%Dmxj~1qupzd=6)apA|5EuhdtW(txTGfYL+~*gWt!HcdKR+bh@} zx^vx#P|bIG2EITVZ{@Tl0?E)cK*nD4fG_DsQ&iB-*_*!G0wXI;{wL0Z4|?fJCk9v?%0O4 zBRHBo1;~viqqHC(X`*d=98SMiUsdP9aT19FA@ru?jNc7;Evza)GQaHQ9eu8!)AME zBCHZIQ1G3Ey6A>g>#vV|Rz5zq!5ZyA&a#p5_{s4BBe?{aAYH#eIfK>7xhbD=*MFmPPszOph^F7$dRBpT4{*s@QbL==Be7lU9uW8WV_2^jK zSFNnFn=dR+FmgMcyxAO6v8#(DVw2W}Qx7kP70n$~2HpC}D&r0e2JO;~@WII4&|jiG zNs0Xc--?fify38g3gTdds1_0fwW7_?-qPG$ba z{_LQ7XUzc3-Yi5)>Q&Krmx0RHN#wKFh`C_Rd_p`NG!Rh*?2O7-#%1$qt=M@-f3r^*C%96UV6&Wg>X7ut=FW9`<4U)}?OGKpxM2uBMN^Wl-16AdwVoCw3ti?Yk!@ zckT(~Sp7hMrumSGrXd4Vf04*_sfXwqg1p-Iq~4@xIf7*KWc5)8eClRFMZeW&#hA61 z>X&ZItv^&lO3!@Rza{BrlS7pAe8tkmVYDcO@D;l$3knlfokozYV%@ zO|>~<8#_Q#J4Nxl-lZS{Tg0(lDR0OzV>?@42%|*$k7vxrU{h>Afb2hkuWjI6%P$gO zU}I0>o%N!#LWl?pQCHz)Z%Eq!-`tG3ZNzH~1!XA@Sfu0!uARST6jqluEKn?Oc+Z|N zF_|SnT%=>v{o#MQp~OI|@~&?%f$p|l8u|5M{;x?<`<;wrnw!nKsxxWdzXKMWQ>(e@ zn3+)qvFD+;`rqtHMYJDA_W}u<=rlBG^$V7zk}6YrOFauN@TiQ#~lnOw@6wVU*LlW}?U zc(gERqr$qinOYz?&)GKP0@8_*4#=7jV!FiSm|m2ov-j3H9vH>)6dhHeX5Y{177s7V zm%3dnR|#3f^{-U{Kx>|XYDG0PTkHoG%snE@tl#FWXMzvUu$L!hrj`Sxx99eTg@6=Q zyiWdQR&%}7HrcnD>f#5a^^Vz{Q`p+{n?npMR(#inCMU287WsGHEe zXrTwt(pOBoj>MHBbGgII;~1NIM)JaUQP_x~lfKXD-k0-1M|Bnmpw^Zg#G!@7hEdgV zWM9CjwFU~B%bagwo(jG-mT+!inl|D66nZ(0_a5PeFA4k0|i*f3-&dzQ}iqD zGs?u@KbJGPp4D~hxMr3!CtJKWAy}b)45;Z2_K3P0+sJI} z;Ye(1UI8pF?SCrRK0M@v2E{R%-be4;rX3`tSPWK(U9S!pjaZQc2!v0o`m1KAs)bfP zA@!aYr1+I2jF}wue0$KL>~eRei)YOQG~ZuV_Mvr^Ck1ER+(eH0-`mlb4)v=IiTV>> zHnK`vzXOSSeK-TkVa_}a85^DY2ZGBtB>d9{jjG#}HV)BXVe|Tsp#;I(7EEE%ulW1< zny{S(XE6>`;iK<0To1NwDzw}B+^=N^hN>|mOEUwk!xzSw_96bklRRH#!kzFr&`~6Q zGj?WL@Zbr1xXckYH1@X%^e&Dq+aEaI(lPX6WZ8Y`@G8FCPT#u`j;8;*}R;B!$+HdPz@w)8$w@QT(7WpeoGOF#G`LMavI9;SNW7 zvO&Mf^tF#$K`SEtp2PKlYAW+xCxe0aB5%O_=O_JiG%qPvOZ{rnQZ!1G7{1Kq2(Gv- z(`~i(IA=QO9~{b@yS2P7Y+|V^7)G{an6@~??<{;xof>V=`d$Rz3-wb>$`lf=Wm3~=4{?<&|{qf%2g17 zyzbq&H6^R11NT=80=b`XY}&Fvp-lku9!RV82{6q3(D59MqxNQ?O{l;v=Z>z=tS7ho z3siMj)-Q!eitQkYH2z8CFf-qTsFsxP0)b>n&#-Khz0SQF{+yFoXW63CaK#e?3}^H7 zQ`J2=i;6CeqNKa&ih@6oF$ht}7A`oC+ds;>ib?g@ePM!H?Tf8Mif%VSVXi}f3I7C- z@~P-)gUm39Xs0=k0D-%&sb*9NBxQDrdib;4%1mxO@T4ng}-Ew{-uYVG;DfEToK}GotR?iu}odXc~Hs#3-C0Om-jAvbxCK@u7@W@w!6$xmLEq> zW7VQS^W5Th?}xLFVNut}9^6$Yg*^CJJGMC&M)~~Cnt4} zH-+1Cq%Yc(!2JBl1kaD{mMD3^$PX*Suv)cF@9V(Vgl>wTJg+H^q#CzpBwX-V0>$(P zr~ET0Z0}dKvfTQ5)9yOobs}7CPNtFL?o^ekOZ|%!mSDw_S?$T8$+q;7kL4OOp`Z=M zmm`~^u)XS>owBD@`Zia(D&9vR2*>5@P2S`133D-~fzKOEvQk}w9*kcPY$P5y(@QwPP1bMNUOteU%sek9}sq3k{mo&oSa@@U~592Ru39!a9ZxV6~r zM}^9LTRFD&Uhh#I#|s@Qg*jN5(Av=nKYB!FwtA*3XmRphoXF>Ik^Vd)Uvuz$+{ZvuCoy{h08u{of5MWpe@9Csl3K4F8uE%#m{dqIH=B;Iea|6=bSr11*m-CL(W63;#at!{*%n~Tq`K!@JmfRi!a3v8v0@zpkHF6 z6$J63cv(2n^~$H^AIab9M3osnZM-PzSpPLpVHOaO^`fFVD@i?om4MHjQ{z#gGs0<>#?R z-pj@>m-dXe2d?%(la-_QaQ!PLH(7hQmA3<>k}T<^;OcVt`qE+H&$kG9NIwR{iEYL@ zk|d)r` zf7PdRxsj)N&aJOgvxuE=v|<8_YTSB&XoDEvBZGhRaSDZ(m45(N0GJ`!THk-%qY4}iUbvcWz z8upV-sl(=lm-9?s7Yhsh!vQzhe(m;JsU4m;PFrYKi<#6z(T8~eJ}^dm7{jl!%E-vQj-Q<60e0WnkQXTcF9?3vIQv7=PZruf90Mu z1YbAY%-vt#`&^CZ4!?g;(6*LTy9Q+Z?sTZ$hVSb86ast}ai>tb*_cPPhIEtbR_F7> z+$|OdS1qADAC5B+2_z`nsNQ>JCdz4zP7bfzWZKf>7X!YJLmGz< zeIn+4UH!cut^5{cWIP6`1z!yodKINV9ojBx6C4{RrX**?A|D<0Snq!%BY#*|Xi(+-N{JfZ$LZ)a23nLw#4kJKbaMWf^IU-@H)=vu#P`0tP0TcC#PU75SEf zrMvSNJQ29+jDC)By8W?87UFVtbnG!QGIw&-GPmG$bmyVb)t>XT#YMx^=IE{_49nl3 z-^K4`*F;07s%gF7EfxUWs`Kx+n=F%PHP*s*U8XBAl4;?4fmZU>Nq) zDibZ}wU^3!4x|@_S7%u~pO5rP+#dH7Wq@~?tp&_(0#g$1q#`$DGRJDcwICl^f#TTX zI}@z)H?TD<*D;cbLBjhprO!uPxt)Kc#pgaf%lqnjrq+%s!hurO%*Q89TSGEJx8v96 z!Q2jtZ5VzFWJ!){)V7<>%UGKlur_v%?^?dCrlYTi!<;Hk1KShJo%S_av1V+T(oxe) zv3m^HI|p24IF;88eG|LI9cV7$im|v4b}h)JYi_DZA7x-0(u-yD@~;KPSeV%e@2!T! zN6E{dbtvB#S|8P03y79}E_uQ3McgEoq+~~|M^$;bj@pdgsKcMH}I< zIOLxvm(hZ|ui~ILJ<>N+DyhtaNjbm6LtlL>a7)j|ycqlL1t}RTWi6@-XZ-DSHpg}m zx;4l)-_~?AOX3|FN-25$D6-VeE(8&3W+k20Qlw(%&t*10c)i(ZwfU`JCm*P{2Li`E zw`C=VSNA)-7F&eZY#9P1Ml#0I$dOuv(`F|`u8>*e5cYJa=&XPHLX+(np7n{i)mb$o zDX#EHu8GUp!R_)z7WVvHgOBy#8if|?v`AET9iT8ajSYrVFHXvbDP9irgP}L2@rG_> zPQ}#v?`+{a0&EXuv*l*I%|Kb{1)z-3({kzCa$%J*PBvqSXwE@GvcFr0dHsx#^eTMW z!-Di!VGGqfGbroDhcMgGi8#Hvw-SXf=l#{BpoPz3OpaWZyou(0pVcfyi=5GOo^m{! z6V{*pNcS=w_l1R~L z+tWIq$&L*1c*C?U7+Wa{no><=D!u%HizA|Rn8HBd&JLrTD-#c^`#2Z1cU700nD5%= zr;?e8C>*WWn|ygDj+?o7<{G8)+@(UBz--Px$Jzwo{b=!4Sh&UeSA6pB9eZ(-C)NAI zwkt`gPxkInj%_M)v(PIL9HcMq1B1Diu$1Ux58uwU{Gs%+l|_>txqBdSLNxQ##0h<7{l`S=YC#I+ zehW7IE1iBef)J*ZX^q>>YCXAO|5K9>Cc-^Pn3gl-=hP1q+C?V+guCyzKA9MFsolFw#lq*>^#lPoIKOZzh>Ghcf#IG<=DQ+HXE{oBY+? z;l>)y@V=u*iGq%F0T93Y*P;eCTy3@tn@SYf((UN94mj(g4oi`K)+JULf~O(Pm^^xci2v! z3-Ok6Sa&a9%oA#oab5m1b%4z!Bwe*EKK=OtnXe;#TZ@E(3N8h}~>VB^Ce#M@TF{VT5n+Vk)e-x4p<-aBsQZimn>YR=ASCWl}MW_`YOzuekk{9+-eeP+SWNSkDO zK82pEaOt$=_n}!3U09Xcg0=c%fx5K%VuHs}rq`!VY9i>SY--Dcy-D{96Px&RmTOas z$1WO~VnQl}Z>Dmp9ft^;k9B9#9 zXEM%8_da5iqxX*-h2cs`y1y2+sbM`|X4?NEn1zcF(nj?e$bIEzGiXhC^ZP55hm9bZ zgwQT0$|&EUSNRL>beb5U+?_pvX?*@vAPh90TTi0KB%BhOZyp<6KV|zJ;0*H{X-{gN zpXs*&R|L-qUJ$b1a=htCL1s!$eb{a-}?;;IQPSyc5D zL?N9QgbMNM&#B%HSAUZV@$KP3uF=Pq`)cOy3p(K$qyYf&h6f> z+ZT4+@p{8h=6=wuJ=odVa?Di8*fh2(sdt%hyZ})jZ{0QORx64(D1=%iKX!Y#))|h> znSF>$p-AT*1xes=MTESJ!}x zNQMkwuo^Zvf8OyrI-L0(e3!&p4D8i)dR~E`ouX;Fm zvYZU!i|~Szel4v~I%S>S0)$7+`B*xFKZj^2pDc+Xn+3CXc!xK9r|9!1Uo*7RIek=+ zx9muNQIqB#o0t0aR3`!)UoyV3v~=tuam|#QH&8_jfI)3eyihG2s4qXIT-?)ze_2K1ZU^~KPj761yF z7-C`1KWmi6mJRHas8oC`qx=+xpV=LKdsgT#$>QXaT*3J^%i6i7WzQ>A&@TK_P{Sls z+?Bu!`IZIqNHrVP!YyWrPyzgaqss&`-Y7LF^AkJkQSVK|+&wMt`R|yFTqvZ*Uz*%x zF&ivmf~m7ncku|VK0o{wkI+Ysre1Zl>tiV!AiT~jq?D~D_SR7D|2{2C(PJK;TB zK4g|$_mILyt%=~Co1>V@$kiw^aqL&KIlI4vS^nRykiH#Gzxt5BlbmTEmFcQ)c`$n# za4o*H5!`;G6BP8FZ>HW!V)BW1y~vmp=D+R`+~8ChYk>pi^P=g(iF zai>B--v^&cWbGrZ$-r0H zFxtgj=dF_DP4oLeIw)66b*g@q2qNy{gER)YKUsBf=P-D4cjkq?DD4;a5>&Hzlv6x) zEjdB}WXWCsYI;j+lkH2JdUNwoY?2x!9~l{`*B(F?Dki8hd9lvY=1I**^;)hWm-DAFq1J5BCo5s(U)JAXhx+nn*sSnU=?O>`viwShOd zr_Y?4o8YxII_WxD9b_eTx41;I%)IMdYqj_j&hAScGm63AJc$vL0{4k--4I~>G1oJd zLE6N_V*RZ@HBF9xX^Wgrq*l59>|V6x8uC%WcUALQRoC;#+$rCq-!kH(m_K-X!5B_N zX)w$`VV`*2L3IqIaw}Kn@QSEq0HMK?J4gUZG~lPb=#T2h@)tkJgS%Bs0*Il|Odvh| zCKKjH`0lqx&ifZyFheL4SiZF8cgxae-?sAs2XdB{fjrz_fotsUw76IO6__-2pu>Z>&RY_39``4Cz_* zf+QwCf=x@uSn79Pjn6W!`tr#AzJ?u5OGC%&l~;1~X%MZc;vQ!Rv55!5BCbm*NwyDd z;kf&HUO-sTqwO-=Xx>SET6Ul1k=NJeRId1@OnXB7Jym9xK`m-dCh6Zur}OjRwk)T6 zT3Ptp#iIDKLMUCOJoKoYWt~(i;mUf9P#z34GWRdA6daktr26|0&i9jBn?fJMq5%_Z z%eam6r)YhZmHk05cuSoNV`j-Cr&{zI=!jw4S(Vw%VvH7TrS~ron3)fqYN|!RaYG#; z*M-~_UvY8v^;>*e;DJfch}L`>hZd0}bnX^Tawn4d7RG85JbbC3+{S-Ndr zA=jk;(;HFFZ27%h4!vR-XR%xYZhK2U3fh~?@}9N3`IXv%EPo-3WfN;;h@1UArutdq z@3Dc4(~968->vNa-1|$2&*8~kOb@v<_Y_oEkp8+EKi^artQSU=!D8`gI8-EQFHfsY zg}4Y=S$T{KSVF(pmvwXx$VX2w1&8k7GRtK4-? z+V+v4mPbe29D~flLjOX%Uk2UYbkaJ#xt_A@4w9r604j|k?elI$Ny)bNi+bnmPJ@B5&#`uO zKjw`-v*43S?TZk$j?*;}WS6?QyfSCJuzpHrXmN7IDQHPF)JZIlE;H=!dvs6gz?cxQ z&Y{=n8pJm)>wE=0#1%>CWH zZJo-u(|Ar{I_{WN=2>IkkwSVc)(~*$GA@#PTk(sc|88@HbnUt_$#;@OPHrcWq8r%$bS z_6R7I3mQJC|5PKSZ~bY{b(|dBKcyV#i=Wd)>YQ-Zu$A+18tHuNTxCD0H}|CX6|y7_ zQBR9?a-8=q%v^+&gsv`1t1d;;s~?UGs{{Ah#&ywv1ltm$(Q(|KJ$kb~H6P492p56R zk+zvVuH3kh(W+}Ry>A=i-Zxrup6E4NAej$Lj z+%hJBvj;C)C6A@Debf;eZfE*i?z46_l|i@oKdL+ZM{X`pe2-P`QgqvYS#g`RRyLft zH=X;6>3LF&e|dEVkb*mRx68OZj-$ZQ(K()-YL8^OyRayk9e-{(f4$iMEAPA`;WGix zEAhq9FasMW46B=a^T6Ky9na0Sk~B&K2BN)N*U0&?XL3)ksHB*lm1XJZh$u6BnwFMk z9T4!Ig57Cqx2;{!`4hyz#eSxS;^-iQ!~e*j;i+Y-O+pp=Vf2M9A%4B{0;<_-Z`<=Z zs{iS9vlAoc1OrGf&ts|Z4~_sbRzT6>J53rpDPR?(#PS=zii7msgf zJB`co9E-l`2=*sPHMug}b$+Bwrn>kelh|6tmy6ent<{ zfmFWF3(vWT`v+rRi|=}ivkGA(SNCL?BpS+$`!`Ze`^p+ht;VN;nL^GM?)Di!6Dd=e z13&~k`hzhaUx*wwLiOt%i6T6xBYF-uf<{GB?=~(U{tDsDGxS(fYjEA5(RDi+YH&SK zYv7jxcfG$n(Qmh)Q(|^|{B-`loma%NcGb|@xG$ZcAc#Q_k{@@EPHMlJ2R((>ix#`A!E+xJm@RO2jdWzzN* zg@%)=8nC8gRA$r8reUMkfq`@Dv9(Y_oMyejpZ7n$cvorB_Znba7eh0U3X&Kbek422 z;x@fNwcj=2bSD%pvmrUIP2=F)8vV7RJ<@9r4G|JTq2l(6TLfcMxy`@7B&hP-;g1*F zRgRbV1)?2f$>TP1ks37Eu;wLT+4gLC%Qoms&CEd^@tNDOv718H%yIl(dGjuPkpIqS zqu|?9RW6qQ|`izm^H4ljQ)S$=)qP)6=v;CwrzRnM*hL>Qa|0zWaU+{(S9om?u;Eq{$||X}hig##K&pkH6}g{>c&4Z_gHw7zf)=+U0o8W|% zrMm-D(X+;aOlDkM?*hgB5dP_X3+c9?q*8mFHkg4Ke+QcB%)JF5B>dAKDNmMh&6 zHbw_~7GCF0X`u0bhy2`hFwFjmIc9bzK(wM{V z8E)Wr744Bz9d}NiEljQF+(rTkPbGH}d{#(Izsa%qu^)KHeywOuIT;ow-{L0V6Nhhg zEc+r5wV~97&gdx4uM?&nhbs`@rx|StPqfs$8#iVr8T}@HQjqGJcu1Ox-p_dsNt84{ zjx@&s#D0zu>?wFc-hz3~eE+HEVv}HLK>4>P&a~B6pZTS%?kA}gD=JIv@OZjB2Lt)F zO0ZTW?*po2>-QfE;-!Or_kb3N3{E2Tye1Ms^X&9TY!;S5eL$ z&=zp?Oyf2u3*pd&@_4Qq)T=Ma%!q~4Dy{D0bgop&lRhB;_rB)rHfWs*cf+PS5lW*U z6rq~5S6l!+AXwP}F*WcDH8CTKfQ#~WoK?U8M(FKtE4-EmhHtrY7*KjkmplVek(Glz zkgod5#N%Qf)joi;C-$&sr3{S=w7NasmXi$~ZUk~xUI-@XNXtXNCQhNC>q1_Lq4C+1 zJoS3eJ9E!|6xgL(0cqXH;RTBuj7J_4cnNiOz0pW>eRhB7$o^*s82T%Gk_)8G65r357vq(ekNq`R4dfWAmYrCUn6 zYk)LJi6Avnq+@i97&)5J-8Fi^fH7kG-uQffd;jqK1D@x0Zs%O*y6%t1Ljh-%jmoqU zm64f}EhcB*a%x*^LqKsi_Q35SQ>(jc@+NvI+dYfyv*aRSIkDebmt=*nPK7;_YbyyY zi$nXiooq&`Ah0yCyg7Oxlr`wl$QMFD9!0IEvkrS@;cE+s7ma)`jBvUK)a|Z^o=6wmwb0 zY`9Zu>GKmM-(`DTHk+sWM!ZR&q8df9pIqa`1p21 z7rH^shLb{=y0BYXf=(6Qh_wk+&J;|}7Z9`cQ0H7pWur2o8`!?p)I6`~1a>6cJK*d5 z;mxG54(n+0i!3SC;)4##peyQ8sZ9c-^UWM>UGedE`;bLhJAoK{5@zqEYPkdbpw67z z(~b20U0ux3#s(&;b84FT)pOE^_vL2Iz|T#-@vb)1FlaKCf^rf zuHd@jaK9N-m#rtNB0*TWDBAK5`5+>A1UZv5?$+7uxEa^Eoof1)R}rqP!d*G-m&qC#Z*ibGI?3Vtr0}Va}Gga6q-iU7#bx}t~)oeZQIQL7eTIO&5{== zjga5iJ3sRYIYECw(S>kW8sfv-s6;A)QAj4!a02`J? z7`GI%Rq&)C=+*|-$L*h!5I)IL*ph`skSK|1EUyo@zCofyt{8^imRXyKqbc+B+ef_e zjw^KTe(ee1|MG2nM3AGCVU+@fho`j}v_|Ds+LxB9J^oq%87(BR*MT+#cKfxXmXRXD z;sXt*u7Rg9ro!Um=00^-3hp zUXVJ2qm^4Sc5NDE!I%31al#FfT4kkwr@uc@+?9u~$vC^)wk&F(%MKvuOjkPD-`0!2 zGxo(gw~Z?eyqQ9CF5a>W9)O(M|4hQAVscE}gufUI2Vq~qePB6f$LQ9q8i(=M%#9U` z{oD$t(%)A(pX$|rCxQp9n2tupEJ{zLJ;ogy5dz~c;=zl<2<*{aIlX>A60rV=8F#~7 zyDym2Ycq=D#G#IIM8Cj`eky6x_^g_Dx~#GHu|}@Sj9jysb^!q&h?V3n6+8;<16}t< zZm&`g_NvmN7uHF;f02{*eTZnWWUlWxnTuqDUxlwCiU79~#DLv)xg&CLFjdsdN5jzW zjoevGF@-pwbyxMa*AyqdQn>XsnDe?WzW%4gihsFAmY-62Iv|J@?!V&hI|;&OQ;~!E zU_M6&t6h@qu6RdYoz&Uqs+p%N=rN~1qHZRkFz$xaQSY{dguwIsx<$wA)8QgiWe_eX z%oYtvoWlOW@6cISDeU^A-uJVsyvI%ZI}I(~Vpq+NkMUNZ`3{fF7hwW3Qi|t3uVa@L zxht)iyXE37YF9g&pue@txi{tw90?BeKb8OMepuL?y<2!v*!3>7np%B&cEUXQceq2j z^EWx8V8Mn5{1Up8gu1mI{NFOmc3t>8okDlr>FiT$d|Z=LDhp}72^&eXA&iEnRB-kGEeB!5*jG3ATX)!ttlE)%-}2rCfPnf!0HlYg38cEr2($wX@{9c z`|zyIaIN|2tP@f0Z819WQRX$IMNnLKrvI^OljD)W{sOh)OS2Qt_FNkB_DYv<%QG0y zD7Ie}Gl&C*OSY|(-(U2w5F(TxUAaZOOik^k)B$tef&Y+*>5%o&0(oY@WixPJCue&H zt?CVl35^QL9?#v|I+OUDz}gL8__iGq&5Ybl_1Cv~zB5hNiCfjR`kjg!Zi7 zfs69nC~@J!m_41zNu$PV0|it%{G^Bb?g`j$npgVtE#==Ykl>iwJG-3BOpD1vVF|~D z|2`%!@`E6i^Ht`g%TB!r=+zhOh3RVNw61BEAhTa=wH}%07h!4XtQlW|@qaOl3h%H8 zUQZrA$u@@lR(tGPJ>$yjEhcJO|c@Y7$~yFc3hD{ zX4mXq6XoK+*vT)+(5nZGrTR4DPPjja+7`qKItTU1UL83Ls)%L@mylFgb#G?Jch-;?P(M;9vmrs z3|$|i5g=a-Hw9g@SUOgYd-(>3GkC6Wc^}7UG}WLDG+1=6>6D=MhTYPV`5!-JG~E6g zql^`=pMuSOUTjNZERj-`5!|sRoLLCQh|}Xcg`_M3bafi#MXPL2^SG|w=%+}l&pWHH zg~HeW6>9cd91WX!0$$QvPf?RgtnJT67p>XoP+G-iJ;_#Q-$-*O7L}Dihej2q>K@CF zzBx~9z)X{pIh zmp{NV(3j1x{#pP*k}<~l?~QxX$Ht~}hL9y~{vef*&NV+Z7n>1JjY>fRz1kgXfu=P@ zi3CSZmF`lductRqoUNt;#$0ZT9V%)Wm?k&z_PG`H!|t-s3+oY^JT@sLPNAaZZhP5* zH{;^(BV#y85W>^lr8APg8g^l?0TQTZf2!003H&h)!6!BH81HS}RSt@&kD%7{d4LQ= z%SsNJv4wPL2u8<;fYzZ)0%O?6T^ zefct1(?KrQ@*#N1$+^WG)ll;4vBA-d7r8*W_~$Q!sWi&b?kT1JyX-E>Eot+ zU${RutLDq6E#@z=iJC=f(|ays6QR<3|ETSr@$?5J{HKGojG#%r_!8&NPs^{@St#Ck zGM$jsw>t~%<-lGI$l*RvZeDR{t)b{lhT&gV6}jn4AD|9}^1RYsi!7yEn0xXp z>uAw>KySAyN%VuE>zz_8t$l=xH3P~}bv&y*Tu*(yTMQ;OA<)(}3!(=*{j+CI%5n^w z;i>|46b4K*9M`|AC30?9pI5B&lo1h+u;u*@n|rEjc`HP{e9<$b5<9OF%c+p;aIo~> z^2%@>G}=tU(l{%=o}kCfrB28kk2E6wVajjCHs5#b)WJN!r5+z|^123L{Z&|$VL0cZ zSG(e67J8=kC!X|GJb z!6BauUem!1PSP8WmSGY+1odAQkBFB1g(XVXWQz8$v59$k5NvgP(V69#9s%i>g{EeQRdw4+?4N=Qovm=SZMMItC*sr#)aJraJ$EEMz3`Eb~)k-E;Y~+KDUxNn}c*5YGYMmq7 z&5--*C-75_VylFAs(lFGOq0h6eXh%1RlW2cG3hv3_AP>x(N6W#zmfs+8^!nehhYz3 zGGvLRm9H|pCApOdJ3fi$PrR$D%)P|OLui9{p0~eWPpO&1=6*JmdXh*0?GUT6o;WL}3*5#pOpD+b&|Wrxr&J=fDGi+kxYO z&%ByxPfaN0;9I*%|5e$iz>enM^xr$QY2xj{hC{1R>;>@dV6{L0lA)E75b3HM%>VON zl>j7oxj5C^q~m_Ef8e{!w9-G9!Y_9thzlV?shm}7KrPD$`qa|ii$%!1Yd5s_D1eGh~y?3|dQ+}y(L=k%;89B?-TJp-u;ZhL}pvYJh} zyK91ep(-O|oJ3B9=~gQ|v(H~sHfu-_`g5VkuC`L0eFNAj%ou9l$o|jQBE|!cJ>pH{ znI1vj=@sM&3Kw01sj7V{f zI3xhihZq>mb4C10$?^21c{}M3@MOUp+Q2vEi`o)~1fj2S)xDo2=(d$)z`N?Yb%Vd( z^h9RpsWv!dZDqP+L(9PJDM1(<7VqG{SWmmUN*N2IL-MDV7XRz6`VB0e6=W0OT-7$X zj0`~f121cHthyt}mk=xHZGLC0KlU&^==Z^=7d$Gdk0W$+MV?Dux*mm4YxzBQ_8Ve4 z5k4!xVV@#QuN;qYKUUYVeNZp9)?nsKF|KI`gaXA(2Oabb(7GPyyxFigV^Lw{;gL+7 z?SfAk&>yN_Wu|c7o(42uJ)K$dpz>ARO% z?8t?tbnft*WzE>;yf?#>%#Ygl9v(*hyMAR#JouMfx^78KNQ4%c_3%J&=x-zBu(W(W zZK*f@GnCq5GHdo6zgJk->q6!b7Ca_Ous+Gf32MPcP`NbYg}8Ihz?k1_c79zYoUc$b zpAWYCK9jW*7e+7K(2;R21$sK#Tm#=4b01{VOPawG9V3z~mH(RtOzgXGv0 zO&DV9r6!T>hi>xgZ~VlAXhCL$d&{)=VPkxNPmO(D7lo5jN-PuHGQS|#qk6;$zaZhK z?-w&PK$0ksnqX0-oGP>LOR(Q`N$ak2PH25ZSM$pC%&NjgYMdu6o14>fK_>f=;T<>k zZ!Aiq>4eX(K7;N2mb*sY&^N70LrU)KA+wB@rv&ijgAqs2L+(6DwzAjLnHrvj&DR;z zb&E=T`mM>)(LAxie_ecXQm#wCl=&Hf!tAOJZ)@ znLnNI7~Y>#b@_JqzujLcf4rRWww$oxxC*odW6Grw#r8QZ&x*8>=N|iJhenARMO#OT zAL${+IT?;ycHiq4nkQwXh2*B}7YYSxWhGk(TuMnb7gIq_ki-ORU(nWDz89)&0b=*t znx27mFss-;lc(uu#xoP04*%3(Kr?!hOPu|nxEr}5@+0P9LTDa)N~VVhZvxV-*SkHm z<EhP;v<-eISV7b+cMi@O5Kai-YdGpKu$bq%%BcDA zUZck%mWWvRN8i)G-8~^)1{C_uu^7^wZ57EqpB}~-&L6_DqfF~-Rzr{2KhfIVAfvD7 zU6nhp$w9sdSa(C|<;a2W@;Vg< z={7r0^n>diV^81x$E?O6u39x>-21tWX!CpSi=GFC!{WH+x1Wno^I*wO)LvdaW@;gX zgy{uG(=(k+{pg*a(&G(#9K!(o78;3st$a5V>)_;EBusn1$Y9esHG-AiULa*3Vd>VXS@c@m&~;2oktYyfx0`QOy4z-2#~FTJTaMA_{+88QhDNsv2+mURGem0c4qa8Is9M6>y^7$XZ)EF{&SLuCc9949KG`rW{n?D}-tf)t6 z=w}iMKGV#=D=PQr(o_d>`$^)McMF#dc%E61#~@%$rb_GUiMnmGFJDy4qd$Ln9uZ|P zLoA~tEi{n==U|ggT`_%Zv^^-AvYXR2(T zQe;NA_K-g&{zr_O0o(RhP$W>$bPm6&hswn&@v?r!Qk@XVGbm#E@~q^bjQ11$#G7LI zWlhHMqh(~E;0&V)=4Wcn?OJGYDa0GEMW4&gW1M00)$c_XofQ6iD#~>|oX%y@v9Tbg zK{{8Th9CH!{nOW!Ff+$QG-^+fDDP})TFNHdS3;G=l@RVXBWm}@sii=6Gpcg$jXGFDc%+w6t*fwH`E(!`Xik@M%{3 zzS$@mwBq8|mq=46affFE<8En12e57Bv)xEFR<6KjBax?!RpAv{~n8-mfWYRY`%VWxv{whHW z_}jbfK&%oPs38-0F;ZR0Np;=VJi6u{HT#R=b%nS`%QHHY6AjjrAH>39BANK``o8*4 z&t(X6G&{WgxBl0@Fwr<#RA!PfNk*5^Y;uma9eaVobW#TPZ*UTyh9CfuHVP=MN6je1 zmxU^7W0V~`&%cwESY_6qbPzMcc?5$V9n63?Z&vRF=!{(0p*JlF8)(*xsy2{E5@bF? zt~hZ2#rLN5LPN|9E+7Hq42h=VKsUqNeD4lS`*N_SIy2;{fH^T0SkBC z#q?zrrAeL{dLYY*r}B816w2R8hPo(%yxBFRx>;B$do;}=Yew(y2hRoe9-5twEwS&D z**wI*lsXWa1uK*OIReB_{p~dZ6ExcHj1h5#CavDk+4fjm!J286&ngLXn>K#~1TQ(`&tfs&ibs^h%{c zwG^S98B@u8p2Oj5WwHH_Uyxl!C3B5Ro1p_&ii|#! zeu;n!_D?r+Wkep$(#j;Xh{sjRPIXmG7avEM@FkpCH#5q>opZE9Dg21U1V2J0CCAnT zdM0;ogH!-WqsZaaU|sRQt_(Zc)%Y1Z%*{el0CWO;*@Bth4Xv!h7lx+{!tZy@?npuIk<)`O%7aWo1dxo#;JoSJn6MU-QUH; z@&8vF=6fu(;EGr6P&=*lgSTq@tx<=q{j-=DPN`WddwJ!fmUp0r> z5m6=L*2m!znz4;FR(|3%NuPZ(=M_^&$c|iB6feUG0?U?}Sa^_2t;oorHZ5Ws9UYxk z+#S!Yth97^=R}1Z3K8crIAqdQ0`_5up({FZ9$q&m@C}Ov_RPu(e;{rcF}I7V2K+bT zlu33};xUmd1D7DS7{LqD{}*?f&vvJcJrl%*PzGJ8-@Eo}p`4LE+>`-5-O|^^N0cbzsUqUFT7CmuT8ct%dtx)?+pN&wdtzSx4 z7Ch|nMW=!4mnuIme?8+e8+#ETaWxI;l*I*C?$=3d0j=@FHSmgE&~g?IbLj7oE*<5u?>mK~#lziOZ;X-a zV_(XA)=9A$US&OqyW7gMMA)<2GX!+42+$lg0_uDIgZGo_BGuVI75^F04E}+7w$@%( z8M|?@WJd%!ND94_;EMET>+WXt`$X}Cl5e3ol1-iNeOzaqA#h(b$K1hzdXb9@g_NIp zwr643uE!Q&V9lQJ*}#AG;6KCZXTn``JN&?{r3AOt85vAg606hwV~HSb#~6>wp*W0Od&SN83o9pHS-{NVI=7ybL~T)hu@CL8SK+-f zeO9T)4}T!T4>|HmC)lVm)l#3Hot3!%LK6e9(mB|3M3eP}=bl9okBimgsaa3PY7xSm zhiyC)!qUE56CQZ9nfB`ey?qMU9Ok-jJe~9o`fF`$dL<9RzyC*S5_QPA_>SGi3tzmW z0NNJIW_+$glC28ZNR&H`*6NrK{8hYX0g@rvsroj|^7%$u75GC{VLwss$WCkfZ;Zf3 zD)g2UaJ!yxsqjF(XJ^zSG0=P2^I+EfsDAd7E{jl#s{H=P+g{UKh6<6mRcH6^Y~s67 zg8L2e(_YuK*EtYnUkN#T9C#X&gIY^sO;#!?`N|#6Nx&rOv;#SCT7!Y^(?ouUQko2o ztFrZ)N3tlEeL3%f-;EZPT+aI^p!ZJ8z-=iY=jK1_>ramTb2jywEf|!P91SWga(V4b z%@?Z72@Yl)J7oI)a>nRY0~t0KxtcB)n3^_z^6E7MdAPH#WQTJA8C_6?X-3DTy6Cb> zgAt9!rC4;fWrB3XX1sh#UY>+f#ng!Cj*uv9P3h+g@hq3PYLER6$41AEpS+dd?yeW5 zu8Z@V;B5(3--?w#ru+gaZ1;{yYNuMdyBCju8f(0cWWKY}GqUddQu_MDa&5+N)_u=8 zP^oBgy~%$SY6p*PM^k0@4t$d*(X`xPyKpr)VOK+4Pq$VU>CbMQ(;pqKYX@5 zlm-Y&9vJvg3B__%ww^uCtY`Kd9%l|XPmGNxD=2>>fTrw7;a|K*R~1S44C;P;Df@cf zQoLjIw$ZTKX;}salbC#M$Xw^~NG(->YwPw6wf6Bn5zm6iu>b15>^Up!ydoDbf2o%u zD4Rw^iZFK^KTv3?#jH+^U1T?w&bL~HfSe%zmx(iaF4o85PNvH0DX5|nVcR%lGS>Wx z879@>unZ%YEblbvYxPCxS6dLV`!qN%4i-S$LO{;OLxX>Kr7zR+^_wktlt+2R-eS0l znSqb>!G-|XGhLJ3>kFnd$skr$wg4JcW?#KZVuHT|BYiuMANB@If@T&k4kW8D8!pP5 zPbI39AH%+iA4tZ!?iJ_?i%o6hhSIu^ZG@|9YSvhfjC?kR_n*9qWbqJ0c*R8*vHtCakLuPO{1U5EG^&0@_vLom#KyN|iJWXesUiL2PlPYq7X_iE7Y z(l5OF)f;O|u8S8C*BdH-b9+;N3Fqa(i&^k`Mwu?P(poVGviGAZOXo&7%C&Mh_)HAf z;4nX^(YQ3YZ(pf)5pOkcbwyaFYZ@sWQS+th`}BV*ss>Ye*4+`p5>DzGNLoRYo*FCS z?Rnc*Mv1cc#0^ea8!#qktBZa6xdunximx$B6w+b-t2a^+k&AT;N>n;=hJTcrl~-m8 zhhqlvPJYyD1af$$dkR!&SNc|7`3KCVqea=YTeN9frHt#{JfXb zXBiXb=@#2hebzVGI%-AoXdo0_CRT$e2PX^hA0#$ZF+MpsbBxE z*OJv=L?TZ0EsGLkIYG|ip`L?S)7mS=w^6iAq!E!3dQE2LP4C%l3$549~PVIm^#-`hl$5# zDA{HtiWB(Kt1t+wPIxs=kS}&P_Ufx0rqnIHRvE6YlL}GZe(Ex*{#nvom@GB2y4Z)x zk=8;F9=0d4*QlM$!bh=5C-jZNCTx>u*n&2-tRq?dOXEhY<-Y(z(xU(#dh>oCuJms* z4qdcdl;@&dzhq;dxC@dgjmgGD{mULta@c+-gS8%ZiCnW60_|1>zd=7RjVCT7PXyn44iWQ-S%B(ws~k}#V^3W@9MdAE$`JLaeiyRcNT(iw1201*PX#n*ls z=PV>H9$xVB>W6Cz28BQ^A+XQDau?g>dJSn^g__NyX-jqtz~ZLeziL-#?^aNZAVyl@ z&6jB^a&|uh<<9z+?pn8K<(v_#!9DQ@d|xkh8?Ihz-CouV`7e7N zIQ5RX#H{h|2&VNV^~{EzDE4EO_aXtwSH6SRy8 z8ZzrTBCh*!Ry-+bli6#|T<$m$bl9a~8Wr9mPfOEg#3i#W%XO!Rbx93u;GJ;rp48_@QW<*RBaCEn&l`q#%VRSc{? z7<;;*7em(MkH*bQow7_h!sQ&tW3SUxeNe$`@;SFtMOVwK0fY&dJ5>bfIGznZ4!w*# z?Pj}6+uO4noDurMGC|_uslhlfv1TNruf}2wBK@k6wQ(&LHv&9T*VIlv62mX3_K+1f z#SY?5`jgmT1gtIq2f?jo6vi-xv$3L$oXtM=1K@^gB9~{z;%w|+XQWA_#Q|A(wT~&{ zBbNp%KXu?$J{xKoc^kEf^aiLzL{73vKcpnksHOe)hs)DNla4IQ)5ven`J|Kj13|am z!Gs(f@3TFB=h%(X2S)v|x9Thw8mz&S%^|C|*NwnEC^pic&D)tm_FHf4^vHwNn@&p1 zc@bbJPI5VxKZ9a8I4$7`KbL3oNWOUCiKh&&`sKYvFd7FV$X zeJtM0k>v&h>yec&*)IQ$f2~&6m<1lU3Z5m!2;{vou=D6zD57>pMAfr3;ZwaD5>tFB zOjW{TNQ9?k{Dt1(aWdWB)ssznMuFy?jA`KJ!Rp4pB+ad{KSQ<;!_Pa>0?&`%H%<-^ zjQyK+8U0viV3)3%lk$Zu*($g`zC-|c8iO0r3iNYj9E1HsPwsiRDhKm2Co->HD#STt zdy-NPgK}9aKYO{kj!S%}Y{>^@aKEr7P+~W|*}l*Ia^m6xpKTsL?%VBu70C<ePFum~%<+Q$JyDFb)AjeIY}3E7HYC)5jxEbLI?PJ~;p zr@Pw&`nUwO{PmC(W#I%*2ke8yMXqv+apm1E;65HPYT z$3=-zITZ5>4vVJdXkBy0MgXot`%W4f?+EUtpI^SnVXG6?O>r2uC+Vos z&BI!_)zQLYRrpX`1zA2vx7-e8V2&>I>`$gk*PTmq;u}%HOtlM17-J|W%*HJt&PsKmA>)a8>2XiJIv(I|>T%ngg7V>C%X+DEZoiq_ZltX!=H6tTQaSQ>*lO?#j0mNQ=wt13LEClYC=+$ zO|Bzxt#?PrEhz40h}my*LIaJ#CTVv`NK&epV@V(52J>z)^_^bN;#m5a{b|BI+u-MDC{8IGm5KO7 zLzDpA%J&(uHyH}Tp>?H@t#Hxlz4MS`HA;gnR1TWi1j= zCXRYU3If+rt$P~I>|q`Eh`+*J3GU^)w9L${R+K^?XywW}R#*-*ptZs8>?`W*H)+_s z`!}XIAa0~SLY3K7(B#e=ydj9)fb$HQ$O%k;z-Q`%T`dLON!nz4QrK5QpV*M@L)6Uq z^Yc064)ZA2?=M&mR*Xxmu)|7Cx+q*Gby5rW#sF!^1FgP>9QcmMn1rB><7JQOvFmKB zUDP=zQZp5c9mEPJZ%1Vg9B2hihEX{o+`OuTDab{v~=63gQ&S`v5kR;${p7RPq zNsvs@g$dlygAoXMQ)<4+qV~{IB_c{8cPQX+{Dx?72tL? ztgujEM%!O&FV;=KpZ(~7^gmsy&^&pgiHrCOfs+eUT!^VD+C31NW+ivzY|>L;dLwh- z)E*7b@u$sSHR;XI#(vZE*;NvrYWk}zPf;K3gJ1Ea?^IQAyHpLlCP^0H^%Lkp{61d<9|!-U6k53gCQX`R(ak-xl{ED;l6ow4{aV^DmCjh zOSeB}Ulx=AozEo`fs8Cy{$^)e>)6C51RQS4?Qcb%6|2fW?PuRuams~~AGJ;4wVx@! zhB&a#eHeT})kRZjH9yf7oOjvgI!v%Y&%81#B^fI^ln(F^13~PoT-BWT#1j%{p43T` zb>m0GnpYbuA2leR@Cke!v*w8iuTzgnw4k1xX0%8Pv#mCNl3&5`NAzPGlMxcq=MIrsa59P4`JLe%o3dfTqpbT+k%qg= z$irtGDfTn+1BU>O^8RO9%YU!_h&8I`8=Y@N9J4B0%DV8Y|l3 zys2>10a~~#aJg|~Ha-6yy|Cxyg@U%Xth%P=S%F|+c#L3s8fD-K zSs;2m;lF&jd9lA=jr|q;j=6miva8(-i!%j?_{6VeM3^@-y5gJE2bf_Sg}&>H17CE@ z>d1N|_{A?xZ3F(Hoe{&dv`lM=POa0j%F5+T(8eqjlB`*zWIQWH?3o3j>`PO@_nRLw z8TD>Q8FO(LK2U0$ERYv_CzP!@y-s1KHwkx{tp zgjeObNMm|+8&vIg@x8;myu0}~6-3Iu>K~~l*F#!?G`JGmwNSj=8A)t)xhXV0MzB51 z(5bmzvY=#<_Z^>SR$-+Bx$APh= z0pr?ORwS=_7`2v6q7C3i(B~vHD3XYjPDEO`Ga>NSQo-*y$OW^lYoWeR=#VjfcV?Sk zF(b6vda`(Xt`!73MYm9n9<5-jOJ8StN+pFM&&qVbR%;k;?t$6J*h5xWGz zzvA=Cd+AlrS*zxDo3A!vn%A%U+;8uYVP?kzjA(Ihw56&y*l8?w9k3lcbzm!cfj@ zU??WN#edh zwAJp>^M)+HjhfL@J=vq@Gxiw4o%xH=L#Ey(DRv-of)wsQog^I^s}C#(9@Tto&NtfT zXQm12=B_i^b2MaJipv#~YeK%uE~Ddn>eNy6sqvd}qH_}k4_u>Kum}Ge&D`(Que63h zs)H|mEg<)m_Ha*wtESxPyLj141q^m;6x_Vh954O(n-I(*mJ>!p7~f{*`WVC-aQ<-# z*^b_fT)n}QT>TCCwSs-%vY$(5*{|d5-^dC)=`1&D#s~KHY)L_Hx53S~rS{U7X&gEO z%G48{5f~KWG)narcbMg!$*J6S3H>r1(6yC5(&8}c@PJnD&BSj)PVTqLLb(Q3QyM$@dK~uwqBwz!=s18kCvZ7$^sFO z{HqYzE&0jRhABj^4P@V*TPDblJWDn0jfew{&1~?nmB@J%+d0`Gv=XrCMwR#R1YKdS z`Mfph&&T9$PjZ^}xpixumi_pi!nHg&+YLtc4-EpnmdPFF>cU9{uAazeFjBqACC{3d zt8ip_Uy>$s@l4^$Oo5b+uMiyeRT2B0@#QHy(cjy6Ps6$xA;_RJO0$E?IKkUby!RC@;^VCkf$sxS#oFn zY_Dk54%^T+_uA~e-NWZ3_~qnJ6hFO6zUt-(dYQv}f--cnIO$NJivFb*yuMdty4II! z0?QD$XMfy9^^xQqyY5zXU-rM+wj_Hn<`K0wqv0dU>4jfI&-%Ze)irvaY*B07$4;jR z7{`AabloZM{p))cT{K|rZsffTpYttZ5&qFY^_{0pY)oSpcrwnl#j^xe2 z?0EHv{RIQ=T%xY`yXX65&TgGVbi&@Fa$&z-QuBmS5t=6ok=Y^-w`^ut`)i)@HeHvN z6_j!snP4FZo-+({kk8gfO_+_?g9kqze0cZv^UjEI|H(Rsc01C?c7IdT z;ii+z#a%3EWTWKe^Tff^*P`iUu;l)4G6QsH54IjF%PcGoc+mE5pC$jt4hxZB&pvw{ zQmdKSUmYOBEvy|SmK{HpBK|)3b6eRSCXs6szqMeGGUkj3I=j^-O)!>yG=8e z3%>+XT8d#5A6bgA#H5~WQEQ?8fHN8Qv|nhJ3IF-^=Q$a#Ut$e~D#_bC?X{Ak?Ejk{ z%sZqcY05;`j>^P%$c$u4qwWWL`gOKX{EzGD5Lw==0oUj3Bw__diMdxhGU56^jn5EKCqisL#Elm{V7&vp=V%)@gR4SDuu7I!YjEE1_IKk zm*qJsK2Oa_Un)m`XL>H512=VLZ+3_vnp_Ph-1;f~Ms?Pl^wg`!0oqZGViN=mnFE4#e&T@yG@C%-Cm817DGW@1ec;e>*(M4Zp_iOLR3zN;~{WXnwuPEq)q0;1}>J zO;g?MeOchtPxJSTLR-~!xpu_k(aG!!PMuMXByxm2-@loeR#`8^+5JxdF=0{H<1G2yh#J@vsq|26x`YTV8%L?tVrCm zUL1I^xVlj;s#R27qF^9JI_Q^Fvq?=GJN&1xh@SP*v}&Ew0}AVJN$!PMrvS?^C|iZU{?nDu$Y*v!m-g>5W&pLKQJ zPnGFJtf$rneP6?)gGdWrWS{Yb%REIlDW4mAFDn<5zUM5ithAUANYaBkaTrQCbAaHM z70UMAIcx#3S={Pd=g!%RU%oIpz9AX%7gbmrEDIdG)&L0^Jb(G`_XjTZw2mrs4j4l6jLK5;LB06UW);F#*4HXQhpCxo|@7D_CEPD@hsi z;U7|UuV@r+DuXI`Jz1x{D&oCRyp?T3HA^o%B| z-{poYTYW{QUzTI=~TUYh#SN*M8Y5~bp&C)^=I54b?Gxm?00gk&-VoXj$ zjI1ooWLlkJA;iyQOlA|#1SBh>eH7eBKj$daD#Wd9wyebp6)sTQ7Q7Z^$4gG)XKE{% zLv*@8>TqaVB27?_>rYHs&Qn$>@E zmgwwr6~ESg$h;O{vEe4Z3kZpW(PIs8KACRmew699-J4}$Wg`gc#YpMq{_IlI{G2~` zJHK?9iJQ-P*s!C>i=7iXk4>6Tt~Xy?D|JQkk~gAJcKOSU?NvJOm8c3Q$Z6-?e}6Jx z^85X&12KW@QqoEbzD<+-{&?DGGPSO7ezEgu4Vy;26MR~(^_I-gsEW4I?&!B3NPm2} z^+E^ziCJ%uy7^lMr1spA3)0#;v|5$aVF~{Qk;hwOIB;x@g^Jk7nP4a(}!U znpA6WeOydgZ8ZBK$NS21s)V+z?ZpuQ`S?@!>m!K!t%R?^e32><7>UJ`K8@#$@xujU z@NBUnjq?!#omN}eL5gdd@0+OOx*MB3%a$dNR8rPi5QZ4vhg`8zwK?-}B4xhD#s-1=hCdhqw?AJ5*6ph8m#MXNaZy{;agI-% z*V9&p5W?DZO=LxNqtUZZ7?Qu+{ZXC%5RCJd8PWT74_}e~s1$uye}7N}!|=^Pg52F+ z1ifyg@p0CsL46urP4^v@!_kx&)X;_-Y%1p?sqFV!O6Clz>;7ZQ%c@+B$zypFf$mCe z9*)<;?8EtTN}I#c-S|XJt?{FIv8-2Ec{x>e&zE{I943+sC@6)r?n=EI?B!+FzejvW z;3tFIoi+H7?=KzCIK(4w-(zyu;Blo> z!RZW*Cz84#mK%*hCfRP$91cey@o~grH%&F(QMdc#^-kyL*{*ktcs?J{%~lpiiz-_? zL&rKhgD5d?8L0WqPYBxHr)XQ==d?)iTV3z<-(T+yBX}M&vs+_Q^j(ANHgW^%sk~m0 z&zsNK-meko(2-npzY$4PhvJ8%R6Cq`yxt!V8*)BrghCJurZU^O9}f}^?jhFJI9(49 zmt8*}PcJ&Q@jy;y?#3%>?i+G8`H=z%Na3BLq}QAP`@=Cti{(k3k4qYpIlp6uV1E%C zso8$2G(>@xR+k+)gy4#Pe+8(v+A=Sor*b#wr15JPE znP#rcDq2U1>fteA6-c`I4yM;b^S6EDEKjLM2-fQlQL$OA_1ESJaXicfl-XDszCE0F zKFmp-%+y-0!hAiH^w>Vkip_SY`~Vr!k0a0V4xXUz@fXYsRDy=QWbhS2;7#odfMopK zJndw*!oszxqS0&()%iSg7LCCiJ7Fx~INAA8T3)7qI+wKJas4T-yMyq9m)sY?0zJfA;32xd>Mui32|I_3 zP7-`p&XVx73Tp{nLDSxZJHINCj}a?dIEv<=WDzmdSn$E(^~Oe23W3~STM#dx+85jH zCE%9?qIYmcB%V9#7euiG`9qw%aXVWhf?2B8Uy!j-l}5zC>^(za)vyQnWzX50PwcmDLk)ZnW>NYm#t>f@r_V)H(<^s#@Z($I1N4~_GthqjW{R8iPH zy*KfO)KpQpOqTPmqO=M#G#}LJV>A>ZP%_Vyi1w!fg;TyEzf(Rm6x6Epm^GoIrpQv| za48#-NO%x{6{whiAu0IKPh6gO{VMNjMG|~-$ z?f4+`K7#2Qj9&?@M`-T~5v`m4AV76AzTa>=_S*2guWEELtP>TM{bAO9pJnq8PZia5 zSMLqZ;a)XmQIN-Ph+fY(vC|+f3g!*sf4{J+d>&xI1mUFUycx)H92%C-NzmB8i?pq1 z-@LZO|7hXMj`{9MthaxDt8uxsI>i(NJJq)7AzGv=ez9aT%?hkq3 zsnSr|&mR=xFVK!8AGQ5K_7=E+RKOpzm)9RaJ)zYewNrBDH1M!TtF__toXBC1=mj$X1%=1EHj%gFmO*3!wDVz)fg=+ z@rshkNT&>{HC$D{YCrC2y_mySP*I6$3hw!MjWJ}lB0+T>126GBoXGUpT3z_f()z%M z%k;UK=J{x`om*8DB`B}I*62ckDqGTCOa(YmZKM>jW(}7QDMW#mk zLWPLZX6$Jy?4wElO*wY-6(tCgfIzA?TT)(D(TZEl=!M2ZSJq-+6QKnP_+4k-;!eHp zs=;A0QLMGLNp>PJI3L(nw|?lWudBbz=im`DOlkd9ztm(7ie!7kvskJus46I!WBW_{ z*RWyLdMaCYqTk3OHk&j_+WrMOq;`Krp_)x;RsdEWIv4?~7vebUZ*Y@^+wmqQ1s`%# z8b9<+VXx#+Fg&)Y&2=f1uU170ZYgiOI@K>WL0ox_l!J7Onu8aEn4We=L?nET#~-x~zQUuDhR61@)tb zPh#7LIj6Xu#WJ1GQcTS)g(*|6EQVtad?|Bwi&CO@O!zvYpN*%&w5Mr@Gf%^9DcjK# zAKZMF-Yd1bf^pxThQS0(-7X)^DmB{>@&HI2Ugie!f&_&q01MokA$o7W`)RI`N7(pL zsi5?7b8pur%t#EzNQ;^-KQ(4P{?}6q(p6FtE)$;EF;tsQo~Skl^xAXT{enFD(PODf zwT5VE6G*&!SD(m5P48FM$<$lk zB?oN%Q;A6Ex+M-ceF%-_)_hL`A*+@;&7d``#ZthXHN(alpIRY21HqxBitn;@YJn7YYh7QA$n&iKqXTt{>UK*qf@em;JGuXXt_}eg&>N;_$kFZat<+1%N$O1T$83y z=QbTV0&0o{8Giamod^lhZeKVaV}oZ|YMoPp2O{Keq=|z(`5O@ZBg>w4HFLdA^B? zfYFB&D)mim3EDj75)^*q+?nN)esL=xhD7<}Nzl*3x{aWx#6kE)Zye|=!A;n%Hg6q) z0%5zv!45}hCO3~YS?X!V9hYp;!R_XF-Y>{(Hk#8N^V@b{JCY}(&iMi3-0B_1v^L$Z zWT#60-CpmvNj0894%0jt6hwXrvb_IVY!Y<7&t(1e!xpKRW|oqR^77VLWl&=Y0|hex zD8o@l#TnMRA1}C|0%fSR+gl!XQyj;@xpWzg^fYE0-ggnO9kdGNyZ4aq-u5W$zW!E) zg4Ub)$Bu880@%@{PH)uj z&&;Q2?j>>ObuU26_ZwR<{3a+Vgq|y>P3J4RiRUqw|QzHX;A-46deTyLkq(zW{TNAp3FW?{%v;`qpYe8)05-NFaa`bd01bNGB))FxfW z-L$OtVbKVbA_L>9%Z*dAoQtW-7|<*0(C#Dd6yF{$$0T$t!3hoXjV5_o}zVH*^Rr3iYU3M!;6m5M?D4#!Hq&cX2IyCQ295!9z$^`8w9rkf@X6dXGCnqi7uMcLw`9cWSG1H z=kGsCjQ*Ll3NS4SNz(F-_hytAaY5jw5$n~bYzE*dFqcO0$_5i zgaw*A2wWl3tx54)g1Q>W5<;e6g)04Xe#XqWL2e{}=+i&El@vt8R#Fl#q|gSC3pblY z+ZH?jq{p&xs!9+v=Z+C|nd0jbu()w6k{}4YE$=AOgCr}!Q~|Z}byEDL4qenO3`uH7 z(e6IpmY;3U>Ai=Vt=1Wa#bKyRK(eTNihQ9+#?|6cm3|UkF9fHl;i8g z;AFL8ER4lgFxu;BHDZA8o8#+dGzVG|!gwlmD4DkE;bgV|xN*acxGjYgm|l4Idi1-e z&+XVdXSkTmQwKDe!0}gIijH^C(vosvqN)W3hdru_)+dTH4i8*W=1Ng>b29Vv2SSB% z&7lh-pCD6`eLgZva!ks{@PDW!ImB_nohzhG>k$2py?OF(82px0+HLjov$LCg!9*Y! z?q(+h7_NC?ECFeiRTV01(D?mtt2OB|rNVYjh!^lY0XJ=uuLYKe%OT_tpJ?QPp9WDP zE8;=tN|8{6rskE0Cj~M3Pw)4sd_JJ!YLpMH?-?lYHk<8HmUJo{^U2BRf>yP|{*Z== z>JcLDFet$!o;im7VxXezF65Y9HH+5EJ?qt6%-;R%ys2Ej2zdgP6zpV(W?o>J(h$Oj zzlBNN@zs)cUu}GQ8rvUO@Q;rQ+Z{5^O8~wi z_OnnS1pr;w1J%)_PN)*EXh_`3A{q`Y7$MsIF}?2NnbSqnh&9ws0EdHGoj&U@RALE3 z@rPf82DJgaUfHPMPyAk&o_kudNPHCBjIg&=(Xc+e5xjdhHtQ9B3n}ah#|-1=2+WhgHg<3ii1ygZ_rLFCc>MdChK@Q=U>PmDY?_jd@6Jra4AD~*Tr z*3d0;{(U;L3#d&O4}2La*JUSjiPxhNnd}BKp(acglD|O=hriK8@?C>PjsALb)xYia zwEX`A>Q6=#o zK;TL>nn=Fg2|y$&(p@Y^OHxMdw=1y96R+9gdF>AyaTpbNH>k9uoCek{<0ZUa5Qg0B z2=y3UrZ>^3*H_M6AM%G>4)rWSKoL>(p8+X#OR~uB7UyM+USj>w(mw*BQBs+Ve2Tl6 zas6~JqPgt*Z%1ZJR1^;}MxwAMv^`{xx}J?=Cl3FG)k%LqLr)~r?l&!Ofq)BzARIT& z%jzv_kqhTD!$S8R^SVG$t5ole{L+&4@ZfSdqSKvEMt^&<4hQQa0lt}9t$00ezHB{r z%UH>*g%t$pE^E0Qh7RyeGt2Qs7+~_IUW3O_L-CqJnVHe$T2qpNskN2fZTygLZdsY1 zK{QABeheRa8IBJk`1{p8rnlo1N9JPy{%qP{uz1?z3bwo5$K9Q>oN$0phA!=bf zbbJ_ztRWiD7I3!-)AE-IEx-i}n>16SRxdObns@RQbym;R0F%!e&Lrpa2PE9d{*hl3 z9Nt1q077=Z5aMTcQiXED-@|`anrvT7qR`ja--I~p2YdpiI`>)Q|9frknwGdPviX5-RpFiOd z^xYV}k8Mb(Fc5ei7tnNF4@~Y)7h`EAU%m!0Ji}TA4526O*KRNs-)Z_8-x-D6D7Oer zY%kdvDp&4*5at-s7^{`mVN+@3H8lPT{Jfr!<&rTRo=p2!Mz8aEhH%yT`HQ45(zSNB zoj*B)5hf_ix_I**4i~>s%Jd^AJlrj~HdH@(VJYf?B1!z9!Bv?!QG`N5l?s?34mgl9 zEo0F3542UMnZ_!dJ(-D>CCy76X#o|eI|!^7B;*Ndjsq1zBI5SDg@`6ZpalZUC4}Vt z4Z$pO3vpZ4aZ4Li>~yeZpmt}|{@#s`h2COn0-yLOK*Lv@xSGOIRNUxX?5sM-A!<_fiBneA~K|l7i zT(w{af{+HzbLDS-Y9>KlC^PNqmv^ay(_+M@nl2uz3B{u}mGK%{g<189Hf{wYTZIZk zQnM_PF#y}&u{b)FLK)SV1h`iBloQWHnA8we+0d5t9?&b7gCDr!fCaa;l6h9pyJnFm z!fWVvdf3+h2JQIbmNfy1rzCSM2MDHDfnpV1zzhsioKomEZsND?G~%-7B9n}LRI zDFcHp;uq|Icpq9f*70xF;V|;VN}5KgAV3VyE$S~#S3-@J#$2b+_^C(XkaUJd67Jh+ zjhK%n<$6LOMWfwVm!O36wwDU9jOt|6pEY%cK#?~D-<$pO=Yz0&{+{mrt^o~K2FR#s zK)cZQSLWmA^<0GsVq&c>Ypy(y(fmMu>TT<}?Jm5+%*WSQgmvEHX@XB|uk!dX8rjNRTsud{%qM zN$$N1PA0dNrCIF@gpPEomK8T)R8Z6!2)s4e$mWt%YZ@jXUaq;r^H`bDlTD-|9lzgL z6Kw^nV#K1%?W#%=q|@POSxa9tZJ~=DCx^?rj4uJ_w1p^rYNnCneI+Uoi!xf7@uTxo z6*j8@iqctyN6@v$+IYXi^_R-4Q#N9gzg%oq{QP`>zBKQ#-)#gYoNA{cQIJxg!Fs+< zl-vc;?c{n4pr?;@NA=er8 zQ3CmhLm-p1bG8twVim)4!x6*x0*{J%oV8mY|4BW5-oEW~b2)%DW1 zx_w+opXFQfVOnL0EVxVQH=7x>sny_LO{SJz&fQGoZ2M}I#38gS_pPh()SK_O`^|G;5di2W%TS}02e;0{ z#r?l2$b+HC`}jdoKzCe5RpE!jw>M6#Gi(r3GbU1iqQI zE>9Da=;mhSUf`dk{OaoJ5AiQ@O(;;;IlDob6X^%7w_HI2_7mS9D|VBr+FqCNcy0%X zo%d56fqsN6>6XRt2DE9GTouYy2an6zN1YDSM}_p6{2W4h^TmHH*BX%td9bjtPZleF z|NWbFQhNY*loX;wSzb{Qx`(h@Z^Ue|T$Lxb&;DT;KP(DcF%`5C~B42U12^G;|=CV+z;i2eF}XEt9L9lxH?d>tN!e0#hMi3Jp} zu3BCd*tcEg13T<0;0U-2nXBSwYue-X1O|QgKH_M|QMJ z#A3*tSM&&$Oyyy$;MVvlbmxn!Ds@^A2+_Jco{N3HaaZb#q6lb+3T#6RsbsXQXjP+t z5^HN~s!iskULL{WY zAvsnR7fz~?g;jd_Ief?W29pjOSFPW4-X2cJP8bbT>n%CXm#hw!d%80o&zo%z;=~w@ zFQ9;wK{?XEKU#r2g+)a{`gy+B z@8IeQW5z8R1L8qwGYbVjxi05Ozi1=gu{>B9JzwPeYsE3%8k?JxqxIICmC@<70~yox z27>K&{C@7eD)?s1isy-w@eIr14A6C4^;6ULghPhw-PA2pQ86jsLi=pvsb|3G$U^zW zMqg9#9@Po@mY`DYy-4X%Hr$O1@=$$Rp3E)`zTEB?ENDm=M(`UL@B~1@8!n!)1`6;? zNil=8Ne3@hEVt{n5fs|=@Zz}cd?m@F$wAWuueFPvz#Cu*&fiSY(%V^N0Vl^82`lk|tqLS&4Rm5Ju$Mcm+&LJ? z#-Ojy_xq+Auc&9f7hh?zkqP>)aU1V@Bpt5H30Gi#lpT(7L6-Qu^It{-TTaSzy!j-i ziz}P&HY8O-~Dv8$3a$wtE%Q?a>B&28s21fn$wBK?b{y?gcYMA-AJ0kAA4ceWFljWi zHn=}hRd>IsTld_vRidfxf&wChMcaFPeBLStvG4<=5euq z=1y5mvR}3SDNPGYOF!Z$4zJx^j#*~B{In^OT_0vg+HUqDOt+T8m(mTX1eoG>KJKTB zY^QeJD@vA-C%>@aFzw~&YzTrl{XCluLRKiRo z*5(IyvW*C?N=&>TRyq}$FqfR55e3=e=OXcxq@U|sp01vFtTzygl<$-n1(4nRTuH4K z3~5d-F3w(9W+f=pD3#g;kJlTsdO{*d+H>jg2sDyPvAd`f`==){+J+EPgB-#!=*>hI1VUeO;dB5&bNv(Djo|Hz*P=%4_*i4i*VZMBFj(*laU%Ymzcf z=|fVy4A8s6(f;C|=Y%&t!U?;bdBkjWCUyM8mnMedveOlK`l;%`?%RNUhA3%~tn3 zClLzlCet>NL=&8Q64AFc?TrZ8RZ3oZ#)$wQOYnH(i5 z65G5oo=-CYXyK_wF6Qf$hxQNE%X6D;om|Kx+aY% z?+2_@NXk+8$haX9aC*ncai{G@@W$Gmu-(2t5mT8>=xRFeIPcMEgt4XS|c%%iuh%gIk`BIfJ9xWSy{4$=KZ~4oYb}-l;muI?d??N7~e;AQ13OYBaK2u`E|Hwj{Xp>Jc*=uvEi@s#G}7Fi4&L^rBJsP2S>lEmos z8U%uah>8mOmap(tO{g%W%~lqW6>)PWN0Vupj&l-FW+0dBKv7+|L`gz#rYJVhtgK74 zyo#axM1EI1c=$&+OwLyOhvND<`IQy%YI*+AYE&6YQlJRVwYX)uP%JbyG*L(N zvfZ>{krk+*qW(@VF8jgG54X$5WlB`AWE!<2Alr3oMmA5ziq#zn0>=qR@kI~)kDpIs zui-u9!oAAuve>S>K`CiZPb^21pT0-Q#!PKEg6@ehkcXs1$T@4)+8WrTCT`HcQ&b+0 zrwjcZ5qRPvF&P;rlHL|@Ka}fa!M{he8l#8xx|#&*v4jC}$eUBQwHqFbON&gVJN&&- z%Su#_h+5cG4dTlAsahJsh4gubq=^Gz60CC$kYJpW%bz#m$%nm2VpAk(4`!yNWWhyl;{zbo9z%dn5F69=6BwBAQFDcb+^Lo_X z4{D`8epxQnXidN`Z-;_+JFle>;0BS)>ZQ= z;tr~+%3rDYX;l;(t%*nJ#my(cxbPyV-%kX6Dord&8lZsJ%~~jguP~YG8FrH!D=k!x zZV<0pCO;QXd)O<*1zL?RzMF@|-OtcVA;TNT|N8-i9$HkFCJ;*#NZV2YvU{uMiDtQ+ivc8f zRV>7n`LpL@TWoQE7eOxr4UzN3F{bGHkB^1w}bK6A|8bDWV zXKwLfido+X{t*$Z?q)5KtwO(xK za*mhQuXd7kHbm)C9AM*W#bspRx8JW?IPN`6W>yXcd)NSoDT=mjFiA;z zdr%ZOd|tSSB2}6l%Aupb*t5f=gf^@7V^;A2AfzP3V$mdV4|T)B5#m%409ZIIZX~{4 zj$0to%*IA!9Xc*;`+X3R9M31W9QP~9e6b91${2?j)-;E&88PL76j}>@l~ZFH>ZUoE zmNf;B`;)Mn>|mi&Xks`4oII68oqS+}EGygw2_9aWR1lKv%#6UwtFvl|>rK?7;f)c(5c& zfYuF`tUxFLG(8n^n0;-fR4r>TFqhKkUjoa_!YZ$!Az^AtwmvJ%u%1I*UBm49HVCN`zzB?eo0A0RY7N>fs8KXiqiy0TwC$RcGBpJTEujIe&XV0b>8$-S8B8%oKnVuQT{19QKOS|&$jo!mrM1) zrv_t7$8}!u%jP*FZkfDP^qII!_4%Utsv+?&>a;r$+bvnJktpx7^QV5WYY1P$LM7fFQtp&zu>Iwe__u0!h>BjXE&Pbs<5dDO_-#%Uq5On6>PY@Ha|XE5Ut%PVGTxX9fB#b2X=iZ zfeIZK^-irYUa6?!bhSaOjhxCeCG<#SRY(V$+?6)LEwC2DqBbs8K?&|o3&RhIE@+|1 zc5kqANj}=_WqqGo|3tVGi&CU;|MJYGNJ+TdZo{*tKhMPgh)s3cW_mgKQA7YGd0@}M zFT5utLzd7d7fSI*j#%C`JBHEU@ei7SJii;>kUk-n7`l~UC4z$Bp2P?%Og~X~36&sP zQQ&^%Sg8W_nOnZy_0+7S*fZPGZRE-q>GUWQaJ^LsykyZ{g_W&)?a1;wAH1*Jx&SJaR?%4x*t;GkK!xVfzT zQb0uwAuWRe=UF^90Hcb^@{A3?Pi%CCGK}Op?$A>oeoi77O)Zi80XcwKfZ}k_W=-iO zMvDwg>r;@U)qup zT%Ffl(M1Kud-Dk{BO`OgNu_n7CJz?}%uPH_L|QwQ*DTd^q-Df1ppiJ#yk>96C%s6edjBi}vX9+;}NRA5t^P0p1=}boat0 zPI?>UxF33w9>gUVhH?rps|9B;{U_Zt&8Ip%{WSe?zHGG7gwF;5zinoLj ziFWmctaaTECj{UYu=+ACA-J-&j-IXAiA@cLJpoFlS4^4OQ6{#qzbF zxFu7R43+$pX|4qwMJ;bCQkkN70)K%x3Gkxv9y}NhOFoTrNtr2->a)7AJn#`}zu#W6 z2*9dj(A1YekktEJ!Ld77koq*W&iyo~*x1r?d!J#gcMv+ON*t>VmWQg%Bw$Lg`(5F2 zgCC%w;x|m1Mtw*q9NI*v8qohU3v4r@5Kq=gpWW#TLEId^PY?C`J;oSR%T(cr6xQc?OwA#2s7w zO-&kVd_l1&7`P8#Ao(N~PK4`B*Pj^sAQ~omHpnpN&4sC)0G(WLDH+*vI+V*BM78jZ>Uox-tqeGA2k)@%GeDM{*-AUU>bTxt((sWG6Z85{@b96)$8rSF3bo>WZREG*n5Js>sAPa284lk=kV)} z88w`Y64Inh0D?hJm0-}UaD)?Yj{mHw0nDT7;*0Tks55v8338?tVWd>cO|7jC?C0}$mWtic>Dw_)9!zjA(1b^gRu3;pK{K+R8zeVnEg)d|U z9|EXC1pvmSW-3(Zd(W2y0R}>ijC6KuFz!!hVb?AkJE80wwEq^)SAoLTnsI$vxB-Cz zyu8HW)wViu6sUojxg`;1+csqxgK`I|#;Kk!1KgP7k!rmmDYNm^(PhiF@zG>jV@nfp zA%94oltvVc&~Q7eU>T~V&IWBs9tGqNnw_j zDp*~SGPjL?%Ra64J*u(O+5sX`p+Iz|LS>r5Z;_o`?9__Znsse!rrX#{?F;xh-{C^v=!`!su7K`_!lNyE z6$~jKiOH4z#q}`IU4fj9f*+C>jRT!Utb;)T6QnQVd4-Ty>71l$KmkEmpBo4F@07E+VLa{(7e4{{QfQe-j9YbLy_gx!oVn`d)7L8GCcS z9w=0_-HZnLR3dS?k^>Mp|A{yX8Y*H!igMW=SEN@x_jK*|nc!M^aJd5Ru7}M4qP=VO zVo9dB+~#6PO7Jx4;N)cG+)Bo#q(mK35fP9J%8^gDxVSFHbOI=(xY(M>cEDUkt))Lf zt}2D7=C5Dg4=N+5`1&WI;6!^n=lm{dCn?|76F)BpM>ioX4u3HZlhP9#l8^)oGf8*} z(TWR{Dd{h3x$K|o@FlgsIUar#-Neoj=g{X1$xvnGicO1>$*$Bf1s9^~vSwem4eDp^ zG%;ajUSU|VutDM6mtm(=Q+KKTD7LVMBCn=u2}RrNuc11=dMn7w^GlHBjGM-F#(+hq zIZ9H|HhJr~uG8)FTw7NkJ+!Y>t&7+BIzvE;zn}W*(@7$b)Lojy{MTwnqgoqF0ENNn zNbwVgA&5{YWNt-CE(WhFmDK>@_s4~CVmxXDX@9L$2SG0>x>)}s5b0+31nk!fk}u8lq5OmT^Cx2{NRtmcrg+f;U7})YR|tE*ifxIP9`ifPL{Hh zX|gJyUuC z`zbE9Zcow#ayc?6u(xOY-a?6vyN7u>MGcLKUcZ2szc$`NbScLN2|c0F54^;paL4|z zQziq!P0|@`Q7Jm!VH!4FN$b`fNq9UNNP@d=vhr02e2U`>S}X5%1{2QjUlK$Qa)_Fx zrP%k`ceQbGf>u_z3D2pMJkMJZXn2=axRc&KcpVqqcmj$3RZfK?VS);9h4X(rcLxo6 z)u%X)HU0I&SSyz8_TgJsy<%rHi%nQpZjFxj(ma8kWArfF*w z5*0m+;=HL6g;JV*sKrkCTa`^Ly_a^{FyoUz1I$(edNAjNq8|v`oy3j(Q{FB=b=_Z z;jqo5WAf8yp=J?d$THDo3NG1Tir@rQ=gP3+^M7}n6Hr!+?ai7k>(VZe{haTmbJhO| z{s``j@^^?9s>YYJu&p(!yKGZDOcW5tY5pmy?v@j&7IXc+z?e!k9EU1|v;eHNIH71n zNu(jm>Mi+25St&Eb0U$o9aDKJVWEdv%2sQIk@oX6(j*cMidJFnJJF={dwZxv9OOen zQ0){Quh=4Pm;KpanrwR0WqX>IuP4e%L*tekF%Ok>anbYJWBYKDxkt!L$g6RNS|>eJ zQ>k5tWBj+>3Eda%cULU^ZOTaYL4O3Dv4#Wej($agL=H85Pb7VoCjkDDH88L5S?TvL z%)j4Pm$d5KVz>T^I^IzscyF|N7_s7SBq+Q-AN%+(^EqFmHu&x!X_E3ZK_3U?Dh>Ms zJq0$`*`qO&oz^235Rz||+FfLT0QS6)J5B$Z2X4XKDqHzU@jzO!8N4s101Uk)IN^g_hYo&Y77IG9e^{i6_%H$2c`&Guz!Cr~b1SoSnEBO4r z_ACkiPa+=8^19)9R#M@x8}Mpbsn!r29UVt)Wl<{01T6QmZzZ#z)^lQs9_HF!u}%Ip_V zJK9^q%PjOrP1kIFW07%$(L4+c&*{BB+y31l8jmm8SkPc1aPAhmL9r<^l1@Qu;x9EO zj!+x{@86ml;^4m=rG28YQLw@X%4)vTudiH*x&2(gFzxn42b-EZ<*T2f+WFt92fTeo32xySSKYXWR(McqX~ z(!Yd646HlR%_m7=iOtQc$Pc5&N>qp#&A&f*tQj;ZdbcR6sEF90XG|oAtSXXJkyBy{ zSp8l@d=U(&5YM))>eG}ckl+8OwzK=Fx7GiJ45Cbh9r#vFXMoKC!#Eo?1O#I{BP{!1 zGe^qJ0|(%VheVDTwniMSU-j<&HAM7zdnhR{FK>`dt=NdZY&n@U<|2hrI2S%SM2Rdu zQSE~LTU$$#a$ks|Pzc^@U&*sEy;ozcSsYy4!G)wKD(g=DdjT!Mox?Re;{OXn_E6sMv++DQDYc^AaI+vkfkF$ z%7_+oIG&gko7a@(NRB*QI1(Q}=VDG?Oh%?T>IEZ}fH!m1s+Jd<)e#mvsBVU6M|9u9 zD*8K(qy%+_{zqucm&&+A5bcnHbAPPp3$}E!SSCiskz#yDz75jvk$PwL9?R!L?}rEb z^*30|yjsAugk@!ENnS+>-@yjGsl~a@MT@)Jb9r&tJB-M=(A*k!AVQ0G=f*c#SvDBk zVJzFA=ew(CL$+_FeC|LdD(t+$v)famus^FhQqQdpcCzz$&mo42O`$zvdEGL4C~-Wk zRaiU;ykfkL;S3RDaiZ+E7`S?HORLI^u2Ea*bVl^*oSPn2lKsvN+mVmLL{0d^KM)e- z>Q!X<>*A=j%`OGe12MQM;vB@qbWzm2=xAZ^RO82YO|5PVjpeSLuI9iK{lQn6sk}8evT#H51L{8HDUmxR4 z;p=@1B{UfypVq5K*xHSI@|rP6CO`}kaRK@_|TDk=vJ8t4&Z5yFMP*>zxr!kcq( zArwU&4A|gV%n_Iok_^p@McKI&deatVXQV@2&e`h+{d>^F=9@k9=|sblBf&=ekEJRq64NP9 zc``7tD-%=;1GEcJL$X{-s#D)uX%&A68u$zU7MRWDty?Bak?X;zb9}%FMIwxrM+3@6 zito(#FO*EK+Vy@={eX}xQ9Nu8IYFlmGf_UM|6Czl$o%|KYst^`x`y z@n%EdeJwEa-v2}zV3~~biL!8F?e>yzg0A3ae6i+-Rj%2JDl{QUtZ~X*l3&U-9Sq0K z#aDR0+EShJRhGCfo$0t>CCD%)XzmPUoO6=gCFhpZC(*(P2i_Xx_WHworLZlRYj&pO zYC98rKVIuiv}nqmBOQ!Jm3HJu^93N(+w7_6=;(N!=OC7pOE|ClsJve$^~h2r-{s_P zLC96aONlaC+|T1kDDenBiEtuHE@S0l_(=u1)Vsg zGYdwl#NPpS7@BGFBKi8Lxy4J%BiysR5)F+pmX#LUCdfX&E@$(0-1B~BH8ibake`zd zfEz(C+b^n=DI9}kj@b(XNXQ&`w2z?!nqnzxtg*LZLzr>mNemS`6G&6UgpGIT8^v&y z;Jj!-8j%g3@S$yv0(CApU}~+iPni!Ls__=j;8Y zHzso4-08`9(mtNp;ap@?WUR2>RcL?w(Q&$HMQ#rUV%&9k12xeNVe-Yp<3S#xUVcnMu}=-4CL&s2V~rRInE)=kut!U`xU%5|x>vAtC&!SvVU!zmvC+yi;S) zqe|za{*EX{CM9Dnq)<7%aXvZ3n6?D3&rG!FU;Rcywy?AtVacbWxCA2dKGWLLj8cqj zsE=Ky*W}6iFH+TXYlF)|J{!W;l#?Cj>KB;L*X+^t2#^`ITiGaZ2Mi%=)Z9~ zu2yTSy+|5gK`$@v62m_#E_7Hr-h4?<*pu-E{0Gymx<;o9!|IhDevdGlJ}cetCl`&I z^+h=V7?TW{l|Tin>4A_as#I`Guqfek)DmUq`4(Euws3i|8vf;lMm0H#OxD!gZZ7}@ zx_TuOG$x(KKyZ$zp*T^rRu^X(PFh>5{k-4+BiT}>@IaAVUJvDAeRX??nx03@j5MZ^ z{^$*$9q_n;&Fm?DAP&$@p-uHbz^rrc7v$pNy5T+E2V_SfLI*7ike65U&<9htnvi)m zpqJ#=7vwgdFB_p*Z*|1jOxqx0&!JH4QQGbPasPvT+YjRe2N9DdE}ZqCzWblj2J#4p zKi{I>{IV4-mQJUTM4*Bf38^`&!50X{f7N%A(@rFXgDw@?pI2SywpZ3OMui=Ilt;jnw$@fW`hVnuivb< zn-WF|{1Of2zgf+Tl9t3Zb4hl)g;2On$^MCiD#|I+t;d!h!%~o}b02rK?KxE?aNi~F z0d+HT97<6vcA6=@Tz}e%QzZ=0nN}-f10YKk-M8>qw%R%BONT81k;Vy2?HBQ<1V@|W z6R|#iKU%u>npg9MNfL-MB$F#kXB6tPO>uv`PwsCt=r`50K=Y&h*h?8uM{76b45Och;%f%lNfw$l;4>rJQG#ra$ZiRFpPaPN@&93s^q zrUnv!*DF0@^lKi%YNU1bu3!}GCi>`Zh7Th}%*0$xExxErAaWxf{(w6V-Zuk*db~sfI$Ar}sxVY!#)z#!#lR@<dj${ILv;>=ZV?nOo~iNg1+}1eI$+$p$)d=Dm&+@9ei0( zOe}N**p`HA*OL)z9V^iZoROf&=|leT`Wyt;AFXAYpH=u>|({< zU^urNJKl=>4q{Ai;qNE#dbop&qsBLPF?iWyjsIz zNQ_D%$+XJGfJrO7N@>!P4Q)-E)oN`r zz0OLcqc2ZNpKb5}s9J_TfL7VC{zzFNOs3Ke zpS~dpg(4dCemqj7oS{Ryn!;CUHiv4nMcv}1$)DFP6e&u^(?3r}^oWpywI%N3!nNS% zF=?Qw;^vFZ5=7&3hiWt))j1ARf8LLZty$IT=;Xn!q~x@tFP6<7Rkv zwMoyiXEzZ*~4&$pW-;kDbM7tnta0t`6JeJ^pJeG9W-TWGFH zq8E5l9C>)tz^hId%TS6$WA<~st_*hb1-|@3K2KX|d_J#?zV94gwk0;(%`fB_=fjc` zoRAI^r~SKlS!D)`1y+yG$WLvzEjf?pJ+W1nR*5fJ9_*fbvR~fRKA=`k6E`vheOc3c z#|iLATF|?tcu8X<-`6YQT7o*DQ)$C)S?fPC0{u&7Jtd>@C~^GYOK-$^m<%Po?vafv zt}4u<&fX%D%oFn^&UKs@F*6DXBYc4e}9nP ze8Wz)VUx~zVAJF5f$>5F9zJi4#H$iOVW^T_^Tc%{^M>nC$e9j1&0cRa%iZ;F01dJ1 z_VWj0jq*OAufx)|O|H&NBQ+Q2*8^^~eSHMcbS||06ELncZ6VDq7ze65t6WA%cCS`+ zXtx?9U^d9hakp4=qN6C?x||er-7Luh&nhs&@Y?uycs_79ec!sV5lR%PG=s$7g`NGF zVp~6fZ~;UDf-L(RqR#2Uuqb)kOJre+KpKdFsp~Qtv?$L~)c*yT`9KmY;<%x(f>R|uKl=}a6R>z9Vb~Dfw!ma8ND6L5k`H}% zd2T?v2ANG`0SE_ry_d0z9E|<&KYFF-(q?{ANP^KYR22tXv(4tXZBI|L?+A){iHS%n z=;%zl&2j|Y`8unLyweOnKHXk{r{9A2dh@fG;KrNtDo`f7U(}az9B?TI<|z9GjM( z;o^KA*OF?p9l=@uS-d`-51XXvO6%CrEvpaoIga8#G26VtU`P?ehP2pz{{@*)C+wdL zxz1?yXsuekEuJ5<0qxl%Rgn}(gy`|otTv7XKQE%g==u&>d<^hNbO5*x-SI>QjKKGA zES4rrAL(F?)r?>%#*brsfkp&SIa=|IesWtP_S~jF_4B}okc_A&-#?GA_w=_pkXo4k zCC!mdk}ef5OH?NV_Po_dQ6>Mjti8qjCtC2J1L#{vA(zvVkdM^*P2Lf)V(E6JLg+gUtpG4zB*OgoTA(z3=SX_e#Fc zLBRPkH&R85Qx@-xfF&>dw~hHE5Wi(|oCUim&GjhOpxl=Nk+@RKJYjH9bdM zs#TgWf~ssBgmewbQ6J0A|3;O4i<2qKsgH?@|7LS1hqP^&9EpZZId;{Bug_n%5*4zF zzO)~rNN>)aw3tTqwDdkjP240S43%#B9l~dyoIxC`YRcB6Y?Q#d+2x=Nt~PjO{YBJJ zI0cLmDhpdoE*nP2Mm`8zQ3A3#Xbh%wY;~QE7>){~{08j&o?tXL8X-0H9rjn|+SJ%F zt1yC>e;@@XsvFcV1P--3iH=qzuh)pH(61Xwmk_pE_4Ha%{d4Iu*>iZY>KqjgWpnMk z6MA6xrq;T8 zmpx}zi)*1vI1{neirrBG4B=izS#IQzicWT7r0iruLw_bcnq@MJrm8$O7+^HcY@sIU z4=g6V?tsqIXQ-RmObJFPMv(ubbna-bd*~aXdwgmO@)@odqr-7dtDTfZe<=OWCK7k% zygw@SAUb*{tetIT$fGOePIunJ8!z@bF$8Sxpz=(we2O8KPmw_l+3xOF7Yb z)sBp2Q75#X|Vmo zXQ0n0A_%6UXvCi-EUm0MOUFm^J1R1B zdA-0eOL*9-^k%Ep8c7mN#m&rO%kq3sO|x?o(?M4v-I5aH zp^Yx2z7QOTV)Q1J8L>I>=ET47Q-J zI*!3~eIINj4gOHUo=zQky!>Ntzd3H5+guO5=XqSUVxX9ybGq|pgrt;faG6xy_P#?3 zBy!f~wSuE*62cAHmPkH>hyK!*hv7NG<{l?aV!6L&n6*EB!cZlH-wvY>EA~ZeIJI4c z;+~#N>BnP3Qc9R3E!@F23AkAu{RSyZMlfae_;}p~5oUiyDh}5b2(_P4s4<$}Qz}a2 z4WfW}j`i#bK3*U8V9aucBPg={ks*@N)d7kklX|?FIGnG4NmuQ5d^B2|sam%@X>91K z)sgmr(ubGyyJ>=jF(u0I?tqw9j{R_>beiRwio;^!+WnAB#+2WYNRzeU^}Ja;&w*B#S(p=u2G@cP zsThL|96alrls{(boK!rWCV_|dFaBnFy@}JD1-fyO+AA9s{Jhuov z%=HPCvV8u&o(M6%1hm2ElByg+;936iM1nze`61-j;DRM^WXb-95=qks(r7UT6kp&0=^Z?;q&iWerep&h)4I^tf zI?eHnVdD&;F~jvK+TWhY1-*Ee3}tlQdCT^{GRZM6cpCZ*t0weAM&M%-@QnkZFf!!E z&@~5sHiZ%9f1iZwCU-tVF>eJHX$YpGjBSu(&j+-Cg`zTw8zd|kRtxU73+pRVdM054 zED~Yn**!2~#(gAC6vpaMsOJ6O@6F~v^M-o6{+TqLDpX{b;zZyV?r3BSg(=F)C(Rze zJY2dtomaOuxNNXBp^+s?pu^=c&%V=}`2rDfaL)Jzg5QW&$4sDqwhOicM+D2&_isk? zj=B&S3foJds)m#Z#*Gv6-Vfk=o=X7bwY4wl#T|a1ThfxJ!u36RXjyHz9m2SSlMFHd zl(_c{Mn+gjl~au4JU@C~FE;exP-V%R_F4R?c`?H2Ji-rzcJ17jWn}u^cM6`@(Xf+^ z)-)#z`9cy5hd}FqW2(%otoB<9{=Kx`ju5_XFOC4D)IV0b?x(C*e!BvW?RePYWs6^v z02#VOaJ+QJ?~wI%O--#vb(-f7px^AamtQDp+%s)+vqiAO2jK~N>$WHE-O+SW{4Br= z&UM`ad@u}Ug|Y?U4!@9Z%F|-@*la!-$QK` zD&!$MAYp@D%6$~eBjbg|WYK%l{)qij3&VTY$n`Uz&dJdT?6bdYUTFZV)?f4j3CNV- zHZ?cvJgg{=^Zd}V*zF+ll@Y~Dyo>Fz&La(^m9-=O5q=3hZh**cwaF&^rf!|X4bM!k zz>6oW2GdK==w|l;I4v6uZz?F&&gq9;;-HhQQSv<;2+4sj*IF3i=XW|fe18%|VRnc5 zKU5T-E+HJ^Wl1MQeeePI+3Ibyfk=VzjIi2hN)wJ6|GIa=Y^4qt!RK&1Sy=7ohmWsc zNKy84y?}t1o!=eq68q9OS)O+V?~}oA2Ax~7lgsghuCedISC#LVey;qkaDxhHt9N5Z4aQC) z=y0&y!}LVTdD~Iux%=bG@s`nvqZwi|m_b5rRq?)OuJG6`jI=Drr9=qDcZ?`#D&8jK zx)FVmH3y|am-gOaexHBJxiu;(DhWm<_HO|v_$jF3C5#4eN)w8-sU5sp&Tz?-wx$Xb zjmtQ=5luRKGyrL*DsGtXK7UDsyO5**H!HCDEsPx!YEZ|{MbA0C_ALMb%zbYd%l&EV z)cZVZ$2d)*)A4?Ov~DHW>vz7NUov`AQ&YWuKlqosXsMi`-tkZ}y1sZ(#k@M>^UB%w z^~I*RqGHXcHDavcYB4p*PK2G{c}LY98;}S{zNa?c827s9PY@#*FC3OaQ)Kda?exC1 zD*O5p_$8YyZev47qgJDTRHad+*_^3Z{tf$a-3kSn`=as6g)SPGJ94qt0jE;6dN&B= z=z4j>&S~93&n#}dliBqi`wKvQ*;-a_JdWOkjQ!gg)$5|3`=R+CY&(R}Xe?OuO_D{f z=NXl1wdOG2+m#Gzs{7kzt45uEfz(lSAk0L8`eACTs2Ypux^P$SU$-g;Z+nD`?kC-B zhmoJ}2NRX6Z3d(ad?0YAB-zBlJ0F%kG| zl#H#kb}k(Z_86sYo4k=%h2(!uTOv+ZBFTx$=!q>!;Az8kI7WhL@E6D=p3x)0q%0+}w;I1$K8deVA3|mqn%Y zXtdiGFqQp40PKFEr<=xyJuTp&sH4Y3*c$Qw-Uwe#QeW)?HYF&ZYL5FoL_!#R?^b&m z6B-$B2&?y5WIKZ`=x{Qv(O?jozWZMBstw>e68&@V^#NC-!Ioa4G)4eTNi_KGL;+UIK(poFN~5K}Xzo36OrdJJJ}HB79Faax8)0+7Q`x*p zCX=f1(!by!aHn}S6kJc{P%SToSH~v25bAm7>u9YR?mkN7Q{JQJi$?ADLa-c9?dA|X@aATsw-ca1ZkyS_daja018@eoH- zGqe4)RLlK-)^9gn2zmb?NzQO#D1ttP6IPos8z{DCTXPDHcrtlfmN&6KEah?dZ$-V; z79UIR-RP@&yDwEH%j2VY#sncFdPHkK=jW zyd1PPU7yw37TFx4Z@+)kDnN`lE-Spv z>hxlgX8#UH5%l9giagn|%N;-1ecZ1-`i9W|n83EaJ4eBsn$Y6@^I5b?04L@ZN1TBm23rqC= z{LQjxofKeCqO%?Rm4#M;AN2=Uq_hq?$GQN!Ml1?57%{;d@>0De;IF8B2rA#zh(&@s z_BpA@4|zF7mPFK;B~_FHVTvAe@Wi3aHW27JZ5FTEMx<*W67fp52+16<^dZ!qEORw z;~Z|zzphDf z1}g}G{$3nrf%50&gz?}oP46fWf&O5O6vYhK{Z!iA9;2q`7lUipkzqquEjzXM@>ue(EGei`&(HlBH4e{2_&E+XvByTxoW#ZhDN%W4S>P>W8{-4qowI_Q&PU^ z4w$xH+l=lbMMOl8q2Oh_60Arb!a2o*p$!h$2)5ihls739Nkk2VK;enk<@H)vSO{ec zmHst|GRv1`=XaNd$7teLQE8=7r_blM^M-3W`{_>d? zlfD>~(^>4D6dD07;P_l<4S6*cl>}Le@(Rg>nu?09R@8<`?IA^nQfsSOMMMF(C{#=;?Ri<)C*mMf zbgCcg>cz^&jhyf=g38%y!CcV;)juRPkJH0SIyy3iN#OdF?Ye$67ZJ0xB!)-V z&74khw6-Y^l-c!eM`Lpaff;G|Qep)`^2@YiUWTsANh;I+2|%ueY__p{FYbJhmVTnBR1pAJ)o{ zGj(2gn#TxGg~amBoLo#92*uoYM;4W1fXkIr=5YVd&M-7UUc?p2?xh7fW;&0Dn(t#{ zxD-meb63Ue8CJBj5zu=wE=qWGvJ=B8%3*RuK{-9e*$=D$6@ZWX1 zM?m&|KHxVsH#gdBux}) z`U0cBKPOJAH#@RwwOH?$Q{?RrAL|X_NTHU@Kb&r))%skXXFE<%VduASjrYS4jGfnZ z&1l*6%9O_gx=$C=xsQE{feN$-8EuQgI9WIzdoNg|HL=yO1o(QzG(IXS&x0)sqPwdM zaC#2!H=}cF347~Swd7fo4KC-Zxjt8%$;W7+I7Rq*Ry#gVd)ZFDWBCFQjOr5^Ug}8e zCGBwayekuMPJhFxNkitsMvw>(lQe=bCh$w_fd(XC88J(8%j&2VQYTa$LrIwDLjwxP zb&3-=&qe-_E27W_`XC?vL2@j;zF{oAkpYpr)iz)J>S-;H%M>+OAt7uI5reo9=pN7Y za_{r|MVe6!%>C3P=+3fPO{*LnZI_!?n}c^fTNHfFkvPK9)17xN8r5q3kB*0?*4En@ z?#1+*tIlA9aHdc+QqkYWC-%7>kDP%-aX)J8LW1D{qj%MIzEPE}+LNyjS?cwMbWdAv zdSvJl6xxW9!3Gfoyf_@ELx7i0x8ALP?~jA{bN?thfm{)by*DnlABVN5;A~ZTlY_mW z8~}1=JfF{-eIM9cqxq-ea*eLgz>|2{_h3ZzBaKd1q)OEq;e7d?S$b~Z&`G^f8*uyWk33xHo0a{_ zFA8}z1rBGOE8Zx{5-b@64Dvy`QQZErmYo>9h?yD1 z-SM=TK8JqqhrzyZGV1e$kRg)+92z-!)?8t-{wNcmZ9h`#gr_xWtG|@WwZ8j9!ftA-pi%+MqxCG^fy~>5=O!cjwBFiw=jfBiC7n{__P9R>Gx&x`RKQ zl2ehigeXHgUXolMet@wG8}+Ln`+`AldY)H*0fd53_|msrH!gvaBtS{5)*rkw*f{Vn z;07nJzhNPoT(d=_ixFPf<*@1QBvTZ(W`=dxV}2lF&*gnlE`hxuzHgOkwTH3Xz`fF} z8uC*b9zBKmS|Uc(qU1QKR@bBNk`g#)73DPqBR|x%rF*c6=8PLXj;1sQ8cb+7K<{@6 zu-33Hytu1gKwj}OpJ>-LN>F*ce-mb4e4P)jRIYcXTdoIMZAYw5&%4d*We09tZ&T!G z?F+M(B%I`PI#Hh#Y@}{L9}24Ao)=sok<`3-F^oI--rGTwm{r*ROUAi}*%hg`>Arp} zuf7?d*L?ew&zS~h*CX{_=rFCfCDituz~3bPIB9omxPtoFEZf4*R%jG)?CWYWSlV3R z{Pwnlnc0XAb8)z|5DN(oJF5y6o86Ws+@PbOa||0;c$1fn0LIYivfH7&YzbNCRR?pL z&Z7~1=Y=C#N1{CNjUew4@~Rg(AU~9hR01|;XmGzs$PAH$rj(}Ck**mdK$Ui5Y^}K| zEQ;}9lUmk&o_bB6)|?JicVJq}CIOS8Etca0RzzR>IT?MTL;*Lb4##Tqu0&G5i2QGa zB5;d(gQ8ei97Zc}ZOMbPVhCB?^BWRuV%vFi=kj;Dg6JCsUA@FY3cdm<)?XjBSK%P> z_erQe9RwbQm-eoBNB&N@`02x!Lk8e(s+Mh0NeR^K9pgxRjilpI#ttCN%qMMq-Lr#x z{+%~ok)&g5FKQS)?(@`vzU_IGg`M3f=8vQZNy2WT6#dast#`nBN9M%#vEN3wpm1Px z!=hw~P7^V#pl$a98zzH}biFcBg8W7MvGG@$+tQM%3fs_#W^f83{zBdz!!z1(ju&x; zbgp9gkdE8d^yeFJknhbGdgtkeCxD4d6Y{6Dym6nuFAKZf-p%Oe00INgaWI)GdT^*W zRouUI&Nec1ZRZ;(7B)7~QciF^_w0|sM)$Ys)2}g0v0mKCiTP_Qv7+b4+jBc^;zWd$ zVGWI2f=tM00#A|c4tZL^Vqk7=X=yl$?^fLG!qx5_V57x$7ex3TUm={hzK+AioP!aB zo`e5Wu^eMqP<-I&1~OqXaU&d3bJuI8`&Wkat_OS&^Bq-|Li<3_`38YZbN0s|q2uWf zwL0Gq(PYIcjmFH%+K$Zg+D-x^!Paov5b^SpwZ;f*HlZ5jKCB5FYaKD$m0DY+cc3@m z|7y=sf1r?xp<3xAjP3fCAa&i}o_BFPkZXbny`J#R*Hl(MKp#3*z(v~(Zq_8@Upc6n z@ZC`I77GGg3j&$?Uc&N!BKiE`wW58GDib*Oqs7JrA0XNgll90KbY;hhS~)BJ{fy<#AS?T7`%i| zd5xN+*#Y!E5A~j}+tP|w1j$b-rgnP)h17G-%uM)@8SXpzLM zH(GLP{VvbF0p~jhg;702D?N`ct{1B@PN(04_ZAFs9L5%4eMB_mA&wAuu5*CiUqxMO z6F~4}%t%@L^pHYl2iLvE8l$t#)6d!S`cUgMtfJ;{J5C{A#$Eb|!+a!q@#{ODpz)62 zbZhS&gsb&VKnlUJ&+9i*_KF{=IM`;UrKb-aRT(-$6hIg%b)93Yfu(S~JJ{Sb9oL^L zx}7;ma+~oh6Ye?Ev9_n#ql`T+H*Rf@>zDc?e=8V)*QP^r%9Sztv8ShC+xyGb`&GqC7}L83~42mm&3SFKPK7hDL8L%y^SFavX3(G*AuuI`0h<4DP*jTrXDp zS9)GuYSwzHb=c|S>Q;4ybQn%WAN<9yBPCc19#6|HN?#$yNGnRe*%Oo}FU4D#qyfF3 zI<*~7OR{~IPnTS5FQ$lJn{pASN_ttJx5?G)UrN$S3>c-t?%EGoghl^dV{WO52>-Wp z&@xRMGV*PP^UZquHkJMEnSv*l{}aG})R^}|>-BhLBwhi|i4#j1Me=kUo9#5i$#Y00 zx5pIcfl-#%lTz#Vk+ozn6vp=~!1p+h^8Jjs)@!=fan@^-wOjp43eCtI;RerfTf9F| zd26l&Y}x1Rp}_&*>3fsrn|n9Io95Pam*agZrB_;E8Chr@WMZOG=Kz4?xh$7Ee3|t^ zF3W8o58=wP02ev+SREu7xxYsQz7D$G0rNiD4ONcysi&r=Z;kjLiE^Lbj~#2hwi>IE zrGno!->$kYZR`$pfSxFM&qEaHd3j|O)ZU0Z`+F#WAN(Nrp8FDpn^V6JvL3+cQ-{qz zBR!7NvNyz1XyKs3v5!NeKJkJ`accA*MfIjFla09#EMM6tggtL4&l744l3*L@uiSQ9 zj?{6!OGO?d%()|LW_i%j0Ok^gbOLDwwScRJK&60R?lz{$CIYo;hTDrk*9}zj#>-Tx zVHl!^!K=qNjGp`p4B<+qv`7k$H0VtRNx8C(f(zGE*NIKPveWKlt}Sl6dry4EqMdVj zjzJ-K0xiJ#ZLEqAIkxR02I@}3|8N( z>f=0d=8~3?QAkEU&ibGUNiVc&i7;b4SvYq)d6-~RHZ#RA{dx}Kkul)MC-7#=&M(YT z`FlmA42AVU1pfmW!RK|@-HPAG%6%u4lU3>IF??u3rf=`;V&12+t=r?J$0!g4miPAV z*J!nJrA${(nVyle7l<$e_&Mv26X!KW`V+CkW;Z#)h2`Zcyf1KFTVQ&SCx?_R0{61ZE-)@Xe;QH$ znz!~lj_%K&)n^o`)jB#J(*Ee&RfzMXocUnbewlH*jj{7hF$YuQVyoI1OUipU&V5g& zxT*JhhBP@%&&@TZ#_HtQtS?^^%4Y89Miwa(YE@E{4aQPZXaDh=6ZG!nah~Ou)C#%+ z-e&lLP--@svGh2!EjI6TpAHWh{Gj-s`^mj81__5b#d>#ao^BTeT4_(7-gYHR%V1ub zl?Z!LA+{A;^gX#AHnK8uOE)f$=rCZ38L^!`4vGjS^er>pdXM8?HvN2K_w3P}MwDAe z*PC(OSA_^?5OlA3X4(gTUV6i37mX!jS<=-Z;_h}!{u>>O?Zec1`$PQ=!F>rlkb-j=C4vfcWjc%C?C zWeSY3>}!Vbmi~pSLv=X~2`$x|zhKQAj24 z{a9u%j+RFLmlh)Dqa&1GlSSl0HiTm)s(~%Sl+Pa?UD21>&iQz?HEQ>qN#WghPD@A_>=5E|0GmKYK!{o@ij@0Y!FuT@*V`*c z=|+k3eof*85u+S6#Q8wYSDY72n==U!)pV24>)vtwTdf-Vx))*1% z&bUaW)v4=QxxRo^J zDr2sr>!VGOt$ELCk>iR@L*>5g$uqR44)I{Yd-!G#8-Q)}1}%&Alin{!whsXTp$mXd z_0G-T`;8;CC^O>}jKOC-%Kc?hcVNAl>K)ffd|8}WV?{TIy_J)b63l;+6s>qgLlfur zv-ed?_ilZq(Q1X>P@H1MwsjC!_vxJHjZ@CV)HSqpwzv7aBm4O$H)n-pz0pYG8E~EWqD3V)ltz8V{U@faH`_Nu6nwFhwO!f# zp0jy%HXCeb9i|Qr2`?`n7$rt5I186@UpFGwSe?3_x`D%(=5{0-=c5re>7t8@-_(T< zo>Rn%r;yTJe%SMcz+@@x()aF*bph~!yEJ-l)JF*67XA_HD^%I%-j?=7M^oI{gBoTz z0M6W;h;{2_BMA zj94nt^Yjj~u>7)mL2q%_PlT_q{2j0BE%25^r7sAqOsaomST*-=Cu{Y0K6w$L&r!_1 z>&@F--zQ0{<}}UKzJ2rmCLt`K{OrjL-!4Fgv-DF7SS{J0Yaw;zE@^?6H&VF;LFp~ErU#@K8`$zl`xoCSnBH9 zQ*Xw_w2GukumLRDYiJGUE1E0b?kueT%2v(~6+|-9XUP&psXkuWdxlZyUx>}}Ts-0L zWTng6K}4}Z!+I~bYV8MMX+}A^Z&j#@!**s+E;`@O09_#p|E=v47LHYriJyxJBHUKg zLyu-Wo$mwxE;p2wed1D zFFJFXR2;eXw|5<>=}904^n%_{ZcsqiaE3ZJp(`LRx86~x_PWH$4~;XnRWJL{L5AAE zOqx+HW>aWhf|u~HYXHBi-{Ym$ZX`|3KKITG!C(n_i|y8gMOofN2a*%ws}7e)nEWV9 zJ}(ngJkuHKiK`UBeLh0_G4IugOXs8gW%EYHPG?>QYoj&S9;B&h>B*CqO?pWVH}jQu z8Ta*LYj*~F_f505&qMPR1Fii)sZ7;QU24B_9s)f_8U_Dp1V{eL`NDabWR{hw*$W{E zfaP&2r7IfE$h!rR8ELs3%8!s&)PQzm3iH17-8V7M`x&C$briW~s|V>AFN@8_aBEYu zBL}%#mmTWKac(^ukPzq1f#SX!5{!MCOdixvgyd~kf4Q>&d~hG8(ValheZ~WqclBx) zTPJ=j)-HI8kn`k0@p%?v%wIWWWMv)e*IxGrGUwhX6WpDMp%pQ)o-p#BKv)nM`Vah^ z_4{n}m{7_~4~)(!ozNc2pnyJcfxmLw>2&(^@axO%Ua9waV>>!N&dAL*SR9d=nvq=u zK2c5-UTP5<4WE|f4RC{YZN6IKQcAoJ-#Bc?VU?)z-*q(Y*8^`i8MD5TT+oq)#-^T( z^w6erz$cFjBZ7V*!dIhFUE47)aKdZ*P8Jg@%zCFMEr;8U5&NFw^kDk_jr>D5O|}fqV#R;fColDh+>*vU zZ0r>Y*Zn3cZ<0Ch1;h1nJ*kwFb+ z)f>fml1wGPu6z{BggUL>YKv6s`M_}P#Q$W7ShRS00SK1hu@0u* zXH>SurZaObLqm4cn;u#Mo>$g(2DU+7ecJ_x`<4(D;Ed8(CIlF%vJV)?K;sVr6^5Y(H|_?XBp& zS!w%B$Em05^keDjfmH>uN;L|RKOJwUR>yZ?*|xLSVUx; z@+m3#5P+UXZ~I_);=8?`92O)f=FXq8zyak%*>W1i5Q7g%#}h`}eWs>MoRn+UZ>0kr zPWHE{NzX5O?r3@+>uJxgE4}?!Tb<8${2(d8zaHmWJAd|gL^Mi$*Q` z`VRZKqeEUk-UsT&sdc-w6U%LrVyq<;J_Ym?#$`%9g5E&wGMd|l$=!U}2 zT~Dib`aI;kNd^mU2=m@~5Qb^Xvb!>pK3If~HKJKX%rLRShSZsuxAq=e`N8=AuDP!u zoJ5C4XOFVw37eJf*0}M(@q6iEQelsTyRr7@8p=e|UaSrBe~gM`dp-WlzPm7UI0F!j zHgB{VC@l{C#>#ARmX$Puhcwht!X!s&1MkyTIynrx zSgqW|x_#2y;eKM8GM+psQ@3)I=E4e9|kd0EyT&FrWAP>-E7IgIF2gNw-h7E(F4|(KthM{YhL(0}kqUpyHY~08-6Y%XQ^%e@#&jvVYAKwF zk@e(medUu{?|t9T;o;x^Ve75`n(*T9aS#whQo03{7MR3<5egDYiGYAKC`fn5RJ!qn zbWA~Nba$7ubc~LT9xz}G*!F$+{_Xw#e9vESf4KKPpZ7e^ITt3-7Sf5wB2i7?+opd@ z`~^FTxz9h;I${&sR(7xM1}yJb+yV~W&Z;0dgu$4Nm^kDUkk$ zq40s=ivlj&WU)q$ucf6H7Ss$hnFoHEc?bI5Z(2Jfv%ZfKvLN4CH4BUw4AdlE+?ZRO zd5LZ7_;u2O&-w9Ome;*Z#*08*J(*y_;=DjPCDecE=fC-J^x#XdQuoCLeLp$k=fbf_ znOZKD?@6?`hy z%5&k@pvgujxM-kn47OPYR(1HYf&5P1aE^1$&#VuQT1+FwBk@QrRJTeqvZ}>b9_)iqJzDJ(Mmb*=>YqWA*-391d5_!mPE(L z9S~cxfLBZ3-V%5ITiCEJjt$*Dv}z0lszkTYOGtM%jusFdFSbM_fw#CYHw&G4tZ&GZ z7)n2&n)M7eEouKZT1qR9QfOd5jc0~>b`_tj1r-$+$smqk9&nc8VsT>$f>svQO?{;g z!9r>qzbf^?F{nvU`T}mb%$*Ulr>@vrtcVfBEo8~AT@M1?;U>jJp-g~Flb9$Wu?H>D zDGd>s@1#~F!QcPBuXr}%U}tZ?RPR9K*D{*xo)&i@1GynZs3rm6P+vhk=U2g%XFEBz zuHzzJb#*zlmrs;)Z}82HJdVx_)^u+o45WMm17kzM-OCM!r<^&uA*#w07eO3F_dDuO z&Xot4#oy`u&g_u{Fg+?!NpOP}?K%z{Cwv!j3ADdxeY$$WI$xf-TP~raZOE0YckKbr znq3!gTV@sjJw05ga6;cB5Qz{*?crP^7H~hQ(;hyq-(`M}^!k*hJiDrsetFnd+NB*W zyYXMFiIidJEmpL{OmL2Ubi^%-1+Jig*!Kpy?LD6m(jU5 zV@%CN&vokNkR4b3U7Nw)uWfm5xg24WI=jRf9oT=bp&wc2e6Q|xmhR9YeUKck-wlvh`cnr=PU|PmSUbW^@ zg@q>35JfokT9qA{y5Q?G7EH-2t1P=Z$9;Zp_^#g1)NlAh#r;aIu=q%;Z%mTk*%%U* zu-~y#-V<6C=d>mUh=?EU-J>_ZkA2zJs4~L^C=&mT+Q_wcb+tR!Adg4;N$^G*XgB9% ze>%=$(E(ld`@#~+LJ+DA^_{jjOERf71}(}~Ey~sa2-WWwGP9R72sw%yB0JB^@gi3L z`1-GPG}C~P@eBGHEFnLn{`=eF7J<=vyol)g@ai8o&jz!gvGfrI0`fi_X@(gty`3pN zm3&{ag(}ls@J)7BQ98vbh!$7j`9_9Sl$UM1x7<_iaA|R~h0A8k-ItS}$9-Q9p^A8F zc0+@5Yy05i*^BiKyo%I!H0O%;t_`*yG6dj^%jm@#5ko!SEH$tnJ}^#lPA%YWQO2>{qE;NOmgq%qU5FENU^#5dLH|}}HaPR3#%nF?@>d>9*zeVL4Qx8-=^@+>0J*;^j$Bq&Nl}%`9w5ph`XpLz zJEo;gTU-o_g*(uYLhyF&y4S%JWWVfu?Xm)ea+#DhI%W^`y3>dJ00VGigN3i3RmLbi z;-d{HmIeeY#@D9M8C6}H$5jJzw+{?xLJo$wIA--J5!m2thS_j6|iWms$@zY-2SFt7$jAgd=J8pCq&gywM+v8zX> z*MvXfHquMBRlKNlyQJ@_JWUktNS=>AN(mzQ>L^MEMig8h;}{CAit zxxkJd>G`nVI*tUlmxK>r$9AmS-(GRhs}e@oI|gQ@cO32B9gSLcuVk6z?9#DpKm3SX zxrPRxH@t86C17NX3O*&`f*V$aN`ZWj#<^V;2T|lghHL#4U~YfIL9Gd99|%pq$2~ROBl+qxTsBH^LiScm5e>oT zsY#A(W?FPxx<;jUTt~>GO(r+ag)8kACOVpXy|!Zb ztF8N3?{^SJ2v=)R9e?Pw;;;;MUaiez!j9Di49-Bt);ii+j%r6s9QQ7Qa{Q;&=BpsK zf4Sb$=NhO2lKZ-RI9ZA|>SuIz{6Y~x6Lzee(#=%TVuu&7Zbrg+vHoj=)570FbifHJ zZ+HfR>-Q#nFgBrO%{ZO#O+pd754LuoKjW0k{jbNnigMtEkdoWH!G}cSi;LS3&YNmd zugwUw{ZL}AS;(A}Oc*8GLbY{Y;En!_cbmDzb+7$dJS6lHz{n>%H*bX3Kr(@Od|<1> zYLT4wJFJ?_81z@;$!zIMWR?r)x?kyz4jH`A)_lE%ggdcD3BOaZGG?{ue7q$P{F>sc zYg3;J=egBCPC|fMt;Qb72}hu9?;A>uR3Y5g^o%r1H(OW%3Nr;pMX-{5_hU+ifciEl zIgLCQ=8rlChwfaE&S)@M@;~($vp{88BKp91z1V!sesKh|p#!lHf}85=YhJ=4z)N=VbH(EaQDOy1R0+U@@1tU=BwPC{@{Bbf31rj8B)S|Tph!r#K;ef z^`q$jhz9c~-7sb5WpDd7f#NddD{fcOiorX5_-=$EI)Yxnd}_mvImp+1C`$3#`XqGW zVe{?kgf`_5IyP8lgSHVD?vMKQ7E{-c^r6$v&ujzZAe(3!p%Y(pn`av=m$18G!POp_ z7ZScHU?l5npve${zGzcmgrgKbvCg6nv$)BwOWkaro5^9up zdaXWnw5|PCPAa@m`@9K!%Z=OT!m}V0D)-(-X_OF!CwDe_@RmpaZ~RGU2waC0Ioh7o zvA*WG@_GvhH(-z=>5&UhRyJEJ%)0kozWJtqC(H4tp`k}QX#J~Y(53K$x#*w=al z^#gtoRcOy9G_0ZHgXd{K2y(BVyPkWRS(!6O;T>b5&OR)e?G;q+txUQ~;LZzoDy24g z6oy(PiP;VA!8xX47{x9ptz7OQ>%DaG>A6>ba@u;T;^o(2>rFd?8ya5GL}7py!Q}j*O^LqYP|V+YRN8Ii(+L&X zDfMwO2mZuSz6Kt{#x0pC`0K0~X;`;AJZ(TZ>t~Zhy62-=gUjgNbca=G!)IQ9n;*!l zjD^uRUvRP~as3rFCEp9QnQ59HoD(0uRewT)IA|>rknj8)O8N_`|5>ifeys4c(o2+d zpB=!}_pVWnMp;OHe&=bl!{acTcT>%^-T|^%*qQSn;flrH;*PO&S9VwG{S4P%gWn3g z^IoWysFhmsI?#Nx16w>J&bem1;Pe=uUB_6*b+fOj1r<*GNQR%vl! z)4daDW@Ke&FDvq64-=N9C#Kjg^h2Rg!O{N`#g=U{rjmr;0Rzl<-l}XqzG7FM8ftNs z)86eNdc*!n(K3NTjrL90b4XZ_Yw!v=VYy_ECL)t(ej|iazK70z_C8fQsdvo^1PI~% z)gkRMAKrUt@K_LmQa0Qi`40*`YdP>58d+Mw|Y`{idagmrD0}$V)%oj-*%u0B%yF=GvlNh zg?bOG-4XrO^`*%Bpq^*eqF>nb=2Xf{N*)}Qp=;c5ZCYrZkAo1MbbSx!Zj4RvB@M@k zj6xA7GBJ=qCO3nlg-qOs1g?LLUIkxt1>C~G=}uLvFHO>BeOL)MRK)@btNaI0CT+gj zK>#?4lU_MhO(gm^Ta>&_&*HTe?U#rMZZICrg-Go>cl148Ry8CU>7Cj4zD@vkB5s>S-4 zl_xST?RhViP5A@B^s%=fqV-sSYJ8v;dAWBPI5shn5T)2#$kyU(Ao%WSvv_}&QeLwi zD@7~Cee?OM?9KX_#@qd7AM1W-2%Y}a9-F_ zJFA;{CX*ij@e716KSUJG@&69zS;t+WmuiBjy`1b&EoCM}-Y$lQQf}A^ASxyaOQiWQ zE<^0RM7xE=q4sN-c+1Y%`1o_ygvF3moWTRBrdTP;m4ZSN90r0*y@=+jM`f32uYhtWH<3uO6st(0;f37nbe(16 zQBBv&z|I(1-6vr8mz8`JZx_PFw{-kulk|00mFt#}p-m5Euoo4O7}hJD*>Nqwtp`W=$!I#2%u@3&+tmn?GSs6Jf#WF28swaXPLzFQliaeV&KV z`rUk!K*jG$qKP2o+)9pIU9M}h!2R9!eQh_sE&q!GuEe@o=0o<h2uC4Ps z?xHR3dgPr_3}058XIV17P!uS>IZmGGU#1DRQMQwCa|wP08ikr~^Q zI`)swLKIE=tv}uk>JpdD$hX`rmnzo`oZU#C->I<~m5%?)wo_1{n`b9I@7;MmxU#-1 z8{LA2?OZtuXX#w^vZBi!JTs$`?1!W&)&?)yci;SOAwPTdrEcGk$xBJP;K5(%Low|o zV9Vb|6y_EFE~Z1~I0b+gH5?Q6+>9)0^{ERLG1_x!eA+VU4{evz+mxI{Hc#qMRhoXN zltX_P_9~GlDX@V(Rf>2|1TcCk?bRIf*yYxOr?Ki-#HG%zBLFdTrSww*^Hr53ZI0!0 zoTt=iMxIsL+T@xT*6Mk=dLN~x%11%cs zC=a`>#-<6<^AnON>7_8DnzB%W3Y-K+e%53{bT7;N&MMPswS#!JB$=&tlB#HNWBJBd zQK8%IZhqASEqlw?%-dd61J3Qbz*`%0V8qI_j#-*wRbA_QgM$x4#-*=V(7>X+;nj%0=hZs}kp7Cz1>BU>s>reoU=#ZfZ4#$L`P9-`ywO(J}W z=GvFnX7Yc5w{CP6I!l?`OCM?4hUp7K78{5@$^3)~%Hr=(*LCR5sP&s%zf&gUt!8B3 zQ)pm+Qx}TE0+qD@P*Bh~{0+^>%-vq`LiYA{28g!q@hDRqSsGi(uy5rQzz&})DD>HK zVl~!qS3vzV^AWSTHrdEU7X|FY|CXhnqLZzer(KaJO<4E5KTXKq1wZkJ!1d6 zucuDRcq+)7y;kS*rFZi{V@UQuj$?;^=xvGeZz_g}$uQCIB1>OZ`6rOoX{RZ6(Pz6G zo{yV`!=;ASyv;PiA|)4|cWNOQE^rm6ssK(a!gB&n!58u@I2Wj2RXe9PN%Dp`6ZepO z`ohb2-uz>GMT_2(!m7HDNONd(VFmFh}&L!;)Ib|HKMRexs z#BURL69=lgdvYx3UG5Vnhm#(3c)u2<=sMbnmioo`ZLa`kt@TPsH zX`S5`&*X03JAGA|RRgI_9Sx=|2X9VD?_in`K6E3D3z1rN$KZS1nmARRukX9({c!D$ z&2492H|V*O=QY!xg*U>~GADLQ*IH|TMeuT#ojcj>k!V*8@$$oL?|c_EeFrsD$F(nZ zofQp@o{v&S=G4rL1AZNh#E?`Iz1N$1U1cB+aHJalY^$|XZ*I{U{Cp&++)d|;>wuQl z%8%uKX;103x$K7>MX!HmYMA&AK70-^2cDJNncwQP6+MPLGOA1+{^VPku|hAm7Zc4+ zWac{2z+BDz4yCC`UC5VDpxWaUiluBnK6Wc!INU0K97zv@DRn_2A|fOm*}e}9JWWeW zBMaC_$Pk-<1|ikoU6nt#iGL-;dA>o4>gSh_qytux{-hE-ZFA@gEGjP6?@QE7s0Xn6 zZ~%;~UIjbcts$|5Y|_qK!@7YY`uYmJC7HKbxghi@9Eg_&U*fvptqMVBxuO7O9|n}R zg#?dP!a$ZrgGjvp2?kN;f#P5E+y6v-k2`7EnB=is?8KKY42;@iM6mdLI=%A?9nkH~ zcqHZ?Z#aD*c1Rb;wtfd62)G#iOMxefXR~;~kS*=@(J`MzLcN!*Gv#n)q7{e0P;P8 z`&5DH@aI(!xPxwpP?HkkDnr)yczJmJ?s)x-nhQ@F>U}9S+prlcYAvb7WX_$y=~y#p z_OnuG0)&@u%<+74`(3$fVH1aLV}jr=ikt6Z%g$ct{Rn4IOBj!+S6xFhfyYygEc%2k z&OA>7CflYCe`7x{Oeh5Jsc`MCg3r4!H^5F6Z-cN*ko?h`8>{Nye@jU+$tZH>{o%T8 znfHh`gFSzl<}%@oF=vx=XZGHLBqorqXIQ>zw0PNI-n50onf-~g%AaFrDZ?y2m6xN4 z;$uH~mUw3&5aLIE#d(t*-KeYZyt63?aH{riaIZzmWpK}x^w3SV-}>%I?gYLIM(_57E}+&x}4XmJ#LzECw7E& zCT!c*pHESjWF`&ghNQZL$oO_kPkg=P`=wBGJopv8dwW`Y3~FJ@$j)%B{Lt>4rOn8V z?Qe?-#RR})Q5Xezy=DKofvgo^nj4!bCdU^(6w5p{ZUaFR^2&%?5EX}7>fjojUgG(8PdW(=| z&iB69&HiF3=@BctZu9JrQgVW}Hl;RuS-u?og(-R?XB?`O2y_|4KFb~(9a(UKG6P9o zW{C}EqE`@N%KIS)pyk>11OV6+ z$rZFgX)c=#K1!*5&D)37-3MEQX=%NZ2LTzwI=-#?cg@d!@v|lMR?>4ntS85<~Vve^w$wzj7X`9`C*yi2+sPpjr zY%ZOyi_0ECk!HBrILtoxQVjDRzWiREbK#y^cgdneYmRvz{^Ky1OZbY;_|KfUdUwfd zx}77R?(9C;m(BpjZQ!EpN{>x5)m`X;CpA1>oJT6|C_(cmw=2BTbSQG|{*CwE68i+Z z0{g?WY-h{nG}^Z4T~5a)HepAe#F0n?>#rSg>jjAU>dLl6!Ncmw8)BF)hYG9JVDZ-% z_f+!5rlrwwv+;|d<-X6>c|N(EGXwN$sZEDxmID}n?^&X52jZU>RFwx`(1}x@`$6j% zQQxD*ubF*8w&HV$La`8l65dP9{&*!z(vwzu2+V90;S?UDo{op8DY;M7*yHbUVNQoI zsg?nhFux<6ms$BgVZs43Y!;B-F0Yw!f`?^e*S|!yc5m8$QG?mLLP<$My$*}CxZO?g zVd6%W>Z_ea+tIFxRBPMLnVJmI7C+0TsE{CZVPs#%w~DEsRDwAfYCS|EK&xgA4#(il z2ZpI?Da1Bp#C6{KOhnnzt{Ws`{#1UgD2|c?gMqoiO+k z|5ChcaYArfU`C8kJ=8b_CJTQs96s+kXK3U;rOI?x;TDp83~SOilv+!VSCgGx6e%ba z>QhtTv;f3)ubzYgb>uNmI?-6ocLFo%g0t*A$!Z4WX^j2|5I$Kfc(coYc_wdU^~6`v zX`%8{=(=L%Qe#3==!I?YH7^$;D&0KKvhDF~X^mq43qvFNh#I@q4~@=Cy+0RRfI}a7 zPnuuy165gEth_L3&OE7QQn5go09F;0|8!iyPnd1rD1~SXAk{wSlYs8zV5zQjVuYt( z7lUy|IINPQW)rM{God>^qEI!BF}<4|Aa?*AWXQb=1%<~Z3-?!=cBS2}AS`nHx9kpN zT+@U8sBn()lXC>m#jb)o9(C#kmt^M^K6E3^LwCOoWVZ4OZV_a&X z+G#OdNF^X+O$S*#oQ)&_?{;fJZkI(}eLaRxq*)p$RQ@zzXB z-=D83V%e1V@7s>8EPeZ(M7oM=(*n?P?AV%-ujGWNuBZ&g4udO3YptFaczWrY}%c*N-9?68PTyAz93a{P3zThX<^mZ~{THZgJ=E|DiN*#C0 zCW&Q3-2kmP-6nRsV$yCR$JfUjbUB<^+>RPsWSd4QbB5R&$lb=y*@q=vyKfo~K?!Wl zN-&wzSinh?-&i?)iuJv#17Ceqf-}ZQ^F;obZ*26pKj?Cs zA_Huc+XS`$fackMzOB(wt&Wv5=r`7v>AOo765U+&5lQs*oWr?XtOVZVCJ;EAE)Kq1 ze0du5GpD$bB%kEl_t*%)6UI0h%jzmoX2Ti(b)tvia$me!+&{?ulr|w{-i#%fisp^- zV&GWsaTnyO;gJz-%CY4kaAq!W3>V9qy2t)6TC2_X*s595<}dQ>KcEHsq^R2jxB5d6|4aP1-zcsgdUm?MHtYLQ++Y>MPwI-k;VCSG_ZI}DC8EffKfU>#%kEcH2 z#&g7BQcgt}uC6=HyTvfh7elfKe+|SrqUgD*$hOoP6(n>n1 zUZ$nlRtf|M#BJ9>2AJ3Po;`b3sJ9H2{4a?i4_Y}x2|?`-_VPqJ371OWb594YDEv|B4u3XN^83>cY#4Dy1{e^CUgg7E)z8Zz7QGuI2cSanufk*M`3&A5Pi zp}rgd)bU^Ed^f(1fbC*AcsjQq z6vReg^f<+=Y%ZjyO9Fk&3!#5M#aL66{dHB6(5x!EefYn{EdS8bqWaE*imx|Ho8_=z zCFxmtd3=pB%&mSOM80jqdKURA-|f@Nd45&t!(!XOz-|-!+yVS`p(R0F`*~{DAEwUJ z_25e<%f8!3UW4w<{Xf?cl}u(YOw!EUds_T0pG(c@&ZQZ}eOiSZU3;c5WM&I3Xv+VB?c4BOJRq`9XNe&Ntwn}a3}W$1|uV9SiaQAEwg{M};A#i+iZ zGsj!y_RZUd6@PUFf}DFaU-LD-M$`O)2?k7f zte?JRyqN|X4j5_qEcvYw(H@XT7I1Ujb*uZ&m4Q>8s#bLSyDw`KB&nYwm% zzI>J<>?YO!xVl88uNsdH?^C)Yy65{m{wC`bR+X%-Qfq*b3YWu<1+|NPk)Jk>r&oPx zowU0cKx(bnxi6G0UG3EG>glM#&*dRr-T7p#zCR z;!$4niFQ7{53&O;3OEN!i}T)aXz&tc?mBjnIxNMEf}1?Wo0zF7a77yqoq(QaZ6BQ3 zVr-VV(lo72m|)9w4&l6`o5)spdEKNPa;?J|=1bZCMK=9gf#J_Y!yCnQ=P358K^`A6 z`UqYF!n!}7C3kW_d3|(wr_4G_fu61NpD?}He%(76OWX*sd26KX-%r4&s#EW`T%fe3 zdNk?<7a!L2BQ)4Wi)h?AD~5weJ0FMFS22sTiV}VgT1Dl_qcU}Ok6^iZ>JwpduSR-N z(dxJ-5PX#ws-X(pwrXjX7iDLj<@GAQu#g*2T5t&4NuKxak%-8v$n2^-l8Y<#Y78lbEgC-1+3E6fb+yT|EJ>rgR5f7W68Cunp_7Jm^KxO{+KB#`owYbqjqxk>r%Wy* zuUaEDdcQ#b-ve$DVF9AivU`@6Yqy`OrsO$~AS?f|1LjL7r8hF|1|hE*{A!z)RX;|1 zrNwzM{1*Mgphm#-_M7hqRcN~2U_sE=p(qD&QpGgV#>oFPMtX7Z7Ym&{E?)INhfZr=34x$NFiX zSX1w5#Kh5aI0t8wZo;VJ|QN|9VF=_T66yc-rYC_TzTbQ{pIr-%c&jhd zC%a9XZXv=r@{7Ryq+KX67PRFnA1KF9NXxc$bk zYO8fM=(6o!y1WUb&_%J!JJtoMs5OleM{Fi_-JgHB2Z`S9yaZ1Bgq_ZIes} ziK1K3A)bqO-POMP@dzcP?9oa9$N#9WDp%WJZ*#ON!D7+E@nM`|TiWxEL8vK-Qo+qrVB1Oxr<2Jmj zcX=i~a?3sMnttIcEn@t}Nel+0T6x*-*Z+Bz_5{2c@=wh(l4@i&T&UE}6OnOPPkl8a z>Qkc6aB7vz=42C(=}2?6vo)tX$l_Pne?2xfc8WuHK`_``_sSHtCnFD~Wc&YKwjnZ{ zduKah|F^^fpmPpxVbg&cBs$ZLjVtL>o2##C=;JMle^$oli!8glQ3OC9qvI%P5fljO+Y$Ak zsU%r#GG=27gZqOaEk^QqjiQzh?*0wp)$GrFMD0F#A3w2+g#pNoYK)&f3Q2`}r;xj- zdg6VIcGo7&t3?gXX}6N6{lp*ki+^JlSRsI#jQBqfJEyQ)VfoQWSfD!w_}eQD7@B zs_#*A;`-hAy46r(E=78zzeLfEIS%30{@q^TuykxC60k+l-gGyqx4>&tK0}$UdLYj84nga}ldF^2TjZnN1>260j>d}$slod|YSSogvET5QU%(GBYINJ= zDyE|8PWQwQ5>)tD0zjI>Js3OnIYxud>MBFny|aIeNKuN6xw)6tCIY3_Wa>U5$dAhI zI#bKTj1>bUB<9{{MbvZTsEQPQPZWui>>4leD)q6q$*ydd&08F#n~@|^{+OR8rY*IW zI>YXi$L1Mk@;EZKm9^-SOGw()Y*GxHWVu5~-7Y?tI1P>3idi~MVPXmq5+c7{=xqZl%-VqomUH_%2Tcx6E^F<`*B9?wk#|?ioR!Us8`=)&|7%p+W zYl(Y0A&;u}CNjTn=TzI5*`y?!y`&*?$h2kV63+2Rx?RaGR>H>Qc);KD0F1tI7J`3G zP?l~7)W#fa{ckGve-P!jx8bT^{=4*O*&t4vu!{a#?uJT{I;AiuD*zpOcLxiE0|qq= z4Fk+~$cJT9<}UnPpz=dS;Cfv`>vYfuxzPbeFC|_3`S&~m7?g0k1zao z(dzm8x{4+(ZJECM?r%2N3x>uTNb2J64M`HgMzSVky|)~$1Bq%53cs0))_*<8nzdXd zhlF(#Mp^sY%sr(-?1ew`E~{HyY+=f7qqeH(;^zR7%4F(wFxqldOM^QiDp$W>4sa#C-6z!#4U3~<;=Y`bZWkkavC7UrCeHNk>4qMcrQ2_|Nvq0sq7a?I zj9$+@8MRRifh=ocAjw9(3R@z5aF0~MqyEM%q<{Z+f?q6!Hkj9URtItnDuN@|IpIXe$S5GO?H48(C9Z9c)B+R zBn}X8D+x<)Tbl%ZvSK_FH4jzyECJ4!TY^SaIPa|T?soXSdPoq@flZMXRVg?*_*rmP z9G-b5+En}7ckk~jf(p8KrTo>qzbo1?6+Ap=DsQq5g1s=4msxL)qGilx!zHKRC%3X)krrfL_yUl%MnjAiEOJiWLz=R{|ypPi?-M}u;_f^Iq<*?J~3 zMWNCviOjWr!LInEB7AaH+gny>Jff$n`IaA>aef>$bMBi=e%~^>E?f8M<(@$VkVMPH zhD&htTPvsD90N^>n#zgyL}vT8-})dDy9T=6AXWNvbqqRNLkQJ5(90o$p^uNtIYK5y zJP+n8?v|@MvWAp!HCo!2(+Oz_eWThhR;pJ7Y%hYIPWs=_$ztR61BQ@Yak?P~Ga}Ct zJg=qAfeSvWdi4HY+k?5xGmqtJ20`k@`nA5}ta;cY`|}lqVz$1{lJb<9EigfOYI#se zVzD}uvg{yCQWO@6Z`h$EK4s;CpOcRt(sr`|yd|0p|Dozm^|6W;~U zX+l*^b5jICRT9I0@F8ww?!{~Z2TG+P(uIjnfB~3hLV}4kz8)QI}k>gQQaSk z5zQQO4`KFR=XWrFJ(xGZXhnVQ9sJ>s=c`zmf1F>4@yYCz>-zHk|oG~6`iK(h)R<7T-49L zhIojg#oOJKy9aMB>)KBiEJqJ5ZUdFc{%4aFD`5r8O(bS4`osSc`%&$E6XW6GX}UMQ zw91eeS~WWRGtO(bcB(gPFiHN7fzeh6VZz!#y6gz`^73+C^?YvHm@^iWt^0AiC?O~G zpmYU$;e8pe+03u46eY)PNxjPw$Nd}r0A(e<=hJ*jh=!NLIe34FI5usPZOrOwTd4{Py1A~W#Y=|cwkPAy)N z0m+hB+0*xbk$z`uVy3zQIs)y8*%=*px+R936Ppo*Is@M^;m#nq7`NbUQ;{(-0yXof z*`EY5&MSXxXp!>v>o^h4i=U+D8AI}sUdUi|xLvV3oUeC&KGfV_7yQ>&d~Tp6-&~g4 zNH<|>26^c|AqFdoT3*Z58MgXsFTS_?J$m}Gu!TOcQnLVd%b{6SiE~6zA2}h+2^u&*z+he1pjdDN^&?hRjP>6Zfv$m9Vxvv}-43+zqvv6dX<+ zyb)#=ieU)A1E4~NMTvI)PHMQ={=h%0C!y4NH?zg3bV^q*8l7chEUL1vZp~DiCcd7W zMfg1?KAiwG*~O-J9YvV=cLkmPq7DhC245GL1ulKcC?p7srbRxC4NccG^k}P_+=>tj zCQJ&P)va~d4h(!sao@bCIOu}(=>%qF&7g6VXiIeyUoxM^;V+hi0bzQCxjH>i7kP{^ z_y5mL5W=$2Q6TWf#_Ts#D0u2(Acu^AYfe_a-r(Z5ok7Cs#AXN`L0Y{l&l=lWdcSc> zU~Z&`xVH_%!oLZ|v$4G#T9wbYqm8O;=0b4K>QgBD=UQdt2xNUR`DDaFD>90KK(RFo zhv@(DQqt(2KD~FQa8c>v7Yx;$d&aGe7)?dwp180$s$XtvRU~43!(`MO?QPdW6yY+k z_^M=7`X`$TrDzd_im970gD9_@y#7AIBQ4KTvnj{6b@7kS*mFy4a@MMW2N!p|yK2^% zhG;T_Ld}7GIHi`K+hh)i6e(>x_8(6%3AeFnv~be1$Le1PcsZ0e;I-#-F3*=h7$3!$ zeg^)O#y3^lUku!x?+t8oIEPxt9K-xN6RT?W%qBeicWU+veu}7M+jEqa~N}R6LPaEnmb*Uajv&1Cbo z%I(_Gs+|$xs-0!0CJCtWhxrOqnSfEo`N~{t@x7^iU-vYj&tjs_07^=_C4FBdlr?)ul?NXB)*G!$nn3nGD%PZAauUw;v zZo4av^ci^!46X3E07fPz!Px(LtiG5CU4H}BcBTW<4Zh1X;vYw`nji2Q8S=u>NYb#S zOoyVImL&MguvBf!j+F&>6L(UIDZ<14IVs_lN^%$_8prWg|$PI$U+GJn5eY*qB1 z=PqvR&0EFq3016=7>sHqf-}uEEq{qt_NVh6?3EmTOVp-SCkj0IIPX32bCOa1P%xt} zBXW^M^U^|}M*i|cl1!HCa4F6ax~Ewwl7sts&-6((kOaImq1>LcTEHbvq0}q)kfJX% zSZ~|*C#R3#fxhuN2qFujp0u+f@O~V~M}7aS`;5fZ%O>6|=(wSu*`&?)m=I9_g*NZy z$CVu3+}BKC$$ibpkYq9S3|Y&4)YGdQHcI) zFxRk)Qfgr*H4Ei%R!mdqOX~;~9T+WvKH#RY{aKk!-qG^yd8C&p5{$`;AO2qa9^BEq zbxAdE;Pbof?7PL$^5tFI+`JpHaAmSs~W%hP~ma# zh2^y&bmmhq^v!C?p@C{^qLdGZC&{J;M^9$KDb=~{GGBnn@`9zy$0DL8edl%YZ5Nh< z7rDZ#(t30GCOVp_zWlz2yp7AgCZYYNA!RUVP^(9^f@8xmhb?(W(m|Hotg+UN6ha_p zvVc6X%m@fOx4Ip2%$`a736RF--s}E9Wnm~OjQESaT}Ec+8?4jCSyy!Q%Ht@c7vpQ} zx>`Y2$n8~Ky<~rou1VT;emCZP@5jt|2-ncu=b;GKgVv%%1|M^M%Bbt!86=wJqK$t1 zwLaknuh$cpZ-YqMy>6Z;pBLUM-wrt~ zPEs%GoA>@TCvoNa#rdv(y!_coUwN+XFk?JRJWmuD)!P~-f_C{kMM1!%9<=G^eUCuK zyKpTTv{}sdc}(E^T@zuH|AzN=?EXrc-&_U-4OioY4-uG|!&N(<`r4}NqPM*f< zJ>qTs$*!!DUxQNOSEXmF>9+f`huZXYr6YmYKI7I!KnvwRyrLX0|6FcVDdJMjZd~S@ zHerU=KUAkGd3m;J$_psh6Jdo}-;Zp9u&p8hnCJ_rg7Y?LP+IcDeEEf($ojZhzCEzoXSy=mZV{Sc0lK%=az?3+;&Q6na{1$rsZWv|Yi(Dq1nAXVSA(0* zP0G69gfF-6iq}A~z29Avso80)FoBuI<#TJ`av2IU>Q=7*cS81k_dRuOk4;BzSe5{^zEQl->wy)kw9dioFQy`-XJn z8SwXX;_A1I<#ili;@{EpB!aMk=sPVH4u0=R6-Cp~smd8sfD@>Uvxsi0->AuLDr8pepxUgi*l4AYF40+^ZR6WX-?fO~K*>Tx1OxG^bfW32nS-@y28+srVo@9%yH>&FV z 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

W-s+eus?I1Bg zew{Sa1!_kp-bw$Dzc8y#=(a1aqrKdwJukGK>RS+U=(z@Qw~4Pww~%||kAHsVti&RD z!6xTHsb-tMp|}lNC9Uq(b?5)>L|S*47caR1)T^Ky?b6PH==2i-XujUG826wvoWfu& ze1WPabyKxvcNnOo9um!<6U2;@(+dnj(qUw|-exv1U_YX;L~kEY`2Y$NOVcKt zmw9#4GsmS+J_|vET`k%#Pio%&x-mWJ^&N-9j29iadhKfu#MUl<>3>4($2@-eG@H~; zpxKA7@lMsiyo>U#mfLmaIMF}l?$qYeF5VMVOxZiXEDPVzfIsKcMbHUmK~%A(8h?W6 z_WV=cg%VBHdx9xD37rhl!{3jNQ~F#J?3|ste@`A;GQQNTH62HY%Ew<* z8d4AeSSIt3IE*{HiA2qhY4d_q5BD3R_y5o404xSw+~_ww&@taygFQZ7pF#eD6rlYX2mZu1ynfcJvK(;o z6~q39h*uE(jnXzyq#}ZGBt%}<=BqLnBU!Z$9dtP2*6Jk6LSlrR5liSLGiO*NOqo9I)Uuj8EpJz$M$}Xx?46&gn6}#Kc!9hKC z(OVT*BpyV%DK)?ofTzM%?5@O?iiB=SyyWDPrqHUDSLdqFO`yaZAE zGn0?kS+OEs#lx*QnjhVL2)7%j7?n{$@^jf+H~B*%5toHJWA`v1L}>elL-N+FNzNy| z4OuqwIEcn?S#I4e4cfob=r=u#-kN=V9@rb-4Af%Xa%bgNIZurIX3A=Kik-5Bi#Wkb zqa6{fI(pV};kmxEjjS>I-jl>tX~!;<+81=>GU65)eo+5J+ff*`C-+jej#YV1=I3U3GYY_cP#pWZ#$$|qZ88|lj2 zA&06)FVz9?_T^FD+=hmR_D{c?YvBvH=R$D`jehe3^ZDBnMqiE_vumW& z)ws}0Ztb8IR#2mPs?1Vx?P^uHfFkTy(o}0GNTO?Z?VgR2 zT6Pt@H&gN+d47`4qX_Np_nEFOq)~bAS75C>pj`KIhP9@ zMVx}+N;lX9@J%Xl>4*6wkpsTtP0h|SwLM2d9o6tc7g^Or`7aywa7mZ{ z3@7j{j`~Nbb~31)_ww8eo{oLpCEEGT4_4|?2URrpf0)~J($ChG-~5z$zskx}eJNE( zLq$C#`WV(RF;NBbUzpCB>g~JIe^)J_fea$BI(*!6Ob$%hT`Sijvi*hy7yVg9yqor^ z@tU~H&Osg0EeJYb*WY_%Hs+yb-RPrbX!x9{lgavbe) z7GpU=!lAE;L?d<%s!fMWcuVEXR&qwh^-q3ofMKrZO;JKW91*vl|O47{N{fZ zZXJ(xnG?}z_c;qse!<$ZgilzoCf0=^)?Znu8gOc|>Obd(6!ZW)>Ace|LA=Lt(6h-- zcfVBVShw>g%hb^-p6c>e(=6glH8w!@4s{ ztim$iy^c%x@bI_AncM3tJ}oa}oRtJw1if88jYn$ut~r5rAB$$KSOYo_cb(o<>0jG% zPVN`x?USjAZ|*SF_mpRXK#J5xKVw~#CRw)R`fYL*;pNueg<#6=&2sCGfBbm=kv*NT zSTnHn9P;#OY}~2aTC_joo((FQefLW*yRoT^LhbS0&fEp{Q4W*p=t+&5Jg;S$f=Kim zJu1_EcB5PcyS_2Eq+w(`FXgboZD;cAEI+3ND|oph){Xc+PR_ADT@|hQ%ChWcUc~Qx z85pdgawk-L8*nqu<9T;9@egU@v%FLH??Z z4$3z6mf;K8AHvo_%U;-m=CAh1ewt@q%~G5#s%@qLCmx)-uP{LTAVuHz7eofw%9UMh;z>o4NkTO~zcQhPeXzb6;g_J5KjRSx6)v5nmjwX; zUS4Hg-M+6^%e1H?$935?aBV5`*)kF2ka91zaz>%UfU0`DX4mCDt9txGZ|<}Tdq=ak^yw@34W zU@`&&Pc?(?j?Y2I<>w+cZY3-0u6UZ^+in*bRpj-_ly&p(@Ucil2MJ{MjiDbVD{JBd zD$Q-QgrC>3KbQ6`xomX0I0fMldpAW{`LeOG)?xaqi5C(Ic20;b8+y9*Et8INbKr*K zdg+@DJ_Ah9a>R%7^mD8Y?BJKM=PF7(G53#(Fp=x31;MdQ6J96qa_IhVL9O#WEqCV* z2Aqvwpx#t5_*9Y;nf<)QoIhLa-R;$9|B+OEMIve9DT*nQq!QfIZnXSW#x^wPl=)nv zxz!zUcl6{Vt3fpslv1e7E>3-Yd))78%S!6(bPZTXV=heZUXI*j7wEGZ8pl*akaQ$z zd+lQuHjvnl0S5$po@4hEVb>rt8D(Z~w1%Zh`Tg@BDuAm(vto*7T)s3>RR9yb!ypqH9?GSw@3HCbQGl+b5O-QLl=27bz z61O2h6v@o8ODK`XNk`(Q|lb zM+vQUf!!}@me=w@%T%OOymdP!pwj%4I@!9V`LlsfwA^TQq4b)7DL9&RFzdIn;Y1T{ z4c+Q)k-<-GL1dWrl}fVC_?mSwg%q08X)VN5VVQKfG@HO0FCd$< z*UfvxPl&`(DF_TK^;}hJ&fQNZ`>KO7FY8i<%$73m(O=vK9YViM+@(+yaApQOY`|WU z6fV$!GY}M@UiHN1jt{^%67je^rF$Lu`>#87i0Q!(yY&JP(%74k3 zcPI`7i|+<|?WNa9*2G66orh}MF4osjAa7KD6bdY@H$SWyNN=2VQvz*g*+86>=N~Uz z9GahYTG3sl-ggwCdRXuZ{cp;bkjm0^`}z*^iuUyfgm;cryK{K`Tv%*VZW|B3!j1w3)B%12cG<&q1s&`1`&?5F-)m=V~ zuXP{&UF2Ne&}jatNfv5vjJn&f1{Exmtjz6`qy(8r@4VqqL~Uh)CfAqH7tfzm&Yl%h z*>*(qYjaBQaaxa;7U*0(J4gGvecz2lD`fHojD|484gSG@vd)!o0!ldr5L7vf%Wrs_ zl*gWz?R_?C*!BifE!7PXQRht(MBv1h3%~j$h08x{LHB!oyBQGvdpD$~w-`dZ4!D&| z4V#3$x*MlvVwCxD&YcNgNa9@BuMFKi*GW9*GgF^9ny<0a4jIjBD7Oi^c+A}R&FpVx zK9#vGB&QzldPV7TiG~HYvF1az9DUR9Sh)e#DDZON#1@Y;qvB`!&r{v>)+V z|4(PKwE(=o!I$5M0G0{@h?0`1wm_gy6CfwZmf!P*}00j*nDo!f? zi8VoyS7nJ)xv>LhL6yEeZxSxV3ge)Ezkn6R=c5C?Xf z4{Pcd)ePLfYpVJ9#Hf3gdz}$yOh<>lSA%HCu$-;A&%Ly6*1}-b;Eo79xELm}Jv)9t ztb{SE!rm}vUWOC+;KP_JBq2kHKz^m0`!LHgaWy8WeDeZlwl;x~!&M-5&d{&ws~6z6Z+@dV?_2dQ^WeM|aq z6Lh{~_k5GK+z`7!g>t_BaDUK3{QQaNw0J|c`$DOyEDqs4mDeu!kr&>_or)|{9k)wW zIR9oB9dq$op z@bMLI$7VtcUHoen-}ofK_>nz<(V%3mdDyLhT2dewWAd+@)!j=h5ME4q7j6i5Dx`Nr zOv<|3E*tuw3@e-79K;#UhlZiTnrAl`H}-ZIMAZaNs{XgFPg!37>3Z?D^Qmoq`n$~Y z=Oow+qT(7_<YV0S|%WvH(BQIPxf%e%+62eKxJo{^oNVF>v|r> z>QkPfAt|G;80TfqYoeU;(lPnJ3MWfZ;XhFcyiyc+_U%)e>ysKf0v^DEm7sdIpw}d^ zEZ(A_`l?x)4~K)s|A}=O3DL^#>&ywetr4tUfp6W{juqTDZmgH^8HC&OI=DRtj3lzs zo_u@v>!x`dDHenXIaa7&^ljbt~ zYIdt6Vy~6G8sO22fASx|Iekx1x>q+P8=S*?kfJPn0rl)qB8=}SJ z!_9s#&)s#}n3b;-k#lVHLFhw!yMHRLgz;{LyySIcUbOjmvWxfhUNrEZ^V_*g9xzYw zIqIB$+>hQPz#fwmm}f8ic4RoPnPXRrH>yZ{DnygVc$PWUr)5;RmB}AcM5Bh&X$l>K zy))N4I{O_Y*rN?Og~7^UoXn?O!wi0k1=m7tb~r>%FdpxQTK)xXs!S?QCt*XME@+Gi zdYQT<5lv#oe--C_SJF%U!H>P2r7YoZ7du7K-wPBm?vRsM@Hbg#RHp12d*Y3q{Qho4 z_-7Lk4~9d4r+xCI!Jtl)-HyKac>d9&$HUGaZ@1Di+(fXp$y{MTB`H3GyA_Vz+9=n$ z>v<<+u=oEUhiU&`*ju|bVnyQUBYhY)ToZn9SjQOaQT71#Wf2ybG$Z;h~ zytV~Z709HSn|@g!Z&WHPD+|DVa9S}w4Em2uEam7`ou+f9<M(da+#N@TSzUcC6^{Dxkj55O8cSo@71!0C{NG5+x2UvCv#;tNG~ zN42F%=R&}%RMXarA>R^3obKhsRpQSU`WsK~ex8>&_9+z&v2yM+X(aGc#gQ;W`0Dit zo$;sZRh^xw!8tWG%3pOh|8(5)l+DzKj?yog%J z>RwoASF>&DH!H%Wd#ZV(meW9#aMD%t*3V{6>i>F|Z`6z)`#BDvBs)x6T3UHP=^H4( zm|ZOM*V$uve8!4J=+2~b#pvx`jgOvrX#ZIdIudv&}j@vr>{?cK`X3GdZ^ zf9=eOp&jejISzF&Z{m2!4G}3FbZvfq{^Yc@+~*(H4L`ouxB53c-Q#2*PWNx4S3@w4 zwQ}Gv*E`)foA+hMd$n8r;As~^kV&1X@Jp9AMeeWd8D|SOVe6g^wr2&J-G7IH#tFCK zQM)@;4PgY&rg2XU*Y!*De-F$fR@ecwVufW^x)p2cR`M_3lmwTZccs2!U_g?P;(#rx z(RbFP$-J$kj3xKY82k5z|3DG5Xn0{2#6&&V0^?H)0e?t!&K@2i>oWccfIqxSN}7Y) zdXZCg5RGdKJY4Oty1zlvbzx@#3~;sSq9%{#Yb>=xh|4!s0qQcyXMdyc6MGr$8DqUJ zxi2x{Ky6G?)7PT1|Hw&#hOQ%DD9ZT9p@}fSY-7y!Nh@Aoa%k~z&l+#Kn}}y#NN^kC z3|()K#mfi!7g1nBOg+dT+On)LG`@y{F`T9UqYwWbZGg4Cb^CT8x+aq3H!vU-bB%;V zPJZ64`WFRH;FByb)o*g97snLpkOG{2K)shv35$xx^6_X3|2FjX@JRpu{qX?LCWN%D zclHg=CH==Gn}7XW>yW33#=L0g>{H%-ur*t1!7r%jI5&Q9pN~BC{gK$|)2G9&e{M&c zMTfNVvnYuFP0$&X(s7F9H!q<=f{6%|0uL!|e+a+4w{q=9k=p22)AwcEe>YzR7~FJa zW-D9nUQF1Llf{Yc7AYd%&)9Sm%JhJ6e0=-O|%c0@Z5QuAnb9$PxGk(`?Pu;S5l9fX%k0$x~;(s}3BaQ)vbnZ{?J z^*T+W+6^T*rsnqaY3zwpg8z_p?)}G~?hoor_2FKoGSFJ(4Q#rj?$6~mw#X{M^?yx& z`VDA#kR(!JDGQ>f6%tM%cUOZ#A&mEzI*z~O2Sy;I|HZT!C$VJOhb7xL9sIcX`+m~w zgx4#6Gu@24pLz;I|4I#&dcFTr71v6YzRLN2Gr|S^){re1JX%c7w*EzblF;Mi!Ui7l zfH0ssVCf%Jj=bAsrGw9GV+CHTM$wX_Qfl(A0h_Fg8?V2-b2GS#i;* zfTgSM2J>+)(%Kd%+)e^UhqT_z3BOR}V|grQx^csmE~AZuypi^8YE5Nu^X(!M7L27^ zQk0LIE#G_^hhtv+3v2pG=4ZTW`J+YNd;{lO4>*i@S?#>Fh<*H}6JE!+>~79Br7Z@} z8~&ZMLzeyed}Hvr<2K*Zr?EGa1Y?NjQR;Uj8gH^NV}AbRJT9l;(S>t$p8Q)Oc|yKQ zZXo%8;~X&gO0B^0?RWKX>&B|X%J%!{sb=GL4o?V;Y^ zTfpy)b1_{uGFX<=M-7f;hv=j{if(l;;Z!R7Pizl8Uru}Q+C$Bw+JF3QJH0io5=Bv>seXcFKvDS;%7WNpDLF94TB?<+aY&I5?_@A zNP47zy-^KiwJ8jjZ07b%u@C&Cd0Aq@8!o!2PO6HZ@R@B*D~xNr%u{Vye#W zb-Oy~o1a%k?$_)6;Q7}&uzuwhf{5{tZ(dClIthNQb;5aqOIAco3_S}hE}^}<4o~H- za62}VU$E(!&|7)R1L+nrO!c<%Gh5l)8A>HZ*AyN9g=SruW{1WalfKvjhi!$RXWqKl zw(s;(|7&pQ$$BbVsB!}~d*VeqsXotSOl`|nVfT8Hc9nU_zdk+v=zJFD2c5~IX|_Jk zBs(ObGVZT! z&U+Vj`Im5Kf*$H4gM5Z*LL-fn#b1i2LwW7N< z(~@c{O24|Y_e4&vu`dD&y3`p5x1Mc&}vesO4UFSNp_daLu{n?)ly!;zRMeqVFF5p9h1 z>yx0<8--c0T4?YnF=@`^a2=av>#6$gg#7v(CuDsf&284UV5NDpNB)dNa>1V|LPmqC zmq@Mu8R64V%9~ToAR)L-PpekG&8(DN4Zpi>%_62e5V7m9)RGfUN>}y1gK+1VM`hJ_ zmbs_ulKn0=Sl{dFlpl}o*6{2xk<>q^A3MQMdd$7q&N=xR}&0wujJTdLv zqg2*(q%4-DcF+27@n4O2ba2S6#rjCqfAXBTrK$Tfuw&8@?Rq?+TWvojkSbu@KMS?r z-YrhUPw1v{DZsy~9J%yy719Y64)JYhZ2@xBdoOr(ZcF~^`3E#}zE%L*owwiLkLKm-)s z-}++g`+};Ji&?BrF#`zBrNW9QVfncDNbib1Q+hUV1C9_I0v>kK%ryE@N-nt5U=2P# z{v|e^sadMipY2wFxPNp8L5K@t?|5{e_0Lv>v$MZ-vcheOHSpgLhzokbtJ-~A1Y5XA z%Wv>J)l!@1&>#xUx|yVJ7fIjrrgBmPi&-@!BPXwX-;wcJU7~)stg%r0-p;Y~t292I zG*WXA2LXV|*e>kWDuB*B$iUK}B$oPYRP`sbO!enG#U$)5Co|U7P7Bf{Pz!w}Q&U+a zWc@x~?wwm{>`-#8$N)ZT`;v1|?Wr_-BBn?KCW9K3mM}(q+pBqWvzpaUkOcSeEcBJC zX9ReirLeA6wws={pHfo-i&gxk$|lU3v4s5WVP`xjg9s^_Ib^li-U3nWS`RQ9Ke-KX zdH7&D(x(p}tIfTBEoKWfpa-iY-BnpV`ej&4HVV_h;7dDFLGs3T-^Hr9!d$x)B*BdqVi}gWlGN`|9(Zdw=)ed15knXqAHabe{0M1+CC5@Kp4j*|_Qv}1 z{Fsb_!d%Va$Neq_3s2@&`^EXCXROa8Zi7iZ8t5&|CpRj*MZcz@iuPsE;m%GDv%)Dm z`Z+^KUV>!Eb6q0(o2P)eN4EU|AFV%BwRd)MWKdcjNJa!^=v;b9fGWJ|o<@&ik~^t` z#l(FQr6Pif2+xbAl8(fy-%8y^czovoR2`LIFt}z`fAPe{HO$XmxOQwsE^K6sR%$Z3 z_PM6Y(Ni^W%4$dmu7tyMuyDj~d222fB;D7ig01gjFb2NP>|T#XB&v6TZ%!6cv}D~t z|ET8AO_cM27^T3|4nU>HoM%L2q)+D~nB_u)hc*uh`>8+MLuG2b{T5o+lCJ2MRH{{= zs+@Nc$NS@*33~AF^PS?~yJd~G5{$3avxkccax|On?H=`$-N@C4y)gBIDT9uyOQQPK zXr$Y&W8G)VTrG8}aAlcQ>haxU{|apv0bKt;7K?M?rgNa|zl|WA`X^EE8-k!`rPFU= z8!1m?Ke6X7vsdPh7ZiSvh>Gexbl$5`GO#xZ`)KMo%1}0R^ocON^JAHDX=Sl4!6||m+ z`2j5Kc}Q~U&%SJ?}bQ+Q)8kT{yv0 zBKQ6=jJ~Y-#Guw`p}(%xP0Fy=3BFeiv)HccN{o!ax2)s46$SH1AOu1wV-d^l=;ze5 zZsk;esI>nZ!^)F#cQf0Awjjro5&zO7`_<7yvK;s&FnIJITBkKHa`A!^XSfYN$Be>) zy(@zNiI4ltynVHjbL=q>HTCDnqVFPl%;6&OFk~nf!K)2XPA)FnI#D|)SG<6 zN98y&W5Qw5OE>2(=8S#t_V;t`4Sd_o&-vU#RxzH9ap;AVIz9nmpuOwlI#>3l`sP3L zwM*|T5gca4HjFoDK*XSwQ-8y@PHX3wF~#nV^$Y0UwsiXEX^5WtT* zTd0B0X<8NeYIeJHOf{`Ne|MM&vkV;}nHOF*52VaK}gd>m(5jT{nYgV8Od408%ak!eR3|k*)y@w%7jmJ zlHdl9lXSx%>Z;u~1r!gm{r~o52i{b@h8Z+RLpf_-l<=1`(Tif*XI4=$uO1vS#&PMr zg6h2Q>R;C`e&_UiU1z^xSD>u%#CLz8DLna&T!!edOz6tzc8sy|&GU{F(zko2zPYhH zI&JHY^K$DTm#pyEmFVv2VPIf*-w{DxqF)^m(k@p5XsT#}N6#%$K<2DB^;Xv{6r)&>V7A(!&gYLP2YnTzQi8yzlAq*Z zW^rB8hOC>zDKNCF$trRB`3Zx^NPpzfj$zJD z>jF}?(ZM8&{flGacae1Ah{vz!goRFtvF9d_DdU03Qp4$LYf3MN`QA(qd%bE;@mFaA zc_ziHD614L5K947-06p>2TI!s#`-)( z<_fXic+gXH#mwuzs#W2_i6r2?0f*szzHPa^PZOQe@|DTWJ$`SB7dQt*6t~L8Em3bB zz8aK%Ve!kZ*4sUb7pra#yOr-Z#IEit0Dn2EH)L(KbGEEe$LoFD^xxwiy{26;wdRL; z;MKmsskhLh4j-Y&~V0yGPQ{(j2iE! zty1Gc{J3FE+S{L=pPysHSs_=m4uZKivuOJ3;M3hhxrRs-_S~QEO65ma zIWOV<6|=l(1KQ47U7jCfsR16pO06*SCI&w#5AF01+vfQrF%X8{qj3rR~$ zI|lr;?SH3h{36woa_2I~Ej#GytiGwim^kF{&{dJR=jokgj3rwq>4$FmDn7H4Z#jfX zYb(w0Pf<}a^Bzi_d+r%%*4WY{%UqpewM?rpDnE>czD$&30YkT^(yz|+G7S$6qTA3T zQLkBkI5=hw2bSd3vWu}C%*m!Rb7gC?p^7B$l-&-CwG5fwNk48;|q?6G#j5 zh%jRFPrHvWP?OocH!YOpX&Z)Y_uHsVh1pZG69EIUe(r+o!Dh76``yXXy@lq;=0>#3 z@IqFR4zD)vMExG@Gcb};P7ti4Ejy{LrM5Yxjb&xan|7A^F=4mztj(GsPAODuppb*) z$Bh%mz-L(dfeKl25+=zzXPMfG;Pa9!5VcHeJVkX2OM5G{fJJDvcG zm-(+1Y_slHLo6d?f)qAna~)=n9uypI?ugt5*58fI0)i_g;$0wh^3lIq zWf7G2!fs8M!qA14=3x-B6Zeba4g)hYhuWA7aZ~UeQd%MDhQwrb=}-6=iVEvtR;*1v zJ*7`hCeIuqM0n1+1>B-6DH4PuI8?TFcfl?$iIxMYqB1grTU*4Tq4@ufr|9YF*;IJe z+2krw0b7j36UeF3+M$S@t&S7H5<@v%+8 zXJmGhdC_myR*6_b{w&*Fy8ow-xJdAKMVyfoEjAkh#~Vk$Xj!A9n&~Uh)*3a})n{XE zELYXc4{CCkUSulaHnH1u*u>;o=G?=Jufy+V{77lBn3`azAAuH5G!w!1Vd6N|Sd5?o zwGJMx-$q|*FW3S6pc0%(dNM*VYDcp>)iH59l#;sm+T3r^x{Tx-@sl|b0wecyO7^t} zovXgT(CP>s`V;{eoYR(p7y}zsnd$s83rS(;1IuFaN--aqV|A)s6?0xiL@G zV?xwCtcnCdf& z;AC%GHj{t$?AcGp;VR1k8wtaowvELa&noBL+b?S5^y=Rc&RtG+ZI!zyF9 zn9EBorG%Ppvyv|T817#V>ntyPl{3juPBne|sFZcY@*%y@@)7Q*Xb2yQLuGNl7mt;a z6^~Vj0MBZ&t~_7a-oW9_tzzG3cY=kT)Y@BJVVuvxLV1_(bFwNC)c9@hO={#z#KWlO zNV}ToVugWh10qSPnkFi>YQTX4b3*Xt3QBbHCe<`_po3N)G91m2n^!rJ_HU1e; z*F$msVj%B{%NI-iK>2mdprzKcwihqp(_WwssZi*fW&RzQpeE1s6(ZOZYy3%iO6U$+69uZXTmGuGLSwZJ^LsWb834W{OEf zL9)Wc7Zr|v%%Me}99!VdJM4P@XBo%f{=V{&zMV?pxUu4u#_!2#p9v9Wb;7nQXWqIe zeJiLQoVP-qWwYuCNW(#!=9mt@CgCugNpBqgR8u6et5Cr*ib{xG=W!OIiv}yd8E?#q zHgY9i%y9N0-QtfVG-{u+eVJ0;NZvo=Cw#(_D!3M&ZC=>6EGz7vI3jwWVa@hExPjv) z@W-70{SvpCeYxx;!Y!Y#UZX~N75JS!$RQ9a&noab-5|?n@z9;;no-e}y4YK6sN!r( zmz^M1{4}xk;eJPh!sCYzdqTc{*SSb5J?&}6p1#zfr3U=%QpG_SE1@y=w`h`eD!dk8 z(H?%-^k|-5-Qk9ee92A4V8&xvmcNb#_^(~>N$2*8rVByw2M@;@xMw-K3Qt~a`YJ{_ zQssNyL@ua56FJ$jdG0(pojygm*aW?9B*Y^Cl!xD+Qc(O_^2EP&+wNv<$yNCtJ`_=V z;}^Pr65dX4N-LUKbV*&{SxdJ<9M&W|>gV`F^sx@E+!p-2&h$BJ)QnTH&99;xrI}Cf z-gu52rza&SiR+mrYP@ zJ?6QUAhGZ2@d$)G&sD5)Ty6}U&q2ONTY_XmZlafy@EE9{Suj6WkCJ^Y+z@~Idoy)I zH_KhdP%Q20^R=f>q?_EA1QL2eG_;2U=xgK~f+X2H^>WDX=Y&bOn~sS;+1c|l`Kzt< zpTx;t<$sXLDk8tGyH4{M~!TzGVAiF)h8G?9BMNR!#$}S(@ z2Ko0HOnae#@7UZa|v|)Y>)~seP0XG?06GXqm~j8g~~S+ z+p{1M2^rF>;OKZooZ8|J*-X3JAEzikQ_e!m2e{1~5qaS?c07I9_-3%iq3bwt^&1sg zcOm)ro8R!iKgtq$7tJ6tG8uSWkUjA=Rh_=`{AL4KBcm!=P^Ivq3ZoN zXPEn})5}jq>z>935!naPgQdpvD99B!uoFLOlbLRL{=#Ga%<25YKZ;I^SkT&Su6FH! zMFj(YX(C8j3YW)mz(aV({M3AOI;}T|$B`nPj5v(!%L8H4;bs&pLd2pTtRm9CZbPFp z_#k0sigSuC*YiU?5+H@KcSZ8niUzKoRteM1gg?cXkpK1Z5biuZ)F(M@_gsCu3AlL? zFx*?}L@sF^Xfk;ds_!)_03&}RfaEw{0;4_vmohwdvl4ckNrmEx?wPgvU7jZoA3m&p zq5R-hxOC7s`(wjgS^i)8;NYJahSaM-R!KB!C-|-3%@VIwuryraveUN#3J?j8+?ur6 zZmi~iBSUZ_2*Y38F!xNFYMM>6jFt(Whk-UWG-$fo=sH090cNzFkpc=v`)yTZ&G zqHWwGj>I_y8U)_lLQ1e^fk-;}HhI|-Jqx(~((Mqz#4q6-#L}((c`tK=#rxle#R+rI zO*;I49iPvX|9>u!x=pD6ujBt+a6A65)?xV(?a_^Z|8L{-qqM;jvO#CugRo^`VB5!x z6>gk-;=6N;jRQ-A?edSk+00@?%-9Kp2+6`OJhMMOPPAmTSMv|ternYmqYu*So{>qE zyeqos$1zB4HI3gqx85$GY%M;G2|8=1+Xxx-zPd;h?7%ms6ct3{XM{6YZ9I{lH@h5V z>@dq8u8usscywy=^fx)+^xUrkBJUw3P8cTRuXfp;m8)!ZilN#2GUb&!hC!Cz3jQZ5 z-vlVAHS40KsQf^*DQ0C0QtVy$8ggCV+jW$gd;I+*5_@6qD5`A^K-3r^Wm@)I4HER9 zo!iI^g@QK5pb2nWsT&E7_sVH`^}^_F55g@0uT7alLIWONu8?YpE!frMNvXF}qs}Ei zgE{+V)x5x&FtduqjaVKKd_#Q6p=@Mng~`UK_e@UKu6YI0awf8+IsfCBmG!t$?^hM) ziv9Bse=SGV^SYcDaGP`_vdD*TKLZ#JqdlFhuNvpra%i?$A`K{&{V6oCFR=E0E2K_- z+1obswR4by7t?dZEf($vSJ;T?DbNYU9jno`*@cq+UU$E}-?H!IEn! z@8t(!{5hlKtex|8f+Uf?F&%|U_6^Q*>C|}%6(1aj{blYyG8o=?G9wxmQ-GLN1_mi# zm~`34SKO=Y?U-JKI z-v=UKR`p$_o5k6Bi@~>Q5RismQco#cpC9N;-dHo`o_4&F!|6tJcFP53-BCOJ9@D;N&A_0z-lFr~tdOll5p-)|$hxQ|| zud5X2zsiboJcE#K#xwi;Evl1icfxV)9atYq=#5=ICMUZ5d$Mzt2|3V|}ZgL)ySHqzrKnFISsKaj^+ zyiTz3n)hE!T}9yqlj7!5pSM5LS4>98QJ|Lg-TbYn^3Yp^a7R*tmFd4F;$|+`s>-I& zI{GNQD-`9Z(NN@w0%lqvKOKJ`Yi{?7i;#zZV*k%hP(Xy!k9b-#Jb{jW5b2Ib3!!(1 z&qzPLZe?{yU#u*ki8x{Z@s^H>V=re8J^Ct%GzMNxF(X_O% zz->j%wRR7foV=?4ncCXD9PmULAJjA&4S1R9ZuhGd;}uW@qS*YnIey?#htW@?v^_=Z z)kA{+t&Hq*KJ3vOJ!B20^8H0{|LIDkVG7QTcKvv$KwOE65YG9$RN#s8DZz{Uk0|2Q zuZ1~bSmuxZTaw>}wG)`TV-un_=RWct%{Mkn%D>$UGDazz5?r9uGGywSh`L7Ak>BHu zQF9KupVAk7qO#etzgc+aM#DZ#H^euSE=(!-R^@QkON3|+1guFC0@pl(sx38;AAwaUL{F<-ccDmYdzE0hgzm^)e6OR?v z!_PqL?H8=c zZfFHw+!&c`HGty>yOc_LgM39qs}ds=$UA@ zSt;U7C5d2@*>V|*a*&=V@(qGCHVi}QedjjVk>hw~sQ8mZE)*-o(n4z%z#eA%Kc0~D z#GBkAKTz-HhdXL>pJgQ6G&-XhMi`ddnG;ngY)>~o`2KtT%KKCk=ag8=y7%uiPV>&M z;@(-cUlWXi{_Uhi9JKc2ss8C^G=kqBRL+=f-MywIpF4fQeb<_aMhN!Tsm^tB1>cMh za=unzD=u2=G^1LalKKdBPE|3v+T3F>fZw<;(Lq=Fk`9(0lD$hOfpm7YzPn3HdS-Ub zCObkyLD?m}XHsU~F?0f$KJ$K<@5jR0aXj5@n(5DIwfJTOZtx&OFd8QKBE=NRuBJ7M zkicfmxSP2OJ7SSHf!&9Z<$j%g-|<_Zsj7L3r8>O1FEY-;ZGUVGtD&bPge>o@B|><~ ze3*Ww)T*E$RXHNhpg8|yS4<0z9G!{Mzq91j< zvf}%*+}BjF;52F{KJYZMfrxddN#|AsK6i*i$wfRA#(kS1RD7KI!2>7VgHsCH)ZS$p zIH$z=;5J-DTSx$gjlk|*51{o5P0zy8O*kf-D)8k&FnHeGw?q)Rl}>lf$B0_=tDpto zA#}0?gSOpj3>dnB4pVe6di;>1wYEzyp_(wN0YADQ_8BlX!{)jWKKxCq)4P~5>ICFw zBQr=${j$TX|1(~`iNZghvqKoF5kG0fsenV}0&F{xdKr;W3-#&l(eMmF0^oYFCc@2yh2$<~62G86wUHNDYf8S#LP2)m7 zw!A~4NT*o4c*1*;mlAL80?3*!$zm3LqU5TcPw!6LM)W4rs>Z<4V2D%S$As?-B0)7T z3Gzj9{WtNAx8TDjjUVKxn{HP(S7X3$P)a+Y8g5rg8`^ICbmfn~ZP5KG3osLh33#tY zUU6|>)mFmbr+%iOVQNs6jw#1A-hXU62=%}#RGhnWUc+JyeJHMkPXDh1NNZB~cs z#4WrE*h{l@bcQ@5mPO-M&+1k*LTrs-F!4n7puYNxO26uF6Ha3`VlT(;BSjSt&W;o8 zW&p+Y6!+5J+{E{(KNEdtb}uNV)Okx*x)7g{X8=`dE{}KfQU8?Q%3JleYgwlIpVhaG zf#c8cIJ`FH5b+3gX2JbIUR?oj-=A>h%WAw48>ou_4ujc1cd4*(moFuX>0Ow8%-cU7 zBOT{FM^v@4-D>6#7coqQI>kQIlb2GRM-%a3w|JW6iwE#i%=p_cpfQKps>douM3egdy3qUfVC4}pwSi=k z$yvSNrjkSloHroHO*&^(1-BgF1%Cv8HQ&&)j2t#sf66h@)|>;*^%~+G!eoed-&8?$ z1e5c8uPoZZ9CL#BH?f;tx$=6l+_a-jA#eJdjhMl>z3|qYQxA#~hUXcxha4b|P@X3xV+MQvCJ0?)YL=O5Zj_Go#h&oO8(z+ca%C zF7@AjWs0{P3bk^BTb}r!gs)4_Cr1I zC#$ zp0Bl&>i28RIdKo>mWHFzXssgcqxRp5Z-|n%Lt#TKT14O&u6{?KvXYd@oH4P1;dgrH zcH+M#?xv{92u|3B4h~t~)L>>%MxQ7{4cH?>@NF51*mxA%lj8`7bYgA_V-1)s>h0O! z7dabyl|EkKd|(HYzPfrPC%DwbYhoo_XUeYwf1=OJWq)#ckQm)cpYN zXT%m6f<$Y!(AmmIfti|)oa1(f)7d!svuG+zBVch{_sR?>Wx$E|MuUHXhebrVMx}Hgl(pi~vs zzK>)28?I$ELi<Z;g}Ic7*_sON*17O9+{5>gG69Gy~Iz@n8?y=e&ccclc9_^vb(D zi4&KcD`8dkXp^h01e4(hy8a1{b6!c7+-q}m`0~GZ3XB4V#OM8x&1C#~ZRHm`0}QMT z+`NoT_Aw>ifuL5s*!JJ`-$7Z;TQy8+!cJUE#|g^R6tJV@XSF3C=Sdj7lyZc+J>Sj7 zLDr3uAlf^7g0d57&w6?9^azzf%PzXZ&FYaY#$rt5MoASGeGh+45ZJ>>c!jXc)XSOn zWse6motlqzU_iM0`T35|V;kuZ@RMho3n^tW2*!RP_sRff*^oXT&Q$TN7&i z>-uQs=*LtI(rxJ?7F3F6Rn)w(mKg;GrEhO)(1BA7#+tV=lidBMzs=_sk~k=k#TFR_ zGY)_IK?TUAR)}x2da2I9aAfe%Fo}mWd^nG0IDgYqIDx%$^i-3w)U$uh;s^6`sZOzG z@kEh!Qts1%@fnjFPrX;oF-VVz$h1s{rirq~AV;+g4u{tml==sc*a>q8yVYIdRwgJ2 zzoDAr$f0t{^kIzSj8no7R?WlaGBu0VQfj41ZUa{E6U1X;V0==vS29l^6VQ#S26|6EuT`4rm}nry74E(kBa z;k8vBl|x-?Dt60TLmcO7;R8FxIjWJIt~G}I2Ml^dKH^D!$c2yW_%!&xRFXyofEpIuc|+r$39leIa28yJI?!T=3J~BF8w(_E++Y=B_*j-4fUCrR>%2G zjO0{dHLG>?1L-29NHnz*`{-D*uAd4j7ImqlJshzsT`7=n&IDYZaycwj#<;-(I0SwA z1YKus3#zVuisQ)ayC%Q~MTdWC7P*YRpc57o8?O%P3a;hGpK8W@=id|@h>mV~@nBIM z=;M0_Y*@Cd2d&q_9EyRm&0%Bmn_|?tsHy#lRf_62?W_kki;rNtiQZ6htW9IMwv9eP5uh@_Ba)@dY z6`w4A$B=jzo7Ezw>ZrVL_?1`|KjRRsf*j7co8NBU~iPepNVOH@^Gg zVCr9uWc1vQ_YI`}g9U{LpJVAa1GPf!)#b0r$!eKHw8_cIKlPPNOI4vsotn>DFqebO zASFqkaCp;mjYVD2SE8Iuu8C7rcmV;I|Bm0GTJ%nqkrD-|YBxG~1st!e5spyp`pl7P zC*C*5s>T>+#fn$BKD921;sWZYX`E4$zAcqqf~io^kM+eUbuUud6NcmeqVCl8(%;5(68K&1>5yG_zfwPhLqd>~&@b4dZYvytxCT{#es5<0kewGubnifn7&6j)e zlM8j)BTpfOrINlFXuTkvt94uty4rNxoAv=*pEv`o&Z-`3gn?9iJ&87%V2k-4kad`@6cN~<#XCk=IF-+J-^+jm8?d`iF3lmA7Uq1>TfMOis6Qf?M3lKzDe9fC(i`O zRdYWCgkYofjhA3x>0~goa`XCcAsMY$dY7Z~_7Xw2!D)QDeXyU05Z} zFnjvNd~lC-uGOP>Z$Rv~*4BiU7GIw$o-mEvYF^!eFDJz7ZN6^S47Wg;xH7jZOP10P z3SebZ(>)wfxkWQ`^Q2FoNEC_3dES+XqQ1gk37@*Mg~jSr4_W+p3Q{YYup^^5=^4F1 zg?z(5{J8NNkzjtddg*5S-yO@V=%4PTJkQf?{~e~9_h)U5V@N49(Pt}R>S;xe#H+n$ z^c&9~m%-ScuLbzQ5j8o&!ZTlI?29l<%-5|GjOUnBoqnY`g8*f;0TX^kZb=EnyzKW6Ql@7rsa{QREM0 zv^)e%!+(NU7_M560jE!Hep|a(btq1GTU1|vmA|Lk-IniH`&V*g_)mBThd_i%PPkay zdwcx7r69J5%u@KQ3_?e*$Apa)E{GVJoG|cXiEz&EkEOvI{WlPWI~kt zwXpG_19un z9(I4iGEDS*vRk!nQo6gF(*`;h_KAe>{M=Q5!pa&kFTDGI7 z-G*~)kixN@i@q>YCq6BUKWRpM{{6e;W)pMMpSen3sr-KV)@VsBDeT)f+}KJnMsiM0 zPN9IcsVRQgZz(T5R^<)!nTK->nqkN)x!WR!Z(s|FEWmnq?iH&B=76uQ9YUwi43t*ad};tO?XN)XoO-}^c{ z325LnVJ!*^I)3rTaifQ44w^WpPl(swu=dLinmM(JLh)SAjJJyD=ZpMjeM~94X}rI3 z`!1mZsft%=+0DT3y*YH8hmI28r&^8gzMDmGcDMER3O#y6iW6Gh{B{GUrb>b#gv=(7 z;rilR!cmvtl_1D-o`?`jG1WyUpFr2VW&SJ0PAV`>SopNwzQ;tMaZjY>xYW7Qc?tK| z#NAhE{OgAaL43mj`_(svzd=3oa%GS1J~{6T``W@ zb{;zACS3sI4IH0pYj?}qB?h3*0x z{+XT)TFVWT<@gD83(3g*0Ru4f0!;^stmt%9;pDwoWbCt4OZ~uab=5Q$4A{1?*j~B$ z3r?aV@|{der<{Qs)__-~nb=5RO4y5-NS}E)YX`Q+qXir7l;Lq(X>CSUHw7cSIKu1h`@%{#r%3PM`F2SC49 z{dAKobFtkf1p^Ob{I_FF&8GHc70ldMh|9gZ%CWsXLciU)q$dix%tfQ(oc)utoEpME zb#vg92Oy9lKww*MFP_|K0@8B5e|^5?P+;+5L$ca&&NFa@PGCbf=y;t^Pe{9Lz;Vv= zV4^%Qr9AN19$OoYLq;jMENOaXSYpted5tW;&g3_&J7Q|RVrp6rAGS363K?kidyEV~ z`eBRm$^(y)2eH;NDXMw2%H^V|0e)yyV=J1-VYVrQRKOdr+;72NugQjevfZ@lILFAf z>p{&N_w@n#IzcdSd%*v4*VGzaCM$#3ezOiO4%Rla5u?YBpQmb>Dl0~obysLFsf|Xy)@@^GRdG{wn z{y1Mw^6EFZe`!6NYlUOGYJkCTBhQ&g;QE>ekPcnY+3r}k)Hl^~;?Z&)3UHe%t)mHE z=S}?pld?v|TFc}pXZuM^803*@Mw*=t>0VLPInVb>ZDtSf2EirIR_R*X)|bu%Fem%% z^aDFIx>z!}-HZ9*OFpOp^g`~>drEP(qY7%}hJ@>l)4}9C?l>hHBz}tOW1ai2l5(TB z7kYka5&1mX&)WVE8VJRj+q{W4K(&UF#k-EIDOS*cnDYQP>(IVmLw9-L=*%zN^U z(i>?jK39|JSW^A(7(KdVnpX&W$7j#%BYI^$u>RKX)|9+tal+u6AM*&2k&UA$PT^t( zYQNac96k!?ll8&!PLv%$T`jl>9aV5uIAyM{DF7co0BAk1>7gqUANuG&o`Vl!3&`IA zqL*hAc6aE_9s;z-#o*#$s^Z+`X`P#}lfS}`FGy`cc{6sS{j=J?9^8~wByHo@d)j^9 zI=@9Ty!{Z=GpR-NlAN8^Xm7U|9Q)qf{G(N4xUfc%whE8Lu<2kNZWL?TeH^*WBaz?; z^><0?yEULe&~bYxiSd|%vtE4^vpjvy8F4!}wq43l=gCl2u!r$G7m_e47uaV@hDGaB zs-yn;ZzU01+`mqxn%h+o{<-k>?yk$drzrl|t=$~>>@6z?<66y!wGODkSkh2q(ENK*k zKUarbcD~+_qwI`|GRZOOFf6tryrY8+ZpR@S`N5WpYRa8mV9g6b!lRS_@j-{QcK zqbMrdEifn>_2sw9Rv3r>QaRmP>Tl6u8Ps5Nfq%>>(E076Mv;xl`QNk(S2!V-s{5x3 zO_6p|)8Bt(#w*Om9upG|^A+JCAy57Hru}zI>y8?iE_2`bufm&-QUvoB-*)(gSLb%2 z1=k~(obo4Bu^hAg4|I+Csw`g}=-tocP|-VT6EP_LlGb`;>F(iCVKe%$tYOD4o7C8W zS0((+Yo3BtDS@e@ZCPWFYQE=qYN=vObwSAq=t&hrv?cpC7DfzXguj2%?T%gb8t2|Di{^Wm@u zef@b8+%LoZ@_`~Tdl=Q~VP(Nb(EEkHA1U#0w+Hy+G}4XIxHe*pPgw4LL)Z>pSsclg zh;rvuNO&$XIxUCzDig7=Kx^ScN!ibqU-_5mKfXAHB!!u=hfygKv$Iw=KgNsX&|C0i zDivp85yv^lU~pgF3m3L(oqJSK8%1OTH6Wv;OwQ+q#!DM7c*Kt8HB*{HuN|(w%{Dlv zY_eg--0t7bOW|`|=^Prj-z^F#$PLA4gl)(|4e-Xd0S97?YE3Bmu}_nn&cRfx?;wMl zx?*6Q1DLSP{MS(j^Lx*%t#f>v|9Zv-3YMxV!Aeiu_irW_Mn&ng2J~IqWZxV`PEOQm zj@lRxfU^j-Ug@TB7KIUp!_K$@t`4<1N!k+dk%|w?s#y^aYS%S#WqgR4Ri&b}ACjA- zEWXZLd&tC95#6*wJ+xTJ!-?C|GqO&> z&2BsTB=qR+`U7b{+URK{@Fy;>_8{c728SMKP%!9FMee5EE`KRQe-cQ%G!lZ47co)_M&G%rtl~S?74rS~o4<8MpUvYDDM=G;B>7 z1uQ`QPk#W`<*yK?8PXn_dSV(pSBj%X{-fm?UTf6Z*Bg@GYBv0quG#IUN;YffkZW+r zDMztxIhGKf2DJeLBX&HDyjQ~a<|l@cR|m?AC-GiKCDu8LNnu@|kpb6{{+l03^`UkT z+~R>rZo{%^45Inf-iPJ_r~7y2-H?iMr(QwBb*-0b>e7ccNzWt8VEyZxz+KY4gL${% zKj{i3Qing`gi{cllE1LbZ{^+^jf+npo`>Unz#<={kSk=>&3#0b=d4W!CSy|uT z!w6R4#u3&rx_p-qchF(qi`Y?(dXducO>u|{_CvDFBW?N>k zXI8y7(x;)(rB0h{%_qSDW;6992M6xEH5n(6&Pe*62m7nfe6^kZ);YP9h243gkeh*H z>!|$#^?;C`3qLBkwzgECPGawVw+wkNF9Ok7Z;C{tzt#_XO-n);zc~M5JH>0$mF@(?BTUcbHC)HM{_mO3b`%*ztVsoP)w<_) zHyJxB=2&GJio)!hHn-P5sl-DLOvh zkIsl$vWJ%+=9hvJpaz`zI`_U;MAbxrDuO{waLE@PQ$7hL?7)~K${iEd2Olj;Q8Bq+ zG8gWs_|(_e=Z_bb{W3l!#9VL(n7tHz_BH5oMA^H$-n8P)D3wi-3p@dZp)vioT+Tj) zxP=9?N)&5?O7nIhP~AIb7M2{q0arq_i4P3A1c4^AvMUr66$wHCjswaCMay2ek%lyi zuL0L1u5H=^XGCT@kvmvma;3Xu{`mKKsq^!jdQ(|wyZ6Or5rKMxiu=nEQx)wd)N)p7 zZbD!+&8eR>Yk!JHG)lHuUOWny=r?yfHayT?ef-bDWr6R=gp2zzk5nR)0-u`;NzE|M zGxk{pS=rO1am2C`Mv|o>*Jif80=VOS1!8Z_BYE6osz4fJROE4Q5ZZ&yNU&{qk}tir*(W#MJi+v zd!?Lk$8qCh;+lI}PAyaBL3?N@SNt$=GyloyVxn}98LTf0ZMV}Y+7#1rniB1TWenG= z^5x~$Eq=S#?y5(8JYDf=4tL~ru=!`{)H?Nt^~G=IuY#C25rEYF;fey(_p>vM%+mM5 z;)PWMr43!Yf&hT*dskWL*4!0j`5O>2z) z5&nzy0=RXuTzX2m?Z=CjkTN*hywrC<3W?5`>Yu_|1===n-q%uxv}a4!(*0(zzFf4! zUdVV$=yA(MP;Z7z#$`dTSsPofdPcWkh7VU>>Yn?SQDxGUa|6Yc-QEXGT0iH2Sh~v7 z%CCQthVsPK~#STHW!;A_Xyu^%K7)kX&{f?lW>6stAKOla7>bE1u zocT3ZJ8lC^e4!dUs9X<5jM&vR?p3n8=abI(y=o*6w+FB``o@!m?zxd>^k8tm<9emc z`DSJqX|ys63*!iw^nSP^(^}(ykN9l;&f6PyGj3lFjiX_3*y|lhZ@avZ4^(XU!(-fd zKyr5XVQ19ul?cJtie@LP0Rf2~_TwWXV|}V7f5}&S)6`;VxDr(S_-#^ygsQ|66Y$@2 z7uUuXmNeb5m*NLvpZfh_mj2o31G9)&7{fHjs(bl;nLFPi-%4+J--C0%?M9l?l~mTm z$_8Xd+=D`)jTbk!SNJdf_~ojl{hXhFR3dhq;5UCWplvUKyLPpH9bIM5E>Li=I;{o3 zV@W3pBRVd{ww8|(;Y+A++Esy-aFH$6VLu$OSjA5GHTuN0rTGRrPdg+)RhpUE*+GGU z{&bOSldPk5!j-79@GWVvW`eIgA3wY#vX|-7v z{gQ9KRUs!IqC#G(F)!Um5K~ucmZ^u5U3A<4J%1&nSw{DeDPE4AY;q+OqH;QTDl}Vy z%o65c|;jkM3$Cp;)$qk8z1c@x}NPK1^mxwI-?ld(AH9ku1lS(HIP1g(eTKf9yQFSexrDqY}9da;BSyV^WIZV~Y+ zP2x6!a@-io+twy$lXm_%oUn8@lQPFv?DB$CvGqYP!11pTm-18XK)#wro!#W_2JH|s z_g5TE5S(na6Z!>Q6JxaPE1H+?`;3A;%0k;&G>%Puxn%ZGBq~Xi+EFOc=!pFAxg+mqeS=j*48)U zl7-YZNkuO&YALB1cMs2q+Wyf`7pCZPxWjBCA)K-#;Cx*i^VHhyR}pOlop2Z-1-r|o zT0Sh1iQY zdz5=;Bt3F%sEGr9DYvDMjstphTh7!QHxbTetG!>{J>3QTr#aI1r{j_Yy-3IG65y(d zLpJx8T;1K>2hl?@Eqt9c5|Zogo}O2qXi?aYNE64QJa#k{rlqq z=T7Ok0}5J6E1{VgquylkplIlsMFg`RM zPV^h^jK*6wPn}*e4s4A- zDB=F>kf~CWC38q30#GKH-(7N=HB!tK3xVc;)G1&+J?B8Y2sUO%1p$b*5`x>F9o|iu#c}Xd^`HbT3G=w*~8T2bMf4DN%;% z#QYY2HyfkPrHm`$N+3#0OVj3}>WpOWIB~TApg$0#hf=V2stpP`b%(s1hHTZrgh;xk zFAaBewIjE3?3)7Z{g>M$^dec8hXoAI$+YJvyFTcUWMHCdV5iTvfu~1By{;U}u?=UG z^ju8spb4sah>&Z|t9t0@?%%B4=-;Tzu~LWKx_ysC*p(^W^-sa5Du+|M zFcachTP!;k!s8-rxDIJ)rgYhEIv%zW!tW@^OYh7dYO2&!dZlEywUO`E&B?yMpK%^* zBNp5|3{#8f$?m$<4tcHf_Gy2Tc|1b7GKP`xX?62G*>%#^{tY6t(l@h2;LdAt89M{+PK(O8T*XGI?-!%79eT zPedy>ii{BRyu!8sL?US2pOc*(#5h%BfuPqHQva0K_&EJ(V@67(z zpyl%?{~(|}u!Bav$#@`WD+K}K>DHy>no8`(zhJ=^1Md* zn?fDs5s65JC)%yLVJdL)aNX^xCC&kvMlUhy`U{a1HM=UhDqlu~hrdBVJOA}J*p)92 zy|Yb@&E-T01y2x8!1}qc?nTV2%<_UE8@2JMQ9EI-gzh+K_Z2MfN5`VVzzBs3bPn2X zNJXoXL_DfKzUe8J1RKSQC!@f1MEHu4HGzm!@sGnR(kSIXl}t5q3U8f)MI9y9-e&|- z^Fn2HxNrN|u_RV}O4J8{zO0o4^1O6b34Uvyps9`v$6EvLlbPc^ zCx=sJ`zkJ^vJGW6lvOds(U4p0LB>AHzrRhql?xbcg$%$GjgtwUTMfu@b_}-S^@AVI zwa{R|Wh$;~KQ|D0v8){0>FsdsiRlB>kpA{_BxcA?{8iRo4#+}Qwj4F+;QNI~Kbiej ziC)CA*JOKkP#g#JhY>%2L&DOCencCA&?|4b}4Ck&OW zQwVZhBt&Zc7V_57e$i7;n^(}&Ai*t`Wkt96wEJP?K`3#mY?n4S?E4RK`a z)maI%3q|K;XWvoCrfA!l7f?ogX~{9G(A6mMYIifGaT0pdy%KGr9bSEEZ_wOK`R(|A zNj-Eb%CD^RXchH=r?b?{5KS#NCn)^@o2=0&-* zcKRS2t44#j8OLpfwgG=%05xtNR%BH3q1kAPmWHM^S3t4>`O#t|IUtpZI3y5blb}A2dp&y#FF19;@2Z@zEyz`nkC~*3TI<&klAZ& zAyfQ+8F-T^o3E{{J&MFWQmzWSaTX_VaX%LCEC~+a6m`eBb0B2**d^4T-=366vNBPX zCFL=GYT2YzNyr~cyQ^S>O&^-~SYwI>#}gR^IL@ByE~NGqo=8h1f1VW^mUCCrs60qaGA9;WWnP>^TGv35~_bb6se${WjclJ^YOEvhV($UBk%7CW6BM%VijPjug;Gc=i{BtXNy2Vy3fzsvGoR!VCm*wDb9OZP?AIQJ(66o2uQp@1 zkm)1s8glEO+c#4cAJpTqhc3*@9|NWx-V}@`31S4elz&c?S?u;N6=kCj^oS=vzI4Yh za>&mWr?F`0{#YBz1}G1~MXGo!hkV|Zd1PWsE6iSW(ojfFaiwZG(*NkBk(I5tYpC}5 z%x&HoL95NB14bA*OjZV43Bp0dU7Pw@+qr5fv`#{@&uaD;pf6D!lbH!WBw)Jwpi&pKyh1Je18}{CQmAw*gyPk*X zBSIbGitx+(pO;(M%jY8Xk@xI0^x>zB znybEx%1h8+(ce*ANi1>|V~^Dwf2Hh8WCXlty|K+5i+4st>sapJV1rN|i4UP$yx{xG zI0mL-q%f~&?{GAzDKlHLQaFw>`!iaQbSt%j<%f|Jm1zn1Kw*#Iox1o8(Znxmt)0&7B6Gblf0{SxBT!N?-adMPU1Gizh5on-lbx`{rg)0iVh=5ZP!bU! zz>tAp08Z*`<^mhwFjXCC5iRwl1iGmj!7Q3i@mef!y|^JX4ceerWykA=>1|EAMK9_Q zsX6U6*W@EyWjm6qQJ`IFd*c6vGCj9UtB9z@yNYmt)8TG|_f%GRRTXdi-s!4KvhVk? zMc+O6xD-|$e}9$ng}O7DfvEG6S4&-zZu1#Df^Lk~UxCw;`5#KRIW{wVsD>E86#s zx8Q`+FV3uorX-V9;0jHkCCRzE_rmr+eR@hr;6u+5pzt3J0DKet`ZZKq{Z4LgJq`D_ zhKBwH7<1=MGM=|xH&tmEY&w?0w#9DCMaJ_QRDaG@i;_B`@rBfvPtSjTziH&(2LW;! z!!k3g$>wUlyxe~3T6Q2G&65!tlrkV;9OqF$rrYT81(cliMw?3~_9doeeXj+6AM9Yg zJpj{KQMkfTSqbpu1gLy3K_%GTTkKW~E0y-t--+fJc4p)})`f?#3XDjy`_*gsPwP+T z(?Q#1_K19Ku6v?%KEsX^Bm0|U@foJ4I5VE-qjtg2U%pLI0L@U7tL8UN9x@at#S$f` zBq$WhC+BL%4Nkz7x@Sa}E_iaOx0M}LlEL4V6I3)g8DTFuiT?3SxIXI6r37<+ACo|Q zfsff~7Eqr0ly8?hE8p0$xW9NnDdM@6DrikQRkPhHfuwFeflq7-zWQ!}0Bu;V)2uOP zWoO5!RE43^L50dynom@t9RCU8XqAj#)|`lFu5r zr76+;)$i1XIq(5GK6(wwIRB=$w(F}$#F*HMa)j)H3txv1eM1jdl*FI>i9pnT!yN7l zg*q@^C&IIZyO?G>h1l>XFqB=LNHw&k^RAoG%o3+BL4f=;I?X1Os-mMa1Kb8 zV1Do|XvjfC_HCuzQ(Vzvl0_6;vtT|IGpu^i?o$Gh%2x1L$@}4Z(xvX-^;g(_ZR5{8 zud|QEhEfqevk5jUs92eDzz@aQn=BmXI8|Ntu7gPW_>_Mq<6?LFRfb4t``Ve1^Arjx ziaVUEJwKA9p;^N;?)PVd5jR~90P1p_;p{6@b2#SL(!XtAC5zq3rlPC;lf159yJH*z zgNgrL%chmI(pEAWS*Y*w@3NwALhprX%Ii)IKJ_||8bjaEh)p`fFmDWNv_9C6GM%0% zD>7(GH*juB;1BrCof?2k5!=fbFVd?DBW}59F$`!^6H4K&>?Y4|+kKr53}!8uZ(Qo6 zcg{lFe6}np4C&#&5QIXZ3&5pg???%Zif(zd(fc+MYTo)pP@y~m^}{wVtuscEAw45{ z%Bdw0hchZ8b`1lSEjgW)XsQi@%Y1sQ?%bie@A`zt%iNHU zWX`+zlK@NS5zHgux9{yVZ9_96=|z{CR@btlbF;GrAClc|k>eWULVA7y{atV8G{Xyn(dc%h2d2Qn6i=Gr{zfmc(K)x>G zm00=9Q^#MmmJ^2B>Gx_5>Mt%%YNc(?nCq`Q$%P1Q7YVF;mfbY3<&V_lQ+Mcd{_QvG z_jJ9qkRwVQvII%=NYmMsX5fe(Who z&~BeIG>+c6HZU`jMf$KH;CaF4qDpD$qmY4` zmEgXl1G*@W?s&q>^J)#Uj@nmqj>UC>bCKdLW*T#FX z)cYZ#jeIi9sK3A%aT#4Eg~&e(7w4O>ll2yV(Wz<@*BPDhy&q&aUQC)76FY+!L&M*{ zdesiJbEo~cr(KbZ_ee+tZHHNn&lahNW@ds3UQ3_8Tykd+vZmJUT1eQ|)^;{<;TP^^a{ewVNi>l&j$Nxn=gn;Pr)~Y4eCbH) zmgSDKGfYcPiL*Ghu{TTA_l47e0KOTiv}yMdH9xEYb*W>Ih;Ku^&w{?Spl7emj_KVr zSd&N61h+UuM83Ehsh`p!3inLEA4pKN?@c`$XWO#~J!yLTY7M9Q+gV}POEvcWl}~CS z0N8klaM;R7nz$`SLWPOb6DsygVc$^F(ReFEx$jmvObM6Wfi^ppJJWw4yuu9AgY$25 zePJp$0YlYv80ILIj-|9VPO7lmGJ!WQHk!k0-@nnGfG~%#bU^R_8(VXIv-Vp6!Jqb`#p&vp3FJmV9g zmcF#9@yB2J_hBnW9Jp+?7^c^*1C#*3U$C_*xYl=WhCW$ycGdcN5?LCK`MV7;R>gGRe?={Gr#n>Ta zEo?qfaeomM5^6_VXLNkEmd@~P3b~n#U{mpVwXwX`_#{)0&!t*k=^%1|GobH=g%H?2 zxo@G-!mTQKCsf@(;_}?!F&*7~tjt-ufpbIf>?~7}ntqGFREmhR>U1_gnQwr zN@TWm+(#SUNVB!VITLa|sKMmq|9I2IbBUC;$wvGTPjS9wKD5hLMWCto8I;H<(-aV&MGt#|KC&>CN=S#xuiyX>5nzN{>l4VEUV8 z!Mp{mzqpvs`0%^#uFS>0u-i=-pLVYFdh*sYL0Ft32 z?_!tvWUA$ygC6C)Y%q8Bkox@Zg%WuO%FzJbXv^JgFa38YdZ%$DLtYUJpcasnCjiH@8Zj8v9|`Tml0yi$lPqz>p>W7}Xr2#C9*!~FSd zAyi^(;Dc#7X)AbLtcKaA_ZjV#`(g2U{D2JQsdV}=U;oXcnPxe0yg3N-k^ZT zj7c9pf$f30D~BdVgQ52)Qw}$X(LmNI*bsoU=DZe;A9U@sj&j&&hP=phU~aE>P?t?e zw%VGi`=ym|>3&168a*BAO%_iRZCvosTc0S)CWHTw7`)S{dyj;~75z4G#G$$sBE1)L zEa*_SE)g>3&^i<%iY?i|07%ORxy^7AX3H`sXWLDrcb>I7w!Ya38`5;uIk9&k=hZ05hxpw{2AjCOQ3!& zxws>D(w*4S+H$tvvbV&1p0om$w55e?UObkgSukWLOe5(D*jff*`sPvLFixi zS6MEuacCda3Utnp%`(5JH@NSPvgT7j@{b?>!ysa7ElR+19ZrYITiY}Y3@@_3A^UxQ5UsH=?Pk(BlHt!#h;mq*#cti)OafR)s7u;0t)m>_;DSZy zN%sAeXW5jE+tBSSw9+YhAmB%5q&>RqEiYTayT5J&^Ch^&lG6ZY++9g*b(}~FO&E9@ zp>VSiK4v%XIx!3dTMtPr+~yYxQa^8FKUux-OF8wL>&QJUd{3mv483=8fDTxO$(*r_ zn*{JiW@a{K^Al+jmjs;q92}GF>}Onj=pdU$cB$u;9gMwTyjc?fbAF$4uEC#)kOaU3lp8 zs~EqLj zoH4uIujnLA z11>bHo##T_qAa2%L!zQ2COO=J1JX>Hhf!4ApmN(Kt47^amuXjb_tD+{EO(`cGvywV z;ne&c^$%hF67dCE?7=x!*)F7D+i;yiP?=SquZZL1s|vK~*~xAM-B`~C8YW=&v;DIv z9ka(~JxP&%72P(kBBKv$w&L-sbiWW#EpAL zkM0M$#~d;x_pud%idO=k_UJYEGp2|-W!2;*>Q`7j^W4zORZscV`&IDgdfp?|dWB$8 zq=<*ZL}@5*N==3B2FHRpsCCfGSULo`}I|ZL=bkXA#q_8ANJRya}ZC<*3{lHAghkdWAmY5=#b_8I86I5A3MJpn>O?Y&<|lB6Ia%9>{A z@)ZYk`R$Zk2CuVScP}qZ{~!`l(lXm6tIHcsCOzm~_})&}y1q&k3+#%^9UK`+Xz@o2 zyN?@r0mKA)*M_pi{QAvC?W+a@(Xf66)kI$FrQFC$%^p`IgQ~N$dOFbX)`Ru4Fg$$) z$&*U{SgR7Z>=<)L07u~%dBhOG!n(}fDUx(0zi|>?mHGLywn^KWjFGV_Nojk}^sJ}{ z`r={QmH908#*&H+|IQuygTq4&1nBplKcD2^%f9KAD@m04izEyF&C?$&oVc)H76MC2 z%IVD4kYZI9Yd+HN{1tp)&h*&L`$`obsiV*2IBnqRmqpl3c?}J5D)0{=>5@+uFSVNo zR+-V$ZQ_cBW9Bv-8y!(}!pUIXuBC^Qbq_Lnn6j?4G&Z_voA&vu`a=N$vEJf9{6AwK zi+xGrk{ZnhDa6UT7HJVxIVgb8sA=s`Rz*cn`d0to{rt^L=Cjjl0haCnnHBuivYweB zQN2A8iX53jzlgPoW$nT+s%!J0aGj8W)xMP7SwXAbq?uJC+vImhMlVwYRQuRDVj(B) zbvMRy7INp#ouBESS@C45Z<+e7ZH4XGAe0@~l){N99M`3|b&FS44V)}&Y(A;`Esi*# z%Y*F@-Q&%F-_}dm+uZCA33`QC%4R+ngF+<>P~^>L9Rq^Cy&RWmR=r8mk?rk|ek2`G zOZbHlx{3#Ofm7I>rI+ivXHy*sTOXRgV@aCXLi$k0=A3pePCJ3gEtt(Jb;9T^|E}9qI^stFm62^i3PL-XRGK%g|eQoqb~q z5sUWV6(UsmUG4F4&jPk@?b4r}AZu$=r8)cs4l4OluMlFP&814$AB)sBT;l+Lg<;7E zthJ&GIEsQZGtH7C?RPk25tbBI>08@GYf5_-3tK9q(c6mKkE(2yULLvDv3e$W zN~e-pLk&dNT!ElItFA?`75SJlr!6S6seR`*w?<`9#_EvV=X?Ep(oyy#S z`n!-KKg8Xk?Ae0T^(h(pn~~&YImed z`!iJu)A|L?1x?7`XV9_YdoS+~d~hxzA#6o}x(5dZ5TNif3rNBE_XYMQ{wC*{PXlj< z^shaj@;*E=VxyM-lqC|Tr7>!g2cf<@Hf`*Z`g0OARX)WNP1fe}P>9~@8+W7Z0Begm zDdRo-$G^1rTr)jDkHDb%qxqzf5##9hu^FQKQX2bY+fZqOF7o#Xtq5Ad5If;J7yG9B zWS^y8UIULEvfez#M1hhK=j?wQbZPD4Z7 zY}`SQzOazu2;l)CJ+huUKqzZDwAHQ*ly4zYl<97h~iE!u05(MsJ_*f0KOvnyNWGznPX zG7D?hreZ(t{vhge!#w?d0dV&8Od9xYmjki4-1B*wEx z_<19V@g0y`f7qcnhEIx@o}3-(%CPHnSJ`AGQho2G$#!cJD~N-mUoKfhIrd}{ZhLk+ zcOT2aPGlWDVpk==7J5uLGvnc>%ZQL2E(ssdrGF%CLQU~pM_je$pj~zkf%Hj`vBvl* z`8{QTRXs#3u0PuwegjbEBZ*F#>8v|3KN*Z1$GY@_k^4-du&OAT!Y&HyCa+O@;n!j~ zZ@$x|3|bnRMP`|EecXNvE_}!Wf-bX=l&s#g?23g?Sana$;Y6%@Q<>6Xsu~jbZ~->y zV$#sYe9CPh68>oGbDN5)j++I~i{tf9&y9j9Hfe|kzX zs-%Hdy?;r@KO+|f(CcyR%`gkn8NZQ}6A3m`nC>%W_g8w=VtIq;LyD5)jnuyzrcyR= zbIm5cVnLHLH1q~)>!X{e4ERZrrGUFBc`Q73DqVfUYvo)=G9zUrpzuqjC?SH0#|8|o}|no)Sv(YaMI%jKT6 zf2)T_OL`qe7fW&Yq$hn2dQo=r<=AI@5~we7!2+vdqKGrsO3bQy(P7-vFPkMi>!aN` z{L`%NbN=_SPk6tIdcGsU0tzcKak(NTr1Pn5G&&5`nj9Hbl969yyOBAndltG}p51C; zmaCtalRfnMoa`-R5#jXJ^-4s%Pebg~lyFb6K7x+}#n$g^xR$}~mDWam%uDybj8+Te zen3`w1a^_V;WYdMP+r5Ocs%ti7Gt}uH}$fB2-V>hF3U+!o0Q5RfstVLXIhywInpe5 z@MP-FSm!|t^D(^*?VN>0du)`acxC9Ypbzo-Jg+zAw4Uu90aHHzEk!G_y1b(S=QBPWYV5xq03wJ+1({eChqE(G<6hvp^(u3wEd+w zaZ;n3b8YnC_ldYB&yTk(RvxkOa5I{;oB`U1sU@P<_59wSVZq!53GC@NCaJ@rQ(i!C z;YCS*{ulh^YdqJfHJn9tAL}Y7bZklv=wXO&UA)nm0T0Y;}g|zD{ii;^)ZC$zI z8J?Fn-7-qns-(so&MtA@Sm<$!LPBt5c_nY%GTv#}Km9Kc0UcUePUQGk%TW$P1b>H> zRoo*LEwZn$E5>zwWH)8~t7X+(P_C=a`>9SgV$vp2QGh%d85j06loby>vVHl{N! zFltj~fguGI8+KLx9-$R=Vo-R>!l_N0N|1ezUewWiY4x9Yt*Zs{P03Rr7M(XVGQ${q zX0Dl|DZ$3|Cut{N_QqEB%yWkZTc!6TU16l#9-J>w9XaYcD&2=R@G=oqAsu;9J+;|p zp>@6QS4rHB`v!F+fc{FTF%S?*vs%Djtb|)3kTL%mqoR%g@jva8>e{{dlQQ4_jv>b5D_e zMUt=uog1e2?b!riR~o!vQojDW19316C|ulN>-^?EVdFJ>1AZJhAaQ0jpxKC`OZF12{?QiicA z?~%@;?X`i4dxx69&Rt&P0rl`79S=oq*$>Og%rzF{1s8A zPY4RWpBQm$lV7n4>3zHQmO!VJ#~aYH)6mTusfY$kd%V+y<#prDbw7xBC)AVgIz^TY z0hM`7)*2n0lD6*%nR^?JIeS9M>v@Vq<3f%b24v!UWv+Nj4DT(XT6<$clWruD{v(Hu zP%=7EF|m&u2)B}4@qRstQ84cMD0gL7o`V3*Wc0+%j{Gb_gZKLQdPp90Ll|T4owdi^ znn%V>?z0&yDyG>oYNdaJIjD0${oZypJz=Wg)vxwrAVM4%Z`mJb*j~&&fi`cZB-*tT zz|Vd?KqB^>iscubu~3;&fYhE_@v$#6`q&G?M%3fxOM%ex-!a!M0~f#CiZ)kiH-Hu|L~- z=C2cnu8kUjU+=DSeAIP}b8lXMERWUXZ_5>?%`XzDmV9z#o}|Bu#kK1i%rUpF(+~gGb}UCv{{JW99;FK2k@^w-f2?n}eFuVz4bYQe zUAOwrb^pg|+i`3@GbWK(Dx0{j-kkiiD zU+z~Tvc`&Qf@Q>?M_XUaa8J2LAf_wK6~)eo>ncY za@s`uXsjs z{T1(fM-t!TeCQ#+r~?w0E1_Jg`xU_iI5+$AP7h+CMw88AoWYFSL)6Tw|rPWmoNMuuMol zo72k-%Ku>8BMZ1NCPsBYA1zsB@Uw*9sWdODmCT;R05y0up10k*kH4@@uNd}#W}MCc zlKS4LvGi^P{u_?u+PJQJ9 z`Dg0pGP#p7oHq;=?kpv$E+a{sxq9gMf;+qqU6}9b*IxTd3N{wBTN0N|u4dc5WZ6z> z8O<1QJWBqr^kM=S`RS3?+4T_dUF0ne{CfFv!+=2lh5+S{$OW~v3K~U|Tavy6;2+QPB@^K>I?YgcC=T8_@aOhlw=)2uHhcsHM@iZwW*zvc7PHUMZDyG5ijTMcG zV7tic1r~Hj!iLfQ4FmQ>!P^groVi{2UcGcbYp!_c`09){!FdS6yrpQL5TJg&t?ywQ zCn*7`o3In9%9U8okn3U0G;Jo!{f?7BYhGA&{QiboBum`=GH1q<2Y$F8E#L6AfA(j2 zF>SYgFo9Wy{r9n<9Iy*^2IZk2l59%{2!h(`(LOs&yN$)e;S0du!j z9RKw$cew&gyOxI1EHCxrcY5}17>SaGNKGO*C93}O1btkHBHIa9FzfkMejW=`(;J<9 zCEVeAp^K})tSr!utlWCRG1Zw8Umv|8*JBLxj%8qAe1AM)Ea~4zof3*}>11u(bSz=7 zitv_mVnRf4AuudCeiqVr zOoOo4Y36xzEq`ugI96izBPN;>7km1BDm6K7gHyyxcJ%B43JXm?#iIcohHc`?x2_p! zqd2v@@aTl33ZiPdZ9(zqck|P0mZdz>)A|+MchBN337y|G*H9U0Od_l4ZjP83`Fk%d zU+T`|1U0Q!Xlu} z`_wrVI;D+Tx(`T~phG`YulKtA@r(XobTCT7xfHH_R3iI;BG6+9pl2U+tZy!mQy{lK z$f?(}+IU4F^N*T68P%FE8#I>pN#WZ0kzqNwf46}Q{xuZk`i?xAOYBh!LMiU{uBEsV z#e1*3NEYH)ZMtMs9L>HZ^Oyf1H5n&m#W5iN>H0?DuFB0jux{d+w=YhUYlSw2jX6`J z=bpF|_Q(0}d;FIAIJO}?e4Qccx-%?>dH?yB0pEvh{PpK4J7sQ6kzr}jw&fq1?v~oFQROd zW0Qgoe*d>w^eO1m6NBYFY4;0xTVPm7kmFQl0G`Bm0V~P_&vxO`q?tm>e42CfPafPx zF}D~{J?1EZC_ex_tNGPde9HUwZqGTrT~!IUxy@{e>1Pzf{QkXZ)tjWj>S`XN#$1jL z7Flq!XH>C0FwQp5d33FK&B?l2G~9Vnxz-_G{{z6Uf4>}lGTg8yp4yB}>8 z#ZSkx&0SeAct)&vHJSq`N8X(`iHpP`*tSA$5{)s>;iJ)N)28PXH z2c*AXTCh#h6RyA^!7uD)3YAlY-oT?Qrj0R{F3DKn^VLNk^~lz+1A&j*hi)vtd#`%TGEL zpP>Ryj{mMj|BV55QV2@VLM^c`rM836v;J~YsdI);h@@rKq9bZI<8Y~H^(gsE5*MBG zNAmSC|D1cw*l;01hHBdzBjLmDldvXm@k4hjk^st^EV0uI`!sICi>{WdEKChSJoppe z$^&ig>Get`ag_FFCE(wCZ7Z}Va_ItY4`g$HyFVNB%sa1C+FkR_`+ZWxPd%lMnFrBN z4vYuI2$<;WMcYgMde@+lDn3P3WraaR(egC9oH{&};?^1}nJRHq%-eZQBH>5~Op^!$ zR4H(gq}nnla4DLd^1dL7{wc}0E%Pc%+h1mP_vlSeM+bI?ufgP|%?|qbdBKG6fPLlU z`NrynmziNfLKN@5;jR=jo2`#J@om)q;pnXYnqb=~u2K>L(#-=>(nz<0AV@ci?(Q5Z zEvZ?kmo@zGoR66Yjo=Br)XD40Q6p_~4k3l#{kq z--{?c`8}%9_jj47`18wO%g9O`Bp|xbAwE2vlPvnkbD4FrzW`Z~1NJWJZ#Gouz`}Jn z{Q|NRDBYKYXLhcnAC=S|3pY--I<$w)%zd z0VlG1R}VMOVy+?PjcQA`Tf-ki=&$iUME;_wgXO0T2-6Bkz9Nha_Q~^(8Vzl3_**T+ zeGMxxS7_xPyR<33fNf_Fl2H#_0h+{JR_bs`_unrqF7)3|>`4p2y0Of4_PGbb?k`Lc zj9HW?hr(?BWFR0KV0Z9X!Maama&tu{TsJR=>)l0XdGbJ09Bs^|15jxt5`DcGajH`U-@F!Q7H7Z;mY z>x;D@dg)84bj1LqK`R_CDWd&`_WKZ7EU*SZk8ys5WA3CKqZuA(qk4 zpljW>+bp`&YAVXM;IQ4rXzw#n&V2E&lvbJ_)BGS`T6F{cZB~xu>0dqFerS5*`8(34_)% z+(x+YU5H@JD{mzMFYODz`6cf7YmuPn>Qu&ZvShd76+3k}&%iMcj_7f9e|LJ$ky{MpWz}LF&E~_}z8}(#G9#0nPV3Oy7Qt z6prbEaBeRl-?^t=f`|6GencDp6Br`~AvMx5ta$m?t{uMRilucwEj^@=eVgra)`H=& z<|VAvU_^H&9eZuw%_7^>>Q=HV!QLc$fF%QHK{Dp^$@Tm>gn#cFY`rfi=4F(-&vMZI z)h~Hp-csbl`rY)uc$8j|?d9Nc1JHx=DXbwZzY)^lsZT!5G4^}H#A*UHwk7QGxyVts zoOZ|c<~rbFb~At~;gDXo^p;7+;kUQzfw2>kWV`i;SON3OhoKu3svSRa3hq{+Qe1OW zT1ta(69VhLZ2iH<*vqar{X8yR8fp}8?;2ZQi_rR!Ua+&~wTIb!iqtmsKkV$y=kfbU zV9-CB>`kfp9#vYPlx0t3X$g~d5r4E~l*g*eKgPe7<@npN7i#g~T0k=)UV^`GayS-p zt8_b6)4wx-hEykv7{U{fO&GDjy%PCxt82NIGC|mNWk{!`bka&BxboL$YTk^ml87oieY^IY2SRIPq(#- zLK63djm)dg6ocI;O})BrKq6g)M?jwitv_rkCGLjc37wSgW{Kt}#d`#Of1md`!>dRK z;@+%cITIHF%0|*q`O=Z5p8vP2xa_&?CKBjNlyU(EvYZ(D_w1%EWwU9F$0T1?ewF-U zI0^m-h)ikumHpk|A5PC(X!t3f-hW{U!OyKiRHGp>TN4%2?Z&`&+}QtEf^r$ZvRYoE zEr}g`U^0ye(~({d;R?K9Gw^t&$%nyP#RMzgeOVmE&1 zO>5h0hr2>AgYm!#Y2C++EYv70n!h*TIekftVrh2bZ9ZF_(LJ0Hgogxn|D-P5Og*f3 z$)zSPe!npJy}?ebFBs&51C9KyzWj@{4pR_od!?@K=mIu?D2o}75^HPMeZ>I{aLw0E zEAVUU5?bjvbS&hG6@z0ZVD1rZcl1s_eLZ9k%QQ zQ+5ed>yKu2zeb?8@AZeQSYJnmToY->e}(^)Lp6p~z!(5^qPq1YUdcAy0~7KRn>)^P6i?QPkh#TOD* z)X_n@w%}$Fc0IQW_vYjum38HPWH}R*mshI}xj#lq1x>|vV|Xgv>ByL_!(CwcF34NX zSh!h;>k+rSK)e^bUUP^_&=JUC862m_i?{16`R{eS@A4+(mYZw3z(wMne78t`ic~d0 zZqNr$&7^d)%D)|C2en#MDc!t*yq8Z zjU;@N{L4RdXa4uXYMWpr_g~I|B7E$R;fT05j=c}elp2w3f1vwscPC=(A)73TZ6NoT zyZN6e^1o#d!PXZjl}H5f0?2g63VcK5b!$+fk|oP1uiNc}a>D@_axtgWF$_RS@r_GT z7K@1H5xZZS*o_w@srTQti&${poF1*|@tv4ak%;i+$FyGoUe-DX`H)M9?JfyfO{8i^ zX2shvqApe5Lg>f&q2wN^E|l1hxA<}nj*F#3i9h-zqQiB?=KKLAV z#9{@Wf%CSBM?z*QKb(}WgZG$87}gx$nUT8GQoV=qhA93!AA%;PE!oaFcSntp^vpEg zng;s;InfrPI)wV^1M3jU8sVl3`0T3+u1qrN8B{2ZMC+RvNHrqb@9UkL%nysO^p1lb_}Cq&-n zVJTIN&PSiP-u{K34-%mL^b!(t&8Iud*LP6jaL1Y1@XnOhQv4d~vqjNR)a zmlS|C_PH*zvJO>dX$`sYJz_Pe_|1ouPWpcLljsI#j)#6-_{N$)(1j7}tJB`=RN|QRl(4r7kz66VC z;v`9mvVTe!d*AB+n*@RgFe)nLV4Noo2ftb-`B$*} z%J8%u&WS7PAmAJ@n&orqo=YBG`BuHRjIx9v8sWCSca zz`UNafH3!5;ugsUdZChJd;mk*kx=Q!8?+zIuvf>2hFK6(OGsXWuLLZ#oM@ucpS}So zM}sK&3?#mB^RFKy*oJ4bVd3nCYj5{hM%6FXf7AkZ?Y3D8w6I)PH`vpSiRN4}5L26_ zC-;d)P7#A+WHPfcPRv)eS4B&*b+dBl6EtwESyu!1{Ux5mYwH8xUh(30Hn2xn_twu3 zR_2CmL9itw6_^jl)A}|7Zf9ZE4B8Y?WOXf7MXEb&4(%H={IRZW^Iigr`&Bm}t0%Sz zNZ0%%jUPw4>wK^5UdS3}ffF4WZ50(8UPBc#O-d;9Yo8)^Cblm7>;yv2Ua3dC zryJ4@>izCR0};C>=6jp}?@no&xyR&hw@ACkc6E$Gr!zPAxM`p76E1r}6JwR+6;<@K zJ5dR&===7?NZ3tae{JBEegrzDZrbIk_cI!oA2Q~kOsB8c(|}34SnI$cSZK8=Iwo1d zBX~~6#Ggj7hNxU=3Sf>vlcr75)S}USpU5V@yYb-mOWKF&;-X2GZG3VP-+NU!Z~;iFg$xDhu%?z6obg+u)~Y^X_z%u~kvr;Am| z&N4y>cbv~K2@}k?wbFS z{BaHB_et1-8$Y85J#6R6&7eE`4eA3Q?Tr<@t-r}7a%jbAvT8<+k=$t=5A2y=Jv2+* z0M#9Z&H#~4U3$M1nq6`NXh^d9-q@wOWHTb{s5?-Tm*q$oekKez*ii#)+0NZLTM2T5;W3Y)HX!xw-e5nmBtUWP+=p)k&fF7XL5Cs$H&@rK0g*S z=hc>HVbeefNwGMS7p;x47#{XyKSB#vgktGX(BH{x1^>&(>Bew%iozsu<$jPkX0RGb zIJDB!ieDG>-+z*3?q({(-BNGqP+7ns=q~G-IR>8FtmqhoR!YY^DWxj=b%*?{Q3bZ9 zGczKsr>U$h>utYfMq7A&slaY`I0JDt7X>q=?z3v!ZHLgDRsR*)Q{(B0epik?MR8OG z0~XDEvZ%PZ5$Py=#<{b+t@-Mezb#$B*9*o$;w;=LSo7iWx>(eeu8~D5_WCx20`PZO zS%7#6I~wD=%d~@IEO;HHRnv5P!t!E&_v12xd74_CVYY|;t5}e%+6ArND!PUM6#C1z2zlsx-z+L*4Dh?S>1b^iMA};9J z`Ly*BcOag66jXV z9EmaVA^Aqj8wtzhjMc`$#i>89(|bT(vMqKM=CRd)qlDe#O;*#Dl_ik6xWXPJqp7vq zE#2K=CoA4AC;WnLtFGTvYg7A(G!G@OTVYJO05BfJrYigRq4{r)8gJggoq9?>mOH-O8HQ-6h-TRFlbp`7dOgQ&>!%Ta z(n7AK#VA@)dxKn^S#0#a-~OYCBS1SwV`%g5~(Pl zq)VzrqfRu6(R{uSX>r}p#5ibC0^evRp`QP1iD)y21PGl8Jr0%fR=ZQBv@+8Uyk!~~ zR;_7`kMFp$`^v0#Fu0ubXT3_G4}X5n0BA!u#2A-nQ%(6;Y#iHigk$=s=xK?z3~( zaq;_1b-ldJDsHYUAn(`(0w!Ys{g3t zs>fPWm3~VwXhMW=s2y&nnX0C*Ou|9jzEp3{+yZl>TrdjYhtE6j$Z&~?uC7{A8-s-M z3fP>qGlXKZ_~((}SKYop-;Ma4b>;;TFhGUm&*=;`NGTQhhXiu)K_535}^;w?D z9U!+B%M*6TwS`}1lhTf)GMbJ38oep)74#6>JBFKf4?sGT9FETr78}2Y0R2Xm^>Vur zR{0zDsSYrSZyI#}u^rD@V9s1fAWnmKIR}9utDu=-`c;+t1c zPfDpl*w8d5l?%nm@D5ZN*YnLVObKJ~g9irkbT$;?GKloTN15;0f1*tGB)tNs!Mn>G zG>&kN#T3#w2LYrVzWTC?)1uPr4<_ndw}b&01ua zdY1B#Yid#CiSzSc+UN7Z#kPvPw60^BdV5DI(Q^7SF*rkUauo4G45tB*QM)>ZvxWz> zu#NaCSFLDpK2E@k@OdhD!)TkU>pvyay^9wFPCi-jIv=yn8L&6<30K{vjg^auJbL_(WJLu^zrpx~r89ht@(phUFX~kx z1|0F8GkoJ`Tv-_52{B($sy(usgun>83&4xSgh3S9yQ$aZ_AaZ%K_*{lw zaBMA=QoqESQj(H<;g%wj&jIvRd$0BwdaqGqaj-&)(;lMqcuS^MW(@Bz^LBrb$S6`* ztv6;EKd;M-<#qq$9%yoaHSnuQ@r$6(+}Cj$+NCCw(v5^pfol4Saa+r((v@xYcfYT- zk5GH`iL`$C7~|Q@GH|#Va`yDozNRh0%03+g4V2i=y~mQhiSO-au-Na>tT0vn-5({@ z{O@$zXBGGY{qrPYo3;jwGXd-q0n}&g)mG8&`DJRl8eELE13$d&HIi#~{N5r%_nz;X zJZ^DM1}j@iN|l-|ad5_IGbkHq3j z{S4I*ARNM)%xdK%j}VL2dem>-3KmW8b|M#@F&&5_SCHwM=g=n;wZf^Gsk6UVCaD61 ze7jf$GV@g0iG1*t`apM~v=@e+6Tb$5v}I zZn$I93L+oHqYI7XDVt^;v;QvtXX#E-!oy>n5}oDXjCN-$CTOBI5NV&SAdINu^;!3O zrlxaY3F5O5pDoYa53)@%z1ApBoQ*asp@~(#^eL*sb`_AtoT<4dpFHXIm ziDya`8OA2fKxl;uGma>u7(^;N zw*0<(G5Z&m3EYVe$!*U?zok?JD zO<=xs+Cde*L$)X|+KRvwsu}3CBbi0Zwu`h&Z=p;QPmPDvnDvU7bTXA|s+LGKY2z)3 zEUibU8+57!%D<%x6JmjcgqV&`6EXx6Uc{DuJ!OS(orN|>mr-I7X84OCOM;|7y9MJ+LiV83x9|PUTjzWubC^U z%Cyj`S@Zi%63+(}ICtl20~s}#mBtF_hd#VBI}eXG&!cn#r(mRV4(3nF76wr72e8Kz zBoQmjQAjuST%X`kYqKIMb~nrfYv|kQG*f1Qf8(NWvLJC5()IKbeLK69q@3*u>qZY4 z0?x4Lc}yy8^wrJNiR@ zO~o+!`lh!|Sj68(E0L@YpM}<+$cSQE@;}+WF*5b2m&b=<){9skN^D((qr^qMLndh; zOJ3=5>yrFScCP#7Z4*DiZ9k14FNy9De(HR2AFVsrtveJ+za;KZWU)ZtXc$oKev!VC z#O*I;wC|t$!+HCHyxROt!-OEM=5@%6v4PP@m_7RB@ne?2KXZqq!%ijTT%AH`NR5PU zV~9wE9^t#eu}YVTL^?0r`K8t-$dm>vLs++Z(g1mhQnl;)G)Y`z)>BXG*YpH?swgIO zj4)?$&2UFo7hVz5<8)WWTrtM@Na>#BSrM<(V-%w5l45mvGF~g9sHw(j^pyTd2V5Sj z2eK;re5G$)dWl*wTq%(WYYh!M>#CFJCN`}-4`|rP-RH-Mdh@|gR>wlHht!2gR{@&c zKA%w=rNm#PKjjns1(F6mUl*Hw(|d)ABpU;!VVGGvV-*t&VQ*O=esN1w&32S&+aVFt zJm-H0rKDv3`jAOdB$+4{zfF95qs<^`-9sngw@yr0Y9>~rptT=BV?h(MJlOX6?VgQ_ z!5__kL!=4i9%K_UFc~&B@#F+Q^BZ$B6WOi{;w0*kFG8$LY%Gj(3*j#-Ruz^VQ&)E5 z`dB{4ke2Mb*d(%f-<~jZ}RP(`bIrm z;CE`hC~yF$9Kj|=7vX9oK$qSOglMGGpTz^JwYR%oGsuS;5Zv~LX*i=kj zkv4=RxcbKpSa~PD+tzx2OcT-;^N3x%dA)MByEpW2?Xt$(KimH ztCq=?b0^po(@24?Xc-uv9UR~w3choY;9McI=|xHM96yu`=}b{L;(WJHjQ+Ck-ll8AYD6Xh z^=srjc@IJ4q&;z=9ouW5E=DrA5gL2PMAgRJK-#dw3&rFK&Lr_1ZXD4hK|rJHTfO?l z<4jeG-qc?s3l9NeziBR&P5x}r1jj=fek?LF+_QU}Vbu;x&uOzV%fFaHFcKZ2$o}e+ zVpZEHq3DX&g|m^aLqj}A$(;DpCA{d}@g!q8vIM4D0(mw326=2REl!OeUS+j7Y@hNg zr~%CPa>i!kT{ZpFI}R?HzL5xNu=dd9j`+*%X)+!x=4@`rf|gXfzO7mx z^+mf12B~RVl8_Z(x;dD9FLd@9KMSv{K<)3wIkz{L?vM=WGtC)0A+rjWWR=HL^AJ|G z8A0R6=*iZqbKC1`4F-(NLl_sI~e0irblW@5>TZ=;Dtrq+bGnJ#$r`Yd9f? zz?AXu?`tTSGPZrlceRmMdZBMlczF^5-q+_-PD>L>#a(Unr9E~>Fc%Gn2G5L zd_xp3w$ek#MCCN{NY_1qY8#gi$`Gf%3h_2E1rhC?{Z9;Gk@l?jfkMyqay1g?8YV9a zAh1_C483BLcXw;uBT}{y+{+f;XY#sK>^r}z>lm?mkodB7IXJcMC_C;$L$Jn!U$MVI zU#!gWZ@r&BJGdcJDz?@pTwpe2a{_xzPE{UcZ zV{dDgBjcApNIts6ZHc}FSg^Cp4e@I^Cg{jZVZ1&2+u^1v|CUp5U?T^Q^0QCoIA3;> z*q;zFAw~ru?c%|=&mzh5KGxf=WxD$EyFDa&CVtwE%{EN>SbKDG3w zZJGzzPWID}l(|X9cKx9V`e|`=%?Gq|?Dok$uzHY-CB@Sjw^qLSHk(6te?o;=pU;s$ zaQm@s+TJjF%?=w!J?w8uo!D6noGb<7GP$ELg^=$uE`&Y(dty*HzSJ?($}9Ah@3uGs z<&@GoP$5R)g&CizB1lx+;+(jG7Q|idvrOnxajRh7$+oo7fTxnnE!lbY;u z@$@7CZkeqhmQ6T<#-JA%fa7|B|jCeb6o;=!G6Wn6n(T`C_PpwU{u=Njb1Hh+ zDV4NdY0hDgN3%Di3T1Z1qq`TT9US?Q(8Ln_U42ZamnJyI9XCctB(_P52Z}3S#=3qZ z*FaIM$L_sqeBrwOA7x|<;G^wPZ*DLid@o6h&JcN}qv4y#hIN|x z!G6r@I)Qh?lfeARs!4mKf4H!@Hs5SXa$=;pclq>Pr~2j{A5+z{C}l|%5*WpwHO)to z#lLj5X_n~sc8Cj0m3WlXk&$34s%3u9eRFM{d$)#R*YCx|g^OS8Z&1vqEXrQj5QMD* z^P}{xHIfRlPq*D3L>zxmF7Q6Kaaw9jiPrF7=>ua%elogp3-G0+Y<4!^9APA@^f_H6 z%$O5>o#Qxbja_hAs($=M`&d0C(YjNI9c=x6!8iy7WZlhK)nrdhNMN4-DAb|bV8$p2 z^T(DBm1dYzmTEByohSZt@D#&}bzd0sqC`BSL)M33wBMJ4lr;y zyrv^w(!lLwrFONcj0x$K zJoEVGMdm(f(dC_-{f?WeMAU8p{=s7T<+40y3?xZ$!>bqYe4q@8^sf1qk5DaB%zm_c zFMC%t3z%dt2nL+y8_zPaSob7yb?kWd9lKsIn{JCYEZ>$ep4x5kF--d2OefoFY@AuV zgW#q{BnG&y4f>SxB_KwO{K5q=DMFi?t3}jg2V<4sH`brovRP~A zmRbb~2!0SME<#+EgvJI|EK;Pm-sICi3+p65<8QqeeM|6T^UDW(;v$Z3>KtBN**cnu z6t?yU!xAnp9_OEfvLF4~Ahw@vIlCXQphAyGD(^9{`j#^+4fBWJso_Y0%nc9U>>oh2 z`3%YDw|~_$@$qVeOOD_4L||3_nj1O)7_8Pv+coEhpen7rxXAq~w6ReCOp} zHJM30i6w7&Xa1+91H)SSl5_b7h)jb{zE^5@k3&@V?k|B|6GbGgL}V_^mqtJ~^&cha zN*@Co>a!4DXlN)7E^bIAksoli(;=;rw*k}6=VKHKkf6aiVp=OKLKqnOH0`N8k<>j8TYKNA-YUTKw4XX+JP zFtLP#Ns(-jE)rT$+9A|U_h)E&eO<-8^-B{VzqBO_&_uu*b%fu_w`x;gS(`Tt5D^tQ zIH*cuV5%*0Zp;rdN$)a5*`Qt%&++h@mnL1Qy60S?_tAAx(yyk67qtHPnCPc4CH0%@ zQ_HrL%dJsrSq>vki@I*T86$kYrm(UjNg7W)+11?dOSn@9-B9UlPhE0?u8{ON!RvUJN+RKwDcwgAp06!` zaCG!D$ELyN6|F$3mJI_Dp0rrFr^kG{_m1gbQ%B?VMo(tJb^i}6G4rXIgo7?4!-Gw0 zOdVl>`9UM6Xv5GxcKzm9$Cc(yD#e^nDe_57pS(Q0Y5Ehj&SCtM6RFiJOKtYA66l-a zpL)JDJFLinc;r-6nAg_Cft&dZH9mMa0=i9hj9CKC1)$0pX*}`R3v=JTTTZ;9q3BIA zRSNsQhhnP!f^8u_RKTs+E3wMyJcbl7g+GbQiTT{~d3i$cfoW)|dO3;wUx9h=IQI&^ z@FD_bfbly1Xex*0evJf^Qu@$8+nIwy%(GoSFX_=l9|g2WTRwH|D&65_Py0y#k7@5$ zE2vml?y2wF%X7Ue|8P)hP5?^dG*LBQP7bw)_q?1MyKz@y3Y!kM`7+-gO`wnd8#m+i z|3*`}%_!p= zsQXV_BCOa@Wd$kT1^Qi_2_uUL(*->J?~40=iEZax=(-I1gi8S=cDV*2VD;GBd1qpGB5_%a&~&|;$&ljgTGKzljK|}i-?Gp z>k#!RO$6YH{Vh?_&k%5=NGeq>DJ}g@Lg(CRc7IJRR7p-oA)AG|pI;mFBQC2sWI>ZP z0;VR8)CJh1h@i%1r8l-)ouH?38d41IfDVq!!ah4`vC>m?*xEYEIcAdTuhoMR23!aW z*=FsjJ-oEGo<8Ha&Ct2>mxrwY;Bt;JJm#_*m0HoPvXdnL92=R+wZDXSzS)o7I0v6= zAoux-Y@??ztF?Jy-xwLNScst)s-dY$@{J`erCD}ni_MPA`Ylf8GZ9K2H(tD+1BwIj zCv@{t@8AdtwlQ)Mucj)XCmU#pMHePSW|4KXM5|xz%0g0WINqD0V7o-Z zJE*vU{FdF%lVk;&!z<|Q)r-8ItgINz{*)H2vFdVW_)-u3(%7x6Ccau-DdV7owMJwz z&K*7kLo{*{w1kOG94TU+BZ0Pz<@Ih5R;Sh{5@^6&<-u_kk@RNk^~u(%Sx-o~fh$Cd zMdKZ0-`xMVlExo7XmO(jti}E(f8|1h<&0G}NHJX~ID@y8x1t5x90v;5E&FELXyxLQ zP94zUGCwsn)ssHeX=7qi2T5eas|y$uC{fPojV0$TpOfCIXYO~qdWX~CwC3w|Lh*Ck z6LPK^vVeu7p$@#OXnGr;EVOn8O^o(O;wrl zTH{@g^R}4DKJ;}2dgS@%Z`L_eh3(R2AM4A?yctCD=NfQua0lD63tCEJd^-WYkYx?4 zWObf43cDr>_kAiTud+y2HJ|c_Cb2W%#8wz#{Jw(mok*p2ZSmaqw%dYrx1INq7ta#6 zG}+FtDQIK3Grs-?EP`!$?dPjZb%G@4?82q3#?1yAKjwed^mm#UASa_LT2gJDom$c= zZE5+yxA{|kh2sTlZspS|Uz|88T7i;e0cWLCYhSXdci-o%a#SlPlTDxrM?@QTMAShG z&OL5CCiN<(<)}x~sxA5=-u;U|vb<{~kELFSc>^e)hyjFuHCIU_i{){I^DNOMp|jU{ z^;v1x8bCHS{=15&&?r^59Qb$005I)@#&D*@EL%lTeyr4~wpwfVWJtjRSx;Dv>|7KJ z0#>bKC;{I|(J};_R)>$8D#@L2KDln8;^qXf=*e^1b3g|yZW*FvQl$K~Yy@6<2{B*b z>R0xsnzv}EDHsm1kMTk3DJW<5x|VWUv4aCd8L^z&)_nb6Hwyk@Ay4`;y;Et}`#*TP zFud|>T4-zaHk}e*@o))ym#xhJ%)rVEv3{C&p4l`yYF_N>+R;gQ^c88 z>CB28X%h5-ay==6u1L=;pxM&F4s<%Mh?AB}2m@%g51^@k~tOo@3I+hA63>`t}QFQR9VGd$CkAL49Q#X zSy8@kW8$&h_mh$70gK@DG6E9s$~o2rBX3d3TbQ?=|8oG&KRi`=Ru3;P*R5gZi?lOe zX2c$JfQ{IW4N{{Iv%K0M)_V7hT*JP-stpQt)QcCr_JKXay7z_(z6VlTlhY-fJmc6aVw?PUzjThGMdgE z$jqXwTc(s(}g{rcJJM8 z=gtezJ)S+^F?Dwx2>#dyFLXyB)-Ra`V#%o!G*S{BwGPF?_BR{ca>5!9sw#DdgelbY z_!=!nMGqrtBcJDMR))|>{S)kKJL8`WA93EjQ!m#%I5;*LNgmD)deRp{jPaIfCO+&p z#M=q3d0?z}o=cH%v_}R9f2nHHWFUFvdIiKW~XUwkz zg&t~@UJM9!1%oCeVtlKoQ*}A1r^E22j(_X0V2I~L$Zy-Pg7_@2=1Fd<>tl25%PLT} zrRv|8^6Ps6Q>(DbQ?H7KlQH?h*wn7zV5Jp(cHLI&|EEu^ zlsQA|Wu+BW4K4FBk&6yI^UF+q<6r)Rh{WB{{enR3&R~!&(eJ;>ASE37$UVXC@!hoN zjNqqW)#??tqbmwRCdCcQ{p>zU*tv+WZ#(z$`QJoF|9{(g7UP*pRr=jg4CUN>vm1#D zT-#6gXOJay=4==C=ZmxFRAx+eKx>$D!2QZ9|1)Pq#GNf))&WC)OGlbKAv2;xuq!yN z`#}b{u*&Um)d;BO=4>LdpK0FDb-vsohV*5BHZ|i(Q@v&4>Q2@&RS{>JLhdPPH%WFl zZ=vJq-561|tojQ+U*tWHzrYpwCkI{J(@*0$+uKEiPg~8Aquu_@-=Lyzve1NX@fqqX zgMP$t-f{dtk*&^D+%|LCuIb?5z~m_TFxwrf-~(TQNfi_9u~F!D2Y$#yj_vDuJU=RC z3Pm9%g4n9HL(hVqF&vh`zqLWbhozEfL67^jTkNX=3*rx>-Tog?JN})}EA^4JmzG8r z=6a!?0Vn)~++f+@&^k?H(w6~RQLjv)z;vK7LzB(>6CYo{_zk?p%>k55xU)l}Y(_g>C)o*dy-YL!N#dzKe zeK@w3+|I15u^Mcq#u3catn?g+qa9pFi&-FXOTI|Nni7)ZR z%m!Xd#O#2^UjP~REqUh48`?*1*25u`Q)vvqIHdcLQ1W3DZ?QE{{PD3vE)gqdP5h-Y z?ym(sax$q<AefD_s#xb7u3>9-fy-pa#rpWkO zZ_-0A@r>lE^Ev@kHTgfnjkjm5vkj{5?=P`@rykjwY+0D>6a!ZZ&UCLqrtm`ba+91G z!%)ylnAXqHCx0|jgIjR{r`55GLFx&N=RK5F;63%jKW0hOzKAQnEDjH|GH;K|tu%0Y z_iaoIukC93Vw@lmaC&5Cu-qeLS}$Bx4yOvx7Ek-U!))LQnR8h0%Gs19RQ2$h&k%Bb z!{xW606c+1-woqe$Zd`1-Y}*aLGc35yQ}m-@%j0aJ&OzxFqqnyxEb!5-;Tej5R~? zovHs=J=(`4@E$^&Xb4mokq;kw7?`{`-q|0{15dlG-?yv`etHN>J` zxNi53-XO1Wk;L!BaFB4E)L;nrd^i_HpK-i>r0cfS@y2)_S6(Q`0XDmg#rZuyp~(2MmOy`Gd4n9w-7E+V4rNW;6sG{Tg-~yr#p3kD|ynT77bEsmBzAxBGyY*gpp4;{BbkUb%g` z3F?>W?h1?d732zn2>YLho<+;A42+Ma)Dm^1LCl>cg8lw>b(<@cF%g`#3)i4$ibgvnq?PuP>RTsn~ut zf*$s;C#Y7Qj=xx&(p}+pbxnSsxE!`JdbjY2PLp>((zzuu4_ExR+gqZ14 zU?6FVNA@j!`yUivHV76Czhgc;RY&DzErJDO+_b~YLT^zkrGkg?hp8!@AQIXV0*)+c zY{N%d7V!D$M;$Q|jKDW_&g-aDOUVqC(d7CYyII0A#s8;y{X|h{cCxIN$(QTgw^b3% zRH9q}*{<0+wO+{PUX5v_ivgvo5^0itq?p}b8ya}8y>oyyhDQ-HtIM+&*NJM-W7GdRz|)&t*mk^R~IY@00Lv)_8wb-!2__#y9hXWe~gwXe<;naHu?<+A^P z#tx|a6`YU$67}J}V2IFbw0RFJ`SFdJm?*;UtUGU0HuQy+{_o$vI+xAPRiCxFvLcMa z3NpvVdUK2{ff_W)ug^6($H(wd`m5m*-4>WHTlak*#2|2(mYixaN*&!?>S|o{ELX+& zSy|#?vwQnzpLBy8jZHzX8L;_3A-h?Y`6|7>OUBXGn=VMU=bj!FaP~dVX1=ZvcpKW$ zc6nsq>OCb@qL`~#tqma)-)gc`IUbW}5!fB$HU8UvRjJpw;q>xGfe+hx+@41aR*G4% zg|or0r2Oi^r)Kkdf>d`ZL3g}Q2+60c`&0d~a~EC)n)LHb9TJMuY{@+8jys=0QP@B_ zjAkH?cH-`evh^YYJ~iM172ZF8*q7u|p;5)E={}WxI=@$Nfi>7nb!NK`Af(>7C@L;? zbtbHwwI)$IgE4K+E8LC0sHmvi_u8ep=c3*0{MI$a($|!UGyI_-IpbA*PtU;8Ela%W zb?^W5xc-!+!1YMD(sK4^$sxkM=-o!SHcrc$7iADa@NEA9vEF;2CXO`NXooxRjT%3a z)_}LL(9!RrMEPuze%!2f97uSb9&DdItz%@k-yZwB)jKRnCaDI6Ro1j; z2R|A5tQ2X(|4iAq+=R$HAi5t!UsNptXIQLCAAbTSA`#FuQr)sP;0JqL06(5MTOOxR z0R{KP>|Z!NEE=ZyZ5^bz;$zdUK{4+-?%Ix1<2<~TcMlF(H4V@OPD3Qu`T7emywob^ zkk7}D-u@Ok0nZ7N6K&pBvwmq_I?E}|H?S-}XH@QHo`I4um=q>m7;2&l!QaofVP+MM z+)k~glxdzzUCVPMjv9KlI-NHnyf?Xs`|VY7TP4gL1Tp~8)5Rq@pD3&0lr?n^Ua$QG zl2ccIqgGpUKoKd;+{@RLK*w0b6#j82kG|_a1HNf{7keTs%FU(zTgQVI`Y=*zOB{z1 z;ZVP(?%hWtthn=|3LkQI&+L}Dsk!m)C%B-`L4M@H$;mgi1>Sa^g5M_Ga^p)PfaPUm z2UlQj*e}S?0T~7!RY6B5<@@hUHW;qdY1hpH%K(ZmkyEX&zS6511v%0h-!FH&-2Ed* zN58|Lj^U+Z%_&-vLiJ7ITo;C0F@PF;w6z@sV`*G|_u8sQ@_;|)4G$p9Ldl>wl z7GMlH1p@=Z!l{~=Mut@4&jYJ-Xt8lh=c^#D%i3o+e0Fy3J+8_|8A}<<&sV8vg9)^y zB_$CmL3cC9t8E-cc?oeU=eOG`h^E@WxRO09gTXJhgS^Q4GTeEWl9Kq~{Pz-%hY9`g z(~Bs~c$(V1#ro}?y}jQV8GX6p9kzCM^b~O>D zOdMsG-pUe{FhIs|O<3ZpT*U>lo7SHYuF`eB*%ygNEAjM+j^Bnc-B=>DDpnD|+^4Bc z%_uG!cLh%+OpaJILy5#&a5FI`cA8~m{*Z)*up(%vzQx+QE%#N<5tH#T!&zxg*UF5f zRw#I_c(!bhVi#;Ia*O}gfcS$FIUzM2dupw&a)6=u<0A_AdKo^($;Kouz64)^-)1&- zg3=a@*ZYPR$Y2mu_44B3GUj3%MB?!x*G%Zgwn1_d97vF@0^zxsTiT-QJrPvUOr4YZX;wGKAIvb zDb&15Zw}6A{~Qgu011n~Y@OWH zgwu|4Z+JbMg~ehDNX6A5^{yrPY);?~Q|?%TEO4+RnI zI)ku~m7VMaDuMD`UsjOszChuHU|}K4m!mXx{>;^_^le9IROb|;?1icMmN_z{K@Yat z7eV}}iEn2QJhis<;fIZgpr)f$Gu9s*)u8h|n_qWWZJny70qN=ZB~|h3k8A|Kh%+D| zAhBV~Mdw`Gsc&Sg&MWf*N;Ux#duiCEb9goX?+g6K+0 zsJ?4!YiRmnm{yqR#voAV<>h&JafOl62@29V0SMi{c^YQm8Tq6>(8l3cbP8!zanv+9#@` zWuPI^RzxFRi%OQa*xS8)@9l{SPrj5OhvD`_6?%S5<5QC!sKkK%4J!jtonSjNWC2K{ zn{IU8Bo#}}$xzf%VQINTC9V;W2|Uw{l#P^EQZTvb5zZCyg{ueAM9Uz=rOjI(9UW~~ z8Nc~sb;BG!a-$vE_9L_N*8KWq_kMrTGf_0AFf0?50+ixcV+~Yx+ZmG2&ep>xB>?9+ z_hM@M?$}lGd;CnT3%au>=63`1yh9p{-2HUa3OM%$J;N|No&~&u$!NH_2~c%#4Qy=p zHhW!JlZ3AM1|`8ZXT>q0Zu~}u%KG|r$aqXN7?}!?91*Hr5WL|Mj#>12L0#uh01y8<5d8INJd`}MYM_}Hc!j{ z{QcoYJ1^*%jC%l%t2{1u^NZgMguj3I|l%hWGS)N-{3Jw;9;IsyEIvc^Z zIu=#LkOn=-Ay8eqB#4=J`V74a-E~(9;W1yh4%{0&2}_au1}6#kgOfh8udWak6s%n4 zxfknwM$J0Ax}MIQgU6R=D{P#cwkt&6_>u8g*ce9o8n7!K7rftjyS%ywh93DitUrpT1?5pbQGXbgp?V?gR6#!>?Czg@LdV440_U@taWG~~Vh->KZ@Qu)AUl@~N zXZSeFI=^G|wdmXE&VONnaX9XlnN@?kRasMjJS3}j>&zPN*5b4P@l@NNDJ2t*oxgJ& zGO`)eLg#Ev^eO3z3UbXRCQC()of+V}?q)A!T1vI+#!#lrG{weSBdM*YVaL8)nJS=?2C7W+j^ zw5Un}H^54h_0i$sP6#|HH4ROcYVb4m?*87@M%Q^!OLy8}RFb-Pd87$_3Tzx+ljVsF z0FAS)nD|FxB3jSO`p528zXFr3nt{QAt_^3GCI}#{g9X7{$HAm zx-EWLZDbme+_MS(J7z}FvdM^7U9kF0HDhba{%c;6e+!%>DqHwT$L8r5F^0`^Q|=Sd z&buF0iA7zZ1o17dX3ExHwRs8BN{=-_?<@9PJUrLWC;oCuN>(@flQ3bp*lDIl0o$7I zPxc=Yl9E)2h=@$Opm5D9Ei45Dm&ZS9U(w0RDk~!jUUv^HEUj-gx}5G12vfz#!yTjW zNeQ=?H-%(XRnd!lotaJT z&UQa8emc8h{kJ|F2j(+yKjS7A^~w!-4^o}u*90@dK5V>@koxYb>FMdY7-lRtSvR@9 z(K4C3p6zjwbT17KkMa&h}7_3aQMu2gs?bs(o zibJw>Xz%L}OP4B9BP5)g3h5hNNbe88|`!*rcEJfbZPP+QtFCXBO zlJ++;)pj1+UEd(H3TRWF+YyAxAcd&2$edPvBWwnn>25rW5k>ZZ+jUwKw2&zJT#Lyk zs=wY7nCL<>F(M2UH(WurwA{s`ps5=f4PWn#9~~X-{oNi^iY!4vO}y;B*c>^X;c)7C zvP(_FieJ*M;zbL?z>1WmeR^Si!=4tpVJa!1lA^=}vKseib5~{N=I-`|NH({${99X_ zXzRHnD=jO#O7vb+va{om8?CB4+un{MD=#lcqx+@XyQy$*?p2>E>W|WnrShT-JB@pDY3l z76A!~G{Yhh$s;t(|6parqQ6(fka)?zC4|OP4F0UAjX)d;djr!{-2`1^K1!bcB7RM32NG8v~h*yhi#}qm zudnNfkA}h`>vZ`EORNx!sLMMVqLPs0t$p;d%F8Psq!m2lGmOSS(9=NVn3!R;w0O=f za#KcZ?dLdC((%U4R;a4`J|fi4N z(%IF;=X=eS2u{ufOnm-Vv(3K(7q}~~@#kW90&Aa`Ou7_uUd9MZMO;h;1xiBC!}Ig~ zi7Y}V(7B!i!%qRP$hyEgqDxijzmV&-MQ4|_Uac^${nC{opy?2kI zx*L|ZQEOg@`$0-7aQyHOe&8E)GnY487YJVw2A`O8d#yvJ7&F6!EoF2RVREwO7Ck97 z1sDqP{0wK~AcVeTVQt;yapV>X)E$R~qA1N1bJyTZ5?+wz6X*xl=w}IfLz0l35%U|2 zihYoGcXxvx2NWIF+JU{DPXeGpD4SBy(ykW7h-&UOg_)fa}!qiVyh*k#Ky z4^K}uO-*T8S$NrBiJy%pqZ#nMwr2&uueHVd`p;legZ*NaR54nXuCBv+XP$18)xYKC z$s)-p8(Z7*va<9M(id)IT$Zgf-pwCS0{$<)m;F&bkK3~NBqZ=K1O&F#`n{22e|HWW zz^Ch7NwXDpF*4;+M=QqYPoMZO=_4+h08ha*dPN<1c!b33%(2jasm!rGMV>v3J1#yX zBAJkmkd}dcG!qjNTaC|UW_x*Yik%>@)cTcvZ|?*FG2*0MX8^owFZQ$Yc9XX}|M~KY zZb$hm#64Vu2>ZWJX8&J)1^_bn7o#ELvGm2Ls0Y^q-BD4AYX$nR_r~SqRUnwn{m}BC zCf#%G`QZB&9QFBJUtwMzMutf#S#Fp7w-C@pE*0(UPh3c%s%kqT(Sx>Fa9NF_X_#eX zho7!ggQp~W{WA0Oo$`Hl7;u^O2J*Z&#GSNaGp;SLe4?VF7?_xLW(%IVw2fR5EN+JIKwT>?VTW{W+ z&hNxb41eTQ>2})Njk4M3J6Bj-4%Q!Brv06*W#loT`boUk`ZwR+L|rcbQWS@!<^HTK zZ)?k#%i;{WVAn*ln$8~G`5njSvPEuQ0wxyriZ5T#0x~g;-2JhhJTv6t=Ely#hvy>V zaVV^AKJ9Id{KBp+mr-cxzL_21RHoET!t z%FH#6B;?Rcx}5+5&WWpdTOIQeQ@>2dHE!;ybN{e!Qtl~ylp zhLb}>FqO8ppbO{+mj~es3L5?h7oXH|)~;~Y;3q~<=ZUULE}yvN9%ie<^1$OG{=?>n zVt(+sH0?00PHO-x*~??pQiC~Th}e(OQCHI7Q~92Ug3XGKj;w;$+aRmyta0IowHW4v zdaBmEu&}{u-LBNGiy+=AZB{yVfS7_C*7|zjcTEyY2Z!9!-R~Rji)R6sVGqfGTq!={ z?M6lK>-pg4udKb7#B{%^mfJoca(EnR8Q2fYFb^5p#*Rit{!_xBGE5>HI1HTfrBeg7 zKTAKyk2u4cc@za!9I;Xcb?F7xgf zBkQ&~*$W194au7SMX z_dEM)*Gl8ZiVO@ zgR=7En)ha4wQf^vS=<3sp@C(5$zLTUCAT-v#hX#ppz!KyuF!s|Q1Q6uXSlD7jN&=~ zz{KwA<~!QBHWRH0eEa!x)l#D;<0m#FIK2Sxj;0X{_(VId@~Dz%e}MnPF#;YR$HTPb z#*?!$n6e}{FJ+8YyP`sh#Gm1#g1!&4eUs}AxHO#*EITNx0op&6te6^Z}g9!`_3pfycPovBC3oR;A3%cwkYqS`}s_i;g!1}xhyWtFj z93LOwKot?d|XY~BxUNn%GcNIx- z0i4wK#!Lrq$8mAD&8*H!&Zm6kaWs%MGb@(Q{8nzuT6)Av&vk>xV(}gd+uZ8KA%baj zvGL}x*%JZ^Ck~%$Cp@0YtElM?lcSDZ_*5y9^YScJQu28aGU3vOqI5+SOgC2Z>3xoO z{T>qXkZV}<>^k%1BoLxyifw8dpef*<;a!U{MLwRU-PT*QzkN%VDeorq`#I1K&hGGp+*b(^m2qDRUApf|mdVWw@QDExh1G6Xdv|Az4zq%QO zEKMUVRgAW77#Q$$(L1ow?SFK7zE9SB&kC5%86QqKLD0MGV+|TTYO;2vLd!4+`cm+6 zYMseDjsF`hA^x(2>;CCB<#Z%5KNzcVTF{z4$K=N zl||b!Lxx}#hKnh4x&h>7^A-q;`tKKr*>M}@$N?j!$vzAQ`~QF+boVu=XK&LQA@nt3 zU2=$3+I7@|oO#(ABmAz#*>CXooc>d*S3jxb5D%u?+_w5CXh&o+dwtkf+nh_TB+`7* z6o-9T6b9p1dnURcg5vV?MI?$~YCF!qio8{-s$~{LErbFW(1Nbh3a*<9-T?jm6r?^| zcsO`?H$4vGQ=bX>@ag8esp(G~UP3hcKY zudqR9bxvFNt4`Jji7Y)2-DK{dUxdum<3QH!i}!PPDJpDgYU)&giMgGX`MW4PyNZGW z+C|p^i&O6zNog4^wx@y7#vS1%=s|F9k=NxtfC?U+G#>d!%ZR;%J(I1e|9<7(mpzBo z&WM#xzm*k%pT7h|XlC%B6#F8|Y0-#3a!ti3x~H*rydM^r z2{E)@8Qk1C8JP3r0}Y`|21{;lZz2Br6otK9e86^jU;OsOakrY@9wd9WuLT`rrKVzK z#fL9q3voo>4?ChCakGfO~aK4J<^!Mvp4*Fn%3Fh@>7 zftEPtaz~N%hV|ps`PSoZc4_HPRb3A*9)u#rUqXXLX*mMbIw!}A-FJ90vxoe3zC_8< zcr20EZFILg?u;}*rW_Zc>BqZdz)jBP&RivDE?-b;!Rv0&*!Xyspf@fzx9971Q51q| zqK1$=582zHZ2KNC2O$E>r5F=UlXN?0^OZ+7N$cO*+Q56@i_gm>Z(+zUs*$I2?~k;i zB2z5vRNXEs)Pk2+Yx*2vXa<@|=9<@Qyrj!nvdttCB;_QZ_k#E4|H_okv_6)h(P??G z5_&jO6BTzJf|G`rMr-RbY+N}rx;%QJmWU48CEO^CliTKLAW&Ugk7>OmW~{<>1E!7* zQ<7Z-5$6t@Rg9i4!V@jP_4)Aw#(9Qugbexle=6w4D~3c#%N6q%tlIV+r2Df~WzT6u zqvgA=8K-N*Grw=*3o%AVIqz~sGzNE<_~L-froUq&<`xU?I*W$IMHiU-uYTkG6!FCh zDKtA5I`23THV9fS3zzT$jXm$uiKkeArliQP~t@2O3P_AbiRwJ z1sT0wrD21YW}pxgPwG7nXx0KBNE|lmd4l4trkJ)n2N(Y>ElFv6<1sTk-aepzyN8GA z^O^3pos}{2v&E}W$v546hvwooHW?jo11!K`5R0lQ%gVy*@O;^8(gpcYAjX7<7A^jq zw2%*3VBw5weAUDV-vb%T=`WM z<*mTPDHR|qcWoBkXO{~Oc@2ds1$cORegcy6eXB96EU?%8scv?Tz8C{8i{4N?0q5j~ zEvE?f>`-^FM*1p`9+(Eg{@?gUK>_ zQFz$Wa_V}ov#;lG3%#?;Px4F@^m&>JY&PzP$*zVZWMp)#y~$nYonhWRH@K-1;s$+C z=f0c5T)b`9vjuN}o_rQGQh}i_+D0mD&VtV$f|tgL-Rkx~9v`DcZLGX3*a zAw+9p3e|PIj8lh?F+*cM8(2#bDI+V7po_Ch(@E9gU$3oWt$G8;=MxUmHl41J@_F!bU_ z7rs@L85#chO8{xj^0-;FaS;!^QY_@`IWBH!I8`Tr;^9!!z=*YIzJG(|9R2}D}#CDq}QnwrgAZL^z#azm(TS^3h- z)dbZ#J<*Lb0W-7ESzSVld1)xyk#wLzr31bH^bGZDAI7Rq9tdq!& znGu7yt%kLttsj_o!vi{s5&hrn{HS@7@YGnk;-Bg1-!>3l2-|DG|sA z7zjR4re`~y<>hVELeGa(&24QZ0JHgH%}-y-bpDqnOgEO-LbX}X<D=w-dpXYq$= z8wI9Ek@^9yuGsCS7C6}1F-7zB7`c$ro-GjUVkmOPS-QfVrRmA|s>z5o*=yOujySfeCAsKf)cLzECEMwSfpxM>1V4Bu+D6Y}UmanjGdL zPnp5EG~6S^rWu#vR|B|#?b&UYb@Byt>?Z{`d>OraUcC0FSrh8$4gk}ZE-uk^ud|Cb zPEJL&t@#T;G+fp!vQ4hl)=7RfLJAWXi`-8KHUHd+iJ@aVGUqeFrqk$C>s@N1a-`IA z`(V5)8T`gRl`pKQsKNMhPhxSk18ZSje$|i^7{BV&O9CitZ;zgt;lS#qolv^|ysc@e z<>ZTeL|IW+S59Q4%!2uvgeP&D2oH^(+j#g>$#5Dxo(Mj)8Qy)=v z56{kE3mdM8d*9BSW27i8tgUbE+cpQ*y*E|t?92iLk&uxIh~h5ydOwh$p~G_dj3!U& zF04lG^Q!f_W1->iXBQU6vYMg^+`SRY{yu!lW-?a-mY_rsIHK+2DfPUPutXT4{Sl$TYw?<~a3H;s=yz8<_sGS)7UiW=^@TcxGu%D5{Ec6oI_%RWoJ!?*p1m^@xc z8rF^?D4V|Sie0ZQ7VvElaHlH71uwsn@#sy`N8$K(%5TH zY|kk3hnHIC)B)NsQe5?YnNd`CAF!(eA0O+kGRRV1;M=Wxk$b-Pa9T7s- zTNV*#Fz4{Hbyee->Voc{Pv++HIZ2~7Ou)Z0jLI9XglsaZYI{d*xgR1`LT5lq%ICFtpB1HeLiZ7en`0B*c- zG7umkOI2;uk{Wx>ZxatezOTpUd~V?8<|Zzur!R<4O#E6Br456ht2bSq-Tv=_lqLY zYHQ3J4<~2ZTr*BMGXVj?zW>J)^!dw?BvBz4QsE)I|DWWw3ldkZn}|*xF(=5V6+U2Y~J_hjCBI zSYC$a-nw6IAMV^Qw>j||&5IhmTc zKt;$`YS!BS(NiKA9)f96SBQY)aR+8WahU?CAs{?w^&|Sh@q$_MQQ5i zmr&9bFxdL>Zgtb#x$&ZfiT$kdg6AK}DDyU!tVD z`{%bRe9z0)J?>E`LT*c8c$^KU%LH4S9B<&BhfZ$ zNr;I1`e$-#2&=sD`xy0-4{IoZ2kNL!AYcenKTJ-sIQ=B+yRFyxn!Jh=4-3n*&VWg|LNlG1A1s^j&o<$8pr_yDIrY8*A|(L=iKFb*LGl=4QS~)N+X!1%^vl` z(&+2Lbb+8Z?rNJO1^+Te&%?#$^wJVNmWv@gg;K8|Cu827y>@acK39xsCilPUtPY&r z{Zv45v>3$*~3Ujy8G z*zFsJ!XbFqT!cBT0EM~q>Izi53-1-B|EGv4Jw*@+fDVF85k16q^spAk_hZ+|C@@yy zIULUJe9e2n?3U%38>{(Kifr7e5KKpL!JF6ZHV~BYY{Y4Qt(LRX88aG#2H|pTGJlzm z{1ghnKPvn7fS#bP=^4MUZ^hx>sM}!1&}4;c<~zOpbd==sfM^7Z=U__R_5u_d) z4~L4jtoCb)o_&+>3jK~Fzvq=ID>tkOqP&(CE@K93896V>-lRKMa!%T4E`O6azfK6m zpyZ&3mzOU(&wz2Mm_7Vr0u*hx&-TDmwN4LI8JDs$GqJE=?vj3MQ&rAvg(`N8l$^Y( z`F3%{1Pk-i8+eqrTAV9iyHA=7EZK|9m$8#z8T1H*@8>Wcn1dlUe|L}DgxzHMK_GB_e<%E;=#U38qbybCq^K2%hud~=;I!qSXP zyDmlP^pzVm+YsLGkT4Mo1KYt!ZK5UTnn3{-L+Xb;g+-tHPTvdIk_w?PBI3QyXF(+3 z6%r(m{lh%-{0+MbV!pZH|7Z0=i*VGDzR!sx$*CeYIKF71X>0vkSQ~nOds(hGMU|HJ zy+uu^5B?DE?Ccty7&l$-lQP7+G3^F*nDyVePJ}ZjP_6_LwbASo~f- z{Of0?q?Q-l0c&nWS5w^86}vNd;$qz zigVZ5v2ro&!r5-aI7YvKZpuvt_WcvOG^OQb#oaQXBYI>G4i0&F6*$y?axi-;b@0J$ zv~yxXUbN%dOxDx+Zz#9-?gNUiHJQS3`TdxmQ9B|MX`R%0d#w>{mkO-jtsOm&=M#p% zwX!Io;Xig4ynDhpE9@SBvj+DsEwPC{zN1&t7mTCw8kn!KMYvXK=8s!_{N;m&4r^C1 zUFN9+T_YtVB*@5pEnDpKM1+Bwo1Zs@4Qp46+Sq`MjFP1vn>O75A#rdv9bOz2cXVVG z{d4W?e7142gRaqOv|wP=VyY-A3TfS!naZ;_@o`8^OjPmndxq!-`QFeoavz!K*2E@7JfBPbJv@;oRE6A#^Yb zeRgeTV*31pfWKq7%zd_;x!SNPd#xJq$PIod{ZB!SkV#jEV?b#m1FTI7aKf7!Vpj5}J#M?3I$tma=rjQ|!65<9BP%Tf<%Y^UL^`aTfJYJKUJ{FIe6E^iXhcDr4 zQ7`smVh~sKRx#C6fxxk1={O~%C(;v zQoJ;{>}8G1t#Ug5*49+^QobgVRuxws2}uqP($i!i$R66*%Q7TR9c&o$!Ymcj}x1NnZ=<+QMPwhaNnwlg%Y{r8OX%UWTHP&^V8%8$PDW7M}K>J z6S`jl6Z82bgFptc*KterrX+%1PS*XQNNGY$CT3=z{uTn91Ye?}=Db>M&oM2NB9V#>NVbi9TSOAxFT|D1i_`2QZyA*?K0p)a{R6Qc_~I zibhP?R`_Ee)PSd2f4uceI zGTh__IB8*F@fxC8tzlsjR8_-3WcvQsB02@a7Bm438o4&4@i`4P@XjK0#KBF@&b=D~)JtJBr|JNEc29 zdHNrAx&d-Ql_kAYQjQQSbZxPJgr3>idA|ixbE0I=u(ftA3*j#C6Te?J# ze*)c!1|kH!qD$D9_mYaJwe0_`gJgQ8iTxKWXCN5`j9_R1YV#lwOAo_7#f>68M4%D@(q+GzhYramR6KeMOaSQ5YzS4)w;Q+V{1>y zmi&7JnpGk`LZMv2JtI5C{UIPx$g}leBM!JB0VwZI(57s@)HO zO};0%Xk`2f_Vz^SQC00X_ia=^fcsWxWFjaEY8>WpC%n}KE3Kk>@czAX1Dx0xcm|nb~ss z#6L4Lvvd$!Hv{-ZjmsSF%+b-q!{hJ9KSg?+!k%~Hl`L{m(E|Ir?#O@tOn)V5&unbS z>synB{3tED5+eUIK5hOKyN8#xs0Wm?jf%hJvYJkubmdY&OvX@~x~l+Vdq+QAy^CZiZvv#>9+H zj|Wpgk}Z5y=UeBFEX_mW;&oh`=~aI_7^TSyp(Hkr}Hce zrh(t1GpZ+q{w?h-TweMZLss>U?KB>%5hrH>==M9(Lt!RjTbltxeP^r15)3fcsG?V- zfR!`%-yIw&h~RC2cuW}VkNf@A_3?u6zuDWt5o2PrQu-zgF~Rm)Me1K2_bVqp`;`Ya z_D=g9oA1CJ=Z^fHVxjvp(Y}sIH~_fO`~?k{B@-{WR;R|Fc;J)ZaqN)&E1ekH;uf9m|+B2ET1c6Ty^#L-&>MJqW?% zWT8gr<6@gJ7ScuV=6bHM{@}TA-OSqBglf>W#@Da77$|V!{67X-9mDH0Gx6z!2w-U%pm=I( zYPbqS4?I`Rkt~uNoL5N)rz|Zj09P^c5I`0~30GIdH(_Bep8Sfgu8iz#J-yWxh|y|Y z&ZQ1_I#r=37>JgZ77s74ze_&-_J>G7R$~=gTiaI`yPCfZ)rm(M+(JclTnTq_)OJIw zHn+D{Z$xNDOt4b?iFDySZFF>W*F6vG%H;UFbfWJBXEl!T%1SdcYoR0v_0F3?`2jof zV`Els)4~rfliV5RtEmlF?6kvl4bX_Ucj$jt-TOd4V`+#~R`D-Fb2IU+0|LzK$UrA7 z_(&KiD;nV}kc9}5OdB`(f)M?0=&zysK&@;RgTkW${^~+yOG_G6(K{xZ-_oFmZg022 zyTxRTW^gA0MR}e65Sb9qe}(B9Seky+AB?D&CKDn3;#B7?5V8_WjL=X<422;8j9uE1 zKhWeYeKS}!DB@Zxkvb(FOuC4~Txe)m2?;~PvFMY5pa@L7b-$Kd59|z|fM$D^F+L?_ zFdrIKiKGp6ze)1xX?6Ri-)j&d7FOu)zEz293Ssd!TS-aD);cfg3WoWh`2t*FvIK_^ z*BG|yYb4q0maK=@0|y^p-5GfS1)xaK~m(-{T`+i}lnu zeJTXl)TJhCGSS`-NFb~6n6=1_E$#&4peG3&e3VNu3*ls6A9)?<70Y!IfsS2z6{vN^%@$|5aw8_eXOSbkxf~R7oy3%)H5|zyW7tpRC$*(&hz& zpr<_?JUj!?xmZz4++Q{Uq42+iiU`eM_2dFoGcy;xUT?cM4Rv)3s6(dF-A5Q~Y)pgl za7sOX3u~*)2Y8>+Wu!9dR#-$t#QY!HU_KTs$L2vO2}q(d0lq|esKl1Smc?2xLr(Vw zrZ_o12{*S!x*zN=h6#>DA@RwP)no{fLTcU1&NYTN;ZRK^^bQm$xCWkg^PuZvk>Oz( z-@AG3j)QY=4M5IhPvFbp$;qHA@n@(o_gZ%e*yq|G%Zyw>w$k1)i?eK`$FuDc&H%kq zxvaFNBPPa2%u~fg(U(I)(BGD@z|zX>nm!(%#VMzP=38g}E=_6bAowFWm4Gu=PD#nc zjY-MhA08V!jhfan^Sdd7(pdc$#UG@DuEfnewz>=q^0e3!oWn8keRTsp4}k zz!&?xhvA255YQ24ibwsOo$JGgc7vbHL<;+1k@6#9_tV~Oez7-efy~B=f}rIwLNGQu%q&$yZifCF?{h!Nnt4D z5#$UEA=TAN)46tE119D}cXy?(Z`@OW5kxUqo)Z@eRyYlQlM4Axj^%B28SbbDe?~`( zI-ha=z+7LuFV(w}c;2O#i^z~h240!1J7=60=ykm=#iz}X!fHuFKiKhP>!iXnC zNV9{?r^0M2%_uEn1;=SMaoIX6sJXGK24QO}Z*gETyZrArj_=riJ?_U|2J1+rbl|isJ}oS@5t>rU(j~MUEr8qnKG$_>zd8gV z4NT&X=+JFbIn{`*W}}}GVm?-!?}S4m1#!F`5I)aDJ7z@WXT92=6OfhG?Q@yphPyw?c4x5>T6PCz#d_~@4m0y)Z7oZrJg{+Cadfry_)W3 zgR-53Src-xzC_^)tAGHMkr*r_SmCR>TP$)D2M?S@&q=RBa7<01uvzS5-Wi?*2In_V z!$qNjXT$Uot*<@CdiG{dgBmK=`D{AbFYQL0#4NZwV~_Q+91=xRMdl?RrXugimk4fe<`wAxS;DTZ)nF30LHobzsbljY*qO%t7L_hnZ|*W z2X7a_tn0#gb91@-^V-G-pQAzk{_q1xg_=XTj9frVOoZ(6TCmh|rzay3PcY`UX-5Aw`cV9Y-w(3fkL}Em@aECwOH9sEB6&;{^RRs=YC3l_i)E4#G#lDYY!Hlso;0+qLd(5$pGW~npWGcJdxyv{4&on4%-g+8p~~fRO^u9%gM0QP zIK0?f>kuF$B8rir0)ZVDvFoXUG_HnGjmu5T4dzjcN()!^ItcRA95FPoAwQJ$^uFHg zPu{gMJHZos{{cPvakixcftN+-y-A9!UGdoS6?_JsndT!&--2rsH_n~>{GThUs`g%= zWSZ4MH)p)vrX4%1TK}1oRgT%zAXQ=&A0l$Tb^0bI)mM+qoq(OoKPzFp$>#LE*pO&X zkA9WXOuB(7eFBr#pWK1C#;SWpkZ#!v5tLpHzDyPk*==wxhJ{ zw~@wX!@QUCs_ zHEr|{G?uo(QB1WUE&+8VeZny%b?P%SGq)n}j6d;06&H64Ia3Y@Bvjje%;DD9B(FRU zVe9Jh0eOGH>Gaw!c8DtY)b+Bhi*6V|pZV)8oa-bfCPsc`bya8};&#D-Jv1R+)Xa_J zl;$cslpjdQgh3q+RjuEfUH2s*LU7X`9v+Qk|MAK^m(Mly9*DNGx?*#|cK5ioMQ;4@ z+-tUq8xw;_Zvf?$oLlW6a#_UcXV0G2S7mm>aE9e+;ILX;+RhjB%DgIJZ@RYC61&M# z#;&cdfQbUD(_;7KX=UkwmorU&)A#y;E_ZTkce?*Hk|L@1-S#kN4j9 zh->~bzQiyRAM7OZ|8C=?UY*x%l%D8l>k*WDw-KYig5s5+QM)e4Dq2hkCHJ+df%%|g(oSFYSS{@d- zM}UJ!po+GjfQ|e#v(Yt^cMUGVgS*=s+;{IUIA8WSd+k+Kv$)p>&@eHvqca7SbiaS@Htyoiz&E6> zy_CVnLPKb$p$hcW8XbjTG!&=GKQ|!L)g9n+#dEw$nX{U#mZ!#M_d`=L(Me6PC z6-Z)a23A>3|Lixnv`Ws(QklYdpF5`(y)k`%8>niR%%{$HYR^ERf93OO9`nV8G9RJZxaJVKaEtioj~yRhx1t$8OKdLlK2 zzqnpywzqtd-f$j=!Ra!rxj(6H;G67SyjxMGEu%%5v}j;9Tqd@j6$L>KiWMN$j3}x$ zAExGWT$XtOYvzdgXN4kS;-G~K_}vrXniaBWS4Tng8nNofXd34K3Jpk%(o8J|Cn6hR zaR>??I9s-nETiIL3Gpdsnwy)$LwJKFg2KWwk$|v#*6~LIh7{rhI@o!ej0(1gw`Jal z&g-c<8%JJi_C16=Jz4ZEcLYQlsu$JudAhZ>j%(B+-51vnNippV(Zw*?cZD+5#?$&* z5Z!KO_RjY?@XsHH;faaA<~KI<4D`bz6DAL}bUXVc+56KqUzf$>VjqbeMbc)yt=AVi zIwXBsTDoh8M+z_41b%}$F%1s3MCp&2+t+%YJTg>ms9-y^gXebhvwFGDjH z%HEF)YEugfY5)NF?k+}T0vUZHWd@xxVPYSIml2mGL z?$F}VbQO1pHdk!D6(XWp+5>ba-XAI=HJ;1$jRe;!g=w5Vv%1>?>moWnCde9C(K1Bj zq%^#uOV`%*AW{K91WjCA93|_D{sN92a@Dc$kd@dKzkbt{49{UQIBmRP!BARK61%L- zrl_REWp9l5Q@}lN7S!Gal{Pmw55ZE2=taZCRBOkqZ=C04b5>DNnORwhO(bD0$=?;H zB3l0MxIyy;R&JnY^=*nOnHyHW##NMV2}gO^nJuawp8E<`})eHcV8-UMbY`~S!Z(Ze-;y5s`-l48J_kWM%ZWxW0(vh zXiA2WPxRq->C(%xq~%1W@(CD$8rLMfQoH-oB33~O$xE#my$mIuj!sI_M@|S?F?!%& zHJPWefs2PrM#JUPGBbslg_fQ?gS0e_HF`7?6D=LMr0yCgJbZy4q;B+8rS0gY;$u_O z07PAPcF2%2l<)I;SN3oJdWOtzBUsDoLFtRsQFAl4UcYTdX)MTY+6(cNe`>6|D%uo$ zg-`AHrK_T>WIQ|IPA1?KbUTlsnkNFd0bkAKhz=URVIfDCy?ZHt_N;2F&G86_=TSGS z2pdIdf3Jyi#P{-dkCWa*=hyECbC=$4-nAYaQS7|tzlAu_n!@95D!P(=EGnW;Of-hW z7{O=J=E(Q#d@|VCvm7*#Y~YLcI(f0f$$+D!CrRqgsl`D-MeV|HftG-98Egd>ruxn$ zC^8J7DYY8gwM{PlE|%k2iUtOv4Go09%7xX`fSAo6WmAEv$wbM9W=5*K*=DHUAlp9+ zo>$2vl!KO2@9{QUrazg^6)p#{P{!L=`y2{qNs3P$*yLWlaVbQ z;D9lj!M?d{%Wh@tlvsjl-*(=kEb_Ek=T_gSuBX}C;s4aMb(w2!VUd!R)$@0Xv0pa4 z?IfL4yVavu-Y-O3s?G(l3{LHFy;X@bsBvbPJNVOdiSxTSQ^{~>bd;W*J-c|6(B-Bf zgNtKoGQ5P|(C^?|p7*17i;!?xm96pJssHgJH0}A>?g~Zs*&+Un@Z~=-$BYD<76qBH zdKs0`d|-)IG`}nW+6>sa00UokhHN}N{+&AGh%OC+^lOCSx4RV^ zu~(b|vz>SwTD!CIbb_8*C;eTBf{0YGe7YJ^1mnVsJO-s&(pup?}&J4zHRIM%*`*a~SDyNv8l4Qko!}u(Fq8Y_uHLNrzD33c zE8FNMV;xx5xbyjM7%Q3i>AIO8D~K~Oe#7K5NEamQ>}s?HWi1aTI=xgdA>?VhQoOK zv+SSE&qB*j)(!_hD{Io+N#|=_{z!!wqEo|A*;QGwKD!U;5r3-j^ARj>knlow3Jw}~ z=hvSskpAZ8;eI*h7@+)BC#Q2tNnoqmVpMK#G9N(c)TF&~-6x5biQBZ%^&vrC_v?@Pjz3fD+%6d9O`&7h>6_)8G|#w;yuXX5 zW7tnYrw)qh^fq&Z;hwCnE{Z?;oSH=2nup58kh)oIsxL}^seza_oqg2RUR;5~eKeVxj*<)2fum&>A7{UeQ@pFQIX zN7NMTNzcKo6)`y}4V@r1`GygeLOlob&}``>hlof>I3*MB_|c%Rt(yIom?oAc+qM@M`rj1&OJ?X69LbHcQM``d- zOjr15Xi`M(RviUAD`AQ<`r=AV9R!DMh6PSAc-?Q%5mO-&kmTLgoupC)|0Ga`HxSz6BFRYxbBJJwMGya2&L1sBp%1aUEe+ zy=Dm(W;PtnpP%<)x)wBprPJiqsfUhQ?_jEpr$wN4E9QA>gwvlb^eBUQdGk=OZ!tLdxMHeVl9Q7al$G;u-EipOm6W4OPnFbGQMKZJS(GrV zD6+RVL<8kfMsMN!OUAyAAfd<{BhaR+uMhHZv%$(`&gn#d{A`-Xt`+kcU5Tc{Uxc*ac8*0^y!zJ8;tpTtM~kvy;M*Lg;T zqEswRv-7E@S;I4%g$xh;*cMiEEL32d z{d6)mH;=o$#h8luq7gMWXa2#3YZk1h4igOD(&8iN^p_)8BI^C7dcJ@BCrD?0 zRu&eV1RxC^@3_Y-gzERq+Wx4~D^4r{8zr6+B^DBO*iMps{$C*G8+E|A4Mb5*t+1^v zF2iW%`tD9$)0Z#6+ZtCiUXJ77dyDU5w5lp)J2)L5nD09%XZv=w@}E)Zd1*?bFZ{|r zD*}py9Lkp6^8vOX zC;YzzwYfbWkvW<-AizXzm$JBnt-@!gEicbw7^RxJT3B){C3L;LgMn!L}K$imHrhmPQJfi6VROA6K61 zxtfy0SUtL6jy@EKfxhSO<`c$+A_QNyD4b{yGkAh4>J_NNUNl2qQG0WEul#SX*M|&i z8;8MonWZLYI8?$h{wmrM-n@2u!X8*Er}ph)eHt$O8M$)nHte%u_h}@JWSpj_c2DO$ zb9A-0J#Qc1r!h7Kn3>}$t(epzcqL1`ht!xV86Pr=0i008w3?i`Nmelf~QL?o33#l_%@3(ndZ&8>QQjzK#ww~)MbLwfoF zfQAI64C7y9sZ>~#L0N1E7A62v{Zw3B90!+(s=#q#dY+_~2MQBY|5%pJ*iSlComLMT z>+Gk%0CC((X;)Wn^4#2=eXGKNeAhCg+o$u*-ob1%&BJvjmv&I#x1g`o18aSMrUbXD zpZ;O_VqkI$?Ie;Uj#V+5FNN6URP#X@pyz!_BoO%EAWOHeGYbs=D2R${%0joT5Rnkz zn{{N(OC+=c_-yAPKMr>|3_`QkvrOoFFpyWS$}&yNY^q>RcIgy5VP57VKuhS};y(0z zN~D`nN8gi^lcO#KN`&`Hi;L_0mNbM~TmPM%?RULOulnDu(T1^x+btZ?T zNNJstk}d7wQBrnWETIs*-6Hc7+sDWELlsX7hk(FP((gh_N@0e`@~VyyR#M=Atq8h` zs;bkez9`fl+(%zr+S0FEn6}+qKd8W&_t0#C;y>tIb0Q%^$0EoXeR(O^RO!6rXl?zs zIK#tvJS`&=55+_Qr)J^b$-(x;VBm>;BQ+CK(o&rj++P8VmlrshLhP`_qT=F|{0zka zL;9=Fd(|%eD4#zoy)T}3hH>NK%V((h<<${lF7D@sWH$-_3Ogr z*65SX(eZ#g`K3$UQoXY%BOw)UI)5yo6i&PVyp&}9d_A;rTR>ekzpS{dICgl*1O*i( zlb1U`DA8^MnkyX%nq8ey)wE@xsXFe)ulCu6#es`}QsNnsA5@91kDTj{4$uxNQzRSe zsvnOuZN)G@?D2O1^lX_s{V(W9*z5bA4&K|71O>rOBSem$ixcxCOXf+6d5FPfV|ki# z+YfSNgPTkHdHc&pl?;M(OK|`^1=0ZqeGD>zzNsk=q<0{fqKb-~NgmL4mu`Dma|L8E zuL$bFP>@#0;FK)qJ0ilq%HpB5t~WL}4^jBquee$%{}(|T1Fo7ydN|{9%QKoZyB{g# z8SiMcwNgrH)E8akqUu4736aXavh$S0(CfnD@eyIM;fR;3uJ+*Yh=C;9>4X=^RaZpB ztlE|^ctGssxRA+dYib(w{463B{J{j`bx};{JqY1X0q4f2?zzfQIydy-Of9WUUUm9+ z7yXm#WzprxNKM77J3$Gnc1q(SfgRi|&hV@;mi)N1qzQ{OigUbVB&`MPa0lZF%nHKqBYbkf5;TWtun;2nR`b!Sjs^~{ zw1V=?%4~0NXrxZHlNd6x;@-W2^p4MFIHAqizph$Z)w!EJlg_g>Sj}7Q|0vD9gGSUm z>L-5@@IKQbD`!+=6uQBtr~Vitld+{S#FMb(0Dp5O&`iw(IBSU=beos>j;ay9h0t$B zAmhqL+9}klLo>6+RrrPOE(pRwgbOu3Ynh0EwO-hCcB6?$^ds-x{tWNUVuP=r1SR$K z5Su@SR~R(LB=_i_{pLDwCKM!r@%Da5O&i~902kLl!@hx^R&^C+X2-vg`efy>dTt{*MYw_7w3er{He-qX^C zh4G_CBPUs`_Ay5AAF?#eGcNEB$c=R0#Mw$@-DH<%ep>$iOFO0_?rS=`p2~C_aebrv z%+yrhXw~U%S3xf&Ew|N}@!B%4CF)!1g_IqDZoGxr^1D#G)O-)wN(%4({o_JscZdH| zja_ySxp{Y6GmD{llMx-=!#nC_WkF5Zmig%pSnF`(;%L7G0P*CMcS?NwhK~eraNrvy zV2{ZqSF##6y$GSp`CVDk^2mdWoATqQxrJr5`GqOZ7yYk&zww!uo|emVgxm>$HeYF# zBz4|9a&lVi+5{|%<9@BG%YDHUo}f<>kZa-s{`RS74`FBG41JWQEz>31;WRP)v{nc_J=c6tzvYN}QAFlfi;jdqevbMq7Ar{i| zzKB)rP*Q9pud`_)b|;_tYts3W{~oP;BtjxrS_jIsUta{43#Z>eHDM77VYRE2j;%j4 z>(G~E27cPF8~`9E>1wEdE*^sA*fi@x~6~PyRfYMf|DTLZXe1E%P7oxYI%O!8CZmfJPR%M9 zt<1KhI2>mh5fLHaaV+ri;=i-AU+b~<-Nm)8^wCI)Bty)mp#IXFq1t{_OGznaYPq=O z5nlY|VA96sh?bJdJe2isl6>CZhyB3Op&_K@=xUQ*1k3TP1JNp*Ta?kutSzB{R=Vz( zRu&Ta_(UA0=3l{inVC`cde$c{$J+S0xuSV`#bu%TgH6SxFjW}{WO`8m-%oYgT#7&q z)p!RM<{ws7@L=~G*7S4J>)}~IJ6FV5eG9&Fg7<0WhCT%oYrl)Cy^XZ=~ig3hD@4(Fw z*gzLSw=Qtf@YDT+biL=bF{*0-?H8kszKyrFjOG`EM(06$&-0yaHLN@&-+hvFRs$(# zMQm#0I>!w|)m$-I7azVltBmBVoTwR&z*oN%ytuua1*83IFgK;~W=MD{UfKsv=eih* zvrO#|B{Cbf^A!bd^DH@0*ci|EgW~&<;`hjKp@kPG4Whtl#7}-X`7hU2*LSx+^)oC+ z)9cKKx4h7wuV*LE^Mp4>g1T@rR?&RIj(x%PT-kr4m@tQU@TCQUnk^1pUA^nUnQG$&JN zg7vs{T3*WWWtIp&V%v0xCY^V;q!Q2E(!APv8^La6?p0K@-EW(8W2>3aup8D69-62v zA-|7xxBg_1=YK1hsu3nd_4)JXOyNc{7_{rVtK2icjKC!}ES;*L_&%ZjwPV zt{n=h6&4bFH47;d-SF`6ucqxbqq+aGEZ$P08Z+4r!6?!*X`jj#a90N&y;|>13k%EP zMrT$Z^7Op!$oXpn)^Paj^#=xSMs+#^K9cgWSoKC&*sa&+pdV$_n!IRKc6?`EFsKV{ zJ?ltYs#88Vqxief84z>61FwOiRjEI)vK}DHzwQU87#)3mgIWC!q3bar%V;Y2VI+PZ zMW;+0(n3##CDY2L3kC-rNjZ3sEExLo>MSi?yLmca51CMJD#padEY@1UDMk-Fr>Gz+ zO!mznIe&!GHEqR=DypgVMhlS%Y7B>BiVdAldHG?$I&Juio1W!~hA}Wq>$*PdW~6|@ zoA%kU_R12Q0!d90-079}Iv0l|5PcRKaMp@mtV|lG1`0f!Hgu;e_yr9;9@kfoq&;>3 zYNtOK&496uTwvhY!uZu2xvuYNoTSj{Kfda5^9Puimlu*cHN8GU*m>?rChP;NrvR(H zh!oQEv#>Cm{bfzu>G+>|jx~J!)<}6JB|rbd3Z@x?g&xcl$qMx0A|Y_(YO-^v&z!cy z@Vmq5j^4&I-}pGIcDk&T3)jQL!y?q;lnO810+g$to$^k4K?qLxG}f1z(GUU+KLGdmVA7QJombVr>%Ftz3in} zmv?pLR%&jl+#YW=mK#Rn;^v-O7hCf^YxFv4Xh$;nWQ!``@{IN#v^cP~y^_HX39=Zp zvYiDz)}Ots$Nzu8cQj0nvPi~8a_$c$9Q-jnEi}5l?{)?NuwZDQRflIB1$Vc^q>T;d zN-Ag*itdwgH}F`lZy|*<^HiRf@KO)F-dt!!%#-+Y4kNWK8L$j5s8@SU!}8fLBP|y< zABZr3$a~|%$};u3_r?uPSt+qEiZskf%qy#tld02LbQC!7B#^u;7MP!R2E`c;|BmDR z2(-?LqzH)Hc;4ezSfLjc&9xX!PfW@E9(J<4U17cZkxIqBIcGrlVqIq-Xv297`eCM4 zQAw#s8-;m!S+}el!fnfN7LS!p7}Nzl@dDde(rH>sdy%ok%jphS^LX5w?#Y0TsLiMd zb^8;p+Pv{A^g1zuUY_o*C^jD0aLvr{2r{Z2)^ukyQ`#2doqJw~9(u#1`sNkWdGZk@ zIn%6HXSut!)x<_KUQd-p-d3*!rWqXsMrtjTn~y|2P4#MAc~WkU=C_2fg@l*AwxSsp z>)tq^JcEM-PKRH~#UC#c{{6FtgJkm7g#ohp`x;BDe*}GF1_q_(l$Y=CW*E{^38pPs zSm5pM{|H*mu_KtsM?^Y2I`Vpb*zc}oVq>#hnxU)d^_{+9Vi|RUE4`SlpKsCyTv^X3 ztRRK%UiXJag{!LMc5Wq`@;R=Dxfnb?`gk7>mwG+?X%AybAVkU$G??@kZ%a6>R82kl zqMbRp2OzB}qUG(cXl57~_;+$(!F5a>`~eF?-uogP==FSIM3bn%5=PLZnp!_+ZaI+| zTu&PuG(KJU?fI$GKX-lIgvG?r*ULIJGVn%VQsgSz>t9qB^p$t(N<6^ezc!#Zd^fC~ zCYc7|KQeOJa0{=jAu(Cjx7|IcUCM~2<7FvM;C(Mf$7%P4G^ad$?U zeREnq|7USCT=CB^8eIKmZ{e$bpvvR`9lalKJQsfF%jKwt%X6P|zjD2$Z)rGT0OM>Lzca;r^_s0At^f>H*mcPNjpm5f<{$VeT0X;xDQIZ zyn-+#KmrSUdF868tX$mKn6chr_zo(&dcMlmftq6j4cv)!z}`fqzMIJ0yoTZ{uGT>9 zo}SvcAst`9c~h7~h!#dA*J*k>FP%MSHZu|e>^lHt%@=cOYDz{~88toVxXq7%-B7|( z?z>6`sko$Wd07N>AMZ2P#SRDUUl(Dp0c7MCo3!Dc&RS3Hu z1i1q5YHz%-`Ue&SpT>-aanRyr`Og>=bG#81vSjy6aqQ5r&s*c;wxhE$IR80p1q{EZ^!}IC&0GB$f3zKMX&6xXu{e40X5s{Ik--S6M-Vv{_ zFuyCCL%dNxOLXO}Jw4o1QOGaj;Kyqia%Srk*KI8%oR0>)??b#?dl>!NDM}E|rEIHe zm;qVw2uux%ACi;T)yK=0IpAK*b}aU*9dwXUEon^(YNAZ3KCIAU9%Pn90b}6-fA&q2 zGBag8J548FVMui84@g6E%mGp@^vt5z#3(bIf2S6w#vUe+8X)QfsS5BT%KkUAfg2-J zx}>EZ3EwD-71uSEl^gL%7Tl|03%u4a4W-hwii(S!#v6c6O}}FXKl$cVR;qSf<(_@f z@lLcDO~=7dbJlF{YDUSnzTsHj0x8}+;?bce-o}c{= z55F(n%qZswx<=I0aK^+cYqz+SHHzmcrqCfp*wxPqR8kFw7JLZD!C_=!F@Ni555q|o z@IJ)rr7_d@S)xnDn=C3W2Jh!Rv6iakce@+R%*^OCI{53?|2G_Ve;336QEvO0|kFH!{T^ z3cesw#n~jrsh>7PO}eG>xsqwx4$f*{8CzdeZ$fgnfPEp}8uLw9^@ zSplJF{6uTDopSvzQiHP)GGIY{55^)9Rdtpn^mmemF(S)X9+zodhWTDKRi(c(qinm^ zU?Bv=NFuUB2BLZ<*6jX~v{40BMIKv3CN|xx7b^Y07haLKXGiacT>}-BI87bz%mlxC zvoSDtw)fS3QflTtWc+_FI(NTCT0S54)e$7)iU_f4X^i zT>hzg;Z_rQu-*GMR?t|Q@YsR?qNt+B@o=KX7JIBo;PhN4E9%14_pKvVllVkB=C_4Q zvELJp+V=YB(9$CG{u&T^?@+khRR(8fzC#YlgxsRKyQx+cQWN~1x9t|2VuC)7rnA%V z^7dzbiATG_d^^X-AQ$R87OXosJ~p2`ZGR^=J9*)Z);blcpDo&IB*VRDGNu<76>M#p zPfkw!PF;=u1|xp@(bg6+Kd&`0Kd0#mMHq}X)gb_p_PbMG(EeL&zKIU`&+cJ+9Ja7A zRl%gMLnt7dKQSSG`)1CLE~*PEt*z_IX=C=-n3p)q?d#SMTWC;k74P^$yz*}wfW}D& z%inXZoy$w7jSriQD^;P}klY+gpl-AfWnlx`{ed~1)#7G^Ni24LgquC)&&ytOD0Js{ zdF7AVOODRRes&_F<9$T&dzeAcw;HztY8GXe>wm}~a$&i^Kyj$O>o8r0(-Gt>F56cp zz0vSirjm1-S*q{pWlx^{OGxYnDO1_7xp`5jM~!{#;o*0&n*+5P(+4R&`{lld1w-3; zOenvZTj9kZ>IFipF{B*}i3S!FU2Hb+yAKfGwh|t9m1{Y{V$mCcE^={H`tR(lw5p0C zXhx^lZmD+3w>!4uS(2!`fm8VKoknrS$TtFRh z!Tnbzusc%x8b%9Z7;y<&YxCY+_1-_3`}0R#PjB#Id+`3>px8v#pK>PlsvmWADGNp$ z(gp^sB(e{yDC_Pk5Ek9~4}1J5M~C&h4$NAMQTn3PcWUxNB~V~v(R`j86p9rR z7A8{^)L@%&HQ~8F!cO5Ats}U>7Fg%^puxrM z8b_L(pIYAe)E`AB;-_QLzbrhp-_Fc_kdn+6K5dGh zwX*fPPl^PisrhXE(1v9#DyF}Dz1!%W`pY5y_VD87R)5ul>i<>i6Fo0)T1}1mWkfFhTeDVdjlD{`>G~LX5CZ`f*y&}Nx{v<;zP^rzjEy2_69f;3vx)>mRFDoi- z%!fn7_@ZYdEoUiIc{H`OG~U(q0czp;0^q-#@NA2(pQG*Ex_bxb^NWscIGtbvw~+N` zqe@Fa=LpqO+aJtKB;ORMmz9c}1d@tlQ=DOD<+Q9VzyX?X z7tec>XNPDts80?oBTD3CvUvwqV_d{;h7-61rUsSSJ7lbUnI}d5ba#mQMhlrlL`Fu- z(a-||H4dCVoXE`)gJAX9hiGKvxCFJS z=_N^894cPH@XpsRQM-*67zh;ucc|}aOYB9GddCgKm)J$$FkqAJHwG{%Gjph~Pr$%{ z+!``8I=YW)bWcXgUwJVvL0_g~LpaD8{k5jZtd4K4sHDj0!6L)y<%a+A*maVVQ*1(0 zO=LK4%~wHHk&X;wc79$-fQ+7*CiHIOMGWe=fkG6}e*FUXUl;rv_A@XrAUv_DGBy#) z0MFNmi5?#`0}q!RPdbl_CO?M*13@Rf?S`+SNrNdeDB(ZgpP&IC-{9;W|$l4My5+jh4@V0{Q>{^?~^zz!WUh29q&X}cZMaXJ1!}|JuRK!XktgP z_oQxmUo9KGh+cXh)SiqXMgxNcU+oLQNLy*n5UmE1-AYzEJlz%r0nor3ztiX5`SI}y z86i|)6B+8bp{lN4aKZCunby;w{usA2qf|>f@tt2U7!mO(==B4KpJcah8aJm>OxojC zl4bP0=tF_l4l2tuqGWZ+pZ`ERn$C6&509XWYG!@-Ih{pE$wy8|z8u4~*meg_yS>70JWP20J2@%5wL@nG0BGsX zOx)~Jqc4#89G0UrVrEMxxwyCHhfqJ=0e9!=QRs2YgU@eza-b^~@t&~s8 zFbuYW542P@?aOkMSdBJ-fdw+-kx;P~v3A!w!IYspZmDq!UR>p~doGgt_-?Y*8^iWTea3D|XqLi&||K|}}7&#|6vPqv+Q#U7s!7i;_@`I*5=KweWz zHL{n8EhAgpTgvXtQlCM#cnSyX5p00O_MMWCYB+q@!x-}$L!F5n9Fh60;{%q!ci4g=-I_1cSkd@ zP;?lJx1%R4(Jh-SrT*R9Dfi7Jg?k#-qY5Cgu-7{s1~4L~N_V9zoN;p-uN*Hr%r_f7 z3V^-(Ubyf@Z7o%hMlsLl1@lam)~ZOCmGAceJ; z95m9sB3Ez|U7lNUSOxqWYsVJy~B9e7}chK_Z(@23#YO4^IT3x{~lM47B#-ptP0ws!_VOC zABHZ5$d=$!nuTXDPycclt4tVk5#=~dENa!J)zz09KSK#gL(gbY6u0U=>HT_jKbl{)Ul4h^{jV^i*6m<=rd6c5WWefCnTRaCBUW6C6^U^f?m*`&7Wc?>Kp{9BqOg!SQJV3SFS zx#yRv64U-yV-yKD)~G{A>e8KidMSB%ZpVPxBb}FjpI;v)Nu9tX0GuDD!NV!^=eM5E z!uBU8&*zBZJ2it^b@nR+9>?c3^IwdNMqTrK34r<+N_kJKbv8COJ=(a<56uXAEjx_# z?kdLPX63;mS9@b7hpEE{ixdQ``U>)c@EqPfuw>qIrk0aT&GWiPVM;c_{Q!JiY$x>90nwKF_w#FAhkOHfU2TMnB4Jks!81mT2+3s&aNn z2GvSJ&5CX~YH5}rvy|)zA#2APZMfDu_~T=;o@`*Kl~f$Mb>%pRyOaVJ-qH%i%$ji6 zPZ$Uno4uR9{sYLz@~6a-fRD(C5((WHatzEs#G-Lx8#*!3G2B*w9^MaQgQ>9@vvj9F z3}Pd5Oq%ys`LAOWxhqT1aY0cVNG6XB&U@su%KyYxr`ni}9%q=jc<4SRMO7rPDqxrS z`FUS_yCmDe(zrT5R~TAr_T*6?GMnfB+34Xi+)TNAP|5yu2p*APYPp6EO<$* zk!tmb0fEUj4F|s25A*LR{5wx=b17w-4z20M9#i|EDjLVx)bW@e1dGBMq$Wc+Tc6Rh0$mH~R*y|G;)kfv1wW@Fy@LMt2eGfM-q0{LAY`9;ZREGo+ z0cTR^Z%(#oHG}xcSR+5Sce9%?)M5X+Zer`acQ1Lo$$N#P-dar-*)hf#CCMz=wo;_cs{n}_>!v#Z`#^4jX^nD>6}px(x#B}mjmF_o4%Mv_0>v_|0z zBNjsOP5e}!VyM1<9FqnMhB_L;8VL-oEl(u52a(bJMZPApRDkD$w%zex(q@`gqis&*AoR;H*~a;8zU3j_rYI`igy>T03R{{@u#K9hXb{M z;dIs&#+_KZl@|0VYX}k`2{ezK^OD2uHEvt^H&s)$q+`7qT@RN|_$rmO9A0*rq5g{E z?Sk#SilOp#{;kG!kT1j(+ZGJ~u-89NR`%L&T_5S`-P`h}L#z*e6*Y$Iu|{XXAX-3C z71`7lyE;&FIceC~fB%V}<{3gU_X=Ag#=zVOgzfi!>*o;*oPn=)^;+=3{df3puk-xV z%gbY!f#+sgmHNZC_Ie*=?d@4Ji~@)q16NkOpM=*Z=2^3y744+!U0k_uZW`Jy(5nXS zcsDv8Ti+$8=P9fgf8Q?w5gGbkFlPai!uPKY;yc}l6k*jt-CtC_aoFAuoY&{=f^$eb zS=|q$9d*1kfR@H8e}2CvpUb31*z|8DAuq$tj(sXsI?R`F5jbh-uz9R7Hm2Cv$S+Aw z7{nnWz0qcwA(Tbhj31=&P#$I$`49Cb@;GyGRRus4FaHM7Zca=#O;826T`C#|A*zk!(g6KCA#10k2t_$v_r#X{4pLf~qR-1yQOosY-!PMj_B`(vlgcrYoFQm%p zENslBoTp_NF)(m&aQIhe#R3(koC!73c%jLe3v$GFYGMbpVwXvnSGi~Uz5aKiU3qH; zco8UW?(R4wDLHTmYXYC6&0@ki94hd#g8WRd>?E-8riWW%xKp}X_|OVDRjhOVihW8X z_gIvg5V}&SY~J$?ve+8~i##orCXZUj#fR}GCG}Xt65FB|FyTaaSKM$8U?GHiF)!JH zZf`wGRKPu3{jq{C^;WJUWej!3-F^(vc(V>_IxjObI6?zAs)h#McMr)9NchxvGSBe# zcnx(v5mIJxvDM%9$Xm5#!QP&lUP2$|%xPS}Yh4WHbwyDIiN%mQwteDIv3A`4R|s;t z@R=5J80^CU32ZryO(Jj>$Ung|1gMH9e4yvSgR^=zQ6XC)1WpRzVjr2VQ+AO^7jS%4 z`Wwq#UJdPSD+epB*{`pn+P1@%b!7h6>TQSL z-UddE0t6wf0C@GSg<|E5^8>ZONUk7EEVlX9;A3Dv73-|ejVJX?96*k^wsL&Fh4ax^*ZxiA1_kG_1<_AgO?2E zQ1VFoD(XW|7aU576jCkD*i}b>uPu`1J}iM3HbN6;8pVo&$*;w!Ws=etB4)zlc|cub zwwmJ>^wZk-$z!4MtIm|+YQDO5p~05I0}AdfoW-csc0N6k)q<9mrq!_h6dFN{-y`>% zbdC1TZ{`g-X@iqjqu;eR1Aa88S1^b-PLf#gn+HlhBdw{#!ugm;y~)*-8CMq~FFf~; zj8%?HqfKXi`}e<*rKPP|`b{O?jPoebz*EaI0JbL?plRpP>7|m7$n!Z0)Nv)6&U|_^ zHdDx+D{r6cSqBg9#aj#FmztONX?bM@EbNE@0Jt+2bIKL)R_NX^>NPqP62X&sEW(zk zfR*UC=}vy*yLq_Rd7St_?UtlGJ^A~kk{(-xN>p;kDs<{Hd2JDz?Rb#55Ke47+}-Oe z$G_j6t|?2K(L}QAE7;n)j+l=EdLTM=>40oLO%?($rx`+>)g(1AMVWr`N*YF3Sa_+; zo2J$Cj4Gd6Li)nQ@SC-V8tH)6rRQ30T;uX*6E#Hxgho^6h zc=rDB?a6UM{O;jYvEJFZb@UThh2t|mpo2Me9$vDm)`EGdxehnhWV)vpEM}HP3j29m zrcO&3#^oP^>Ed{S?755%*M3)!`UrXN28PS*O)zdpoGKT+}zYRG;r82 z^LU=@MeHo=GHXG6;^Im9;W%vPS@y=j0KP&R>_*h(7(#@{Cj_0&0GixfgX6=~g1YjQ z#|R&KwbLIB4dA6Z%h~mH5`N(IY7tolv-k$QBvx1{2iu4Kk44t0dh2pJcP%TjtvoCv z#>kvX>U~xZ1JvSg284Q0yWKvs@7_1NvFZ{LiD*B)vu~5I$Xi*mRH4teBr$=!YH$D+h<>_rVj}P1yq+0BD}nUx_WvV zlJIJ*6*X*0AdjDhW(JlsrZq!*06p!_0MQ>{ftq-;bTWhQ3v<$Dta!0hD^PojwHEZ_ z#hTF5A%ow|&0r?Y(svc4L?su8{O_YlUTJd2*_3f!d^TUL{ffuq<0GeWEt9xDas`bkAI+f4wN40DS%5~>M-n^H+{i)rI<}SIXr(P9Jsn8+FK4hnoI>5{r$$!zn{Tb&UazBSR>Zvb zPEK;Gt7C6&1Q?amzvz35Uha(1q>M@vI4#nSXkMlf{YNtZx2xz}v`hqb>nOx>dXA$4x98Z)<$&@XUuINnlt(xEj1LI8riw4~PLqWAF4 zK$N5Jr1xn@ikSC(H!zt~EVO9nJ3y6bbNVEk@)a|!b>k#3shaczw%o>0bokyVL2!E^? zq_J{oZ~vsID)*;+f=DW#JLPndsvBIQOfwhfqWcLx4Ld#PG9Th;P_>H8=7D?SBh%=pZEiMrSNKBzdg zkW(ZqrU`=x>n+E(a>FR|ii=SK=wbo!;do4c@`Hgu$+;>@XqDk$k#ffV@rokjCQq%| zo6Fr%3&_8(75Z&4c1w*K?4kd%#S%#PiZnG7A`2-JWrIt}Xix*GQB`>`Ybq!DjGwPn zPYl7CG1i*GPOG24H@c}A>z~GF=>^ph!ahL)ukqhv%8&&Xs{_p{^^NDC8zdo?hR<~Q z1m*7bJ24jU+WPv0O+t#H%aqLFk1JosyuRgiZsUnO64Sh6T@B_?-NNr!o|BM1T@`mC z$!Rqpl-OiA0Vi$#4$_&j8e0Y`5{KePAWBKSy%-1xy$7a~8u|>@JV#VEj(t?H_rAM7oPr(b(#!|?h0E}DbF9Pf$xFa)H7e&kRltrEgYcjGZuqk4 z!>hvc!xI&C=lM7j%k#EKHKQj_CUR&u%;Rd3|L9;*M$;L|d3P)~-;5R#8ux1<8r*2V zG8RF=Rzt|BVrAA77&0?EH_>Lv6)WDDzS8DCt)@#fTSyn)Ry*nK+y^;+g!}u4ygWe! zX)uK!ge}i!t9|0=V)X)|7xhi(dX9dGeNu8brF_+tt76`hKp7sTbJ8%flr1M=8xY9a z-a0qm3>WLV#VV^maf3NO80owf+TD!v1cg(@E{&JL`t;qdeGH#CBIogtVz|FYyk z&U_GH%}@QeIwXl2M=N~Y2eY8Plat;z35>2kX4K`RdAg06PnDMP`?9JDE?>_R+$$>mb@VSLjEw2ywZRcy!3|9=V;Mbb^`cnGdWcA-i zzd^WFnid=GaQQ_ppK?;Y(B&JL>x;li7!NUJ@SKmgw z?p1v}+UCVEp&-+03rLVqQA#;R2||}Jq!r!gC1O@tSYM9^;;5{w>^mSwMufxs{o_$B zjn4tGn=zuCU;ApHQ!v=+y#p;4`A`@jbLtrJEG?T_Jx z9i70XJx}c+i6)0@w}zqGLV4|9Z>!Xp@lpGJ8*nNzI+a9+3|EQKx*M71PM&cVM(62i zJu2<3TVN~G>nYpVD#8WQ&s}?LManciEgAx;KVrUr&nqPKrIvW@fpWn%1`_(t2iAfz z=xEF8lo{!C#yD={1M$I$=fWQ{uiN4kxd8k+q!4Cjf-`zO=MS>yaN!`ogTYI}vq6f( z@{6({GL+;L@UD0T!{)8Fk};HxtALmno-^oE>>EP zuLqug-o4sJFVDd>R;EP|mnPnQjC2Y>2C1_dV9=%=wF`7k7XHIdPP(st^546RVMK zD-e|9Nb%I1LzXiMBzyZK3I4KCrMFzHh|Hs?OAeGg6At!%G8$t+XC ztVnc{r1-sfGG@#idbF}6r-q3tB_(jEshN@A*;(~3heiH}rh19X)5YBzsGd{HQO#2g zy}UbX^o5eu(C(!kh99G%fIcuW++I~9#iTg;ojd0lgWKp-U-pU*%^x+?lJXITr!9@Z zf<&YS>i-lIrd!1*4J6Mt#)}&sKH$vd}@9IwRy`we14V`UnKpM zAq`a<$Z*;87Vjc$+&6ADTYP6UYp}ekIT2DMQc+86ffl`uVSJ0;*6POR4J9{vSxnK; z)eVD6CUQ^8A$R9rx)34g>PyvxdT>)j=Olb^y8=3KOUrYYNB6KA^D(OYKr{Kqf*%oV z@ak`xo=L5+SLvm7@qRE1S<9>gZ>Aky% zq@S8#WvaFfB75;;NQv#6>uX0ergtcF#Ax-fnzZ0anC>N${2;vTKh4mKvwl+)kzdK^^^~D?Z}DN)kAEs@K=|1|^LUXdgFBtk+$g&_?;-@&$U+wYTlM7Lk`A=?z z`%}C@Ic?u}wln!u+a1=*Lmsp(Z~}P5ZL2yiBRpLtTism@uk!LXNw<43uPxTYo|4I^ zmrsoupM@}zvy&x7+4O37vIly2-GE�b}+fDqJ=%})7hS4mR4Hl2^5!B>muW4o=p)q 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