refactor: 重构规范部分代码、邮件队列增加失败重试、去除多个支付方式、更新依赖

This commit is contained in:
xboard 2024-04-10 00:51:03 +08:00
parent ec63e05575
commit 4c6c7182e2
50 changed files with 421 additions and 1005 deletions

View File

@ -46,8 +46,7 @@ class CheckServer extends Command
private function checkOffline() private function checkOffline()
{ {
$serverService = new ServerService(); $servers = ServerService::getAllServers();
$servers = $serverService->getAllServers();
foreach ($servers as $server) { foreach ($servers as $server) {
if ($server['parent_id']) continue; if ($server['parent_id']) continue;
if ($server['last_check_at'] && (time() - $server['last_check_at']) > 1800) { if ($server['last_check_at'] && (time() - $server['last_check_at']) > 1800) {

View File

@ -45,7 +45,7 @@ class CheckTicket extends Command
->get(); ->get();
foreach ($tickets as $ticket) { foreach ($tickets as $ticket) {
if ($ticket->user_id === $ticket->last_reply_user_id) continue; if ($ticket->user_id === $ticket->last_reply_user_id) continue;
$ticket->status = 1; $ticket->status = Ticket::STATUS_CLOSED;
$ticket->save(); $ticket->save();
} }
} }

View File

@ -5,8 +5,6 @@ namespace App\Console\Commands;
use App\Services\MailService; use App\Services\MailService;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Models\User; use App\Models\User;
use App\Models\MailLog;
use App\Jobs\SendEmailJob;
class SendRemindMail extends Command class SendRemindMail extends Command
{ {

View File

@ -44,7 +44,7 @@ class XboardInstall extends Command
*/ */
public function handle() public function handle()
{ {
try { try {
// \Artisan::call('config:clear'); // \Artisan::call('config:clear');
$isDocker = env('docker', false); $isDocker = env('docker', false);
$this->info("__ __ ____ _ "); $this->info("__ __ ____ _ ");
@ -52,7 +52,8 @@ class XboardInstall extends Command
$this->info(" \ \/ / | __ \ / _ \ / _` | '__/ _` | "); $this->info(" \ \/ / | __ \ / _ \ / _` | '__/ _` | ");
$this->info(" / /\ \ | |_) | (_) | (_| | | | (_| | "); $this->info(" / /\ \ | |_) | (_) | (_| | | | (_| | ");
$this->info("/_/ \_\|____/ \___/ \__,_|_| \__,_| "); $this->info("/_/ \_\|____/ \___/ \__,_|_| \__,_| ");
if ((\File::exists(base_path() . '/.env') && $this->getEnvValue('INSTALLED')) if (
(\File::exists(base_path() . '/.env') && $this->getEnvValue('INSTALLED'))
|| (env('INSTALLED', false) && $isDocker) || (env('INSTALLED', false) && $isDocker)
) { ) {
$securePath = admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key')))); $securePath = admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key'))));
@ -60,23 +61,15 @@ class XboardInstall extends Command
$this->warn("如需重新安装请清空目录下 .env 文件的内容Docker安装方式不可以删除此文件"); $this->warn("如需重新安装请清空目录下 .env 文件的内容Docker安装方式不可以删除此文件");
$this->warn("快捷清空.env命令"); $this->warn("快捷清空.env命令");
note('rm .env && touch .env'); note('rm .env && touch .env');
return ; return;
} }
if (is_dir(base_path() . '/.env')){ if (is_dir(base_path() . '/.env')) {
$this->error('😔安装失败Docker环境下安装请保留空的 .env 文件'); $this->error('😔安装失败Docker环境下安装请保留空的 .env 文件');
return ; return;
} }
// 选择是否使用Sqlite // 选择是否使用Sqlite
if(confirm(label: '是否启用Sqlite(无需额外安装)代替Mysql',default: false, yes: '启用', no: '不启用')) { if (confirm(label: '是否启用Sqlite(无需额外安装)代替Mysql', default: false, yes: '启用', no: '不启用')) {
$sqliteFile = '.docker/.data/database.sqlite'; $sqliteFile = '.docker/.data/database.sqlite';
if (!file_exists(base_path($sqliteFile))) {
// 创建空文件
if (!touch(base_path($sqliteFile))) {
echo "sqlite创建成功: $sqliteFile";
} else {
echo "sqlite创建失败";
}
}
$envConfig = [ $envConfig = [
'DB_CONNECTION' => 'sqlite', 'DB_CONNECTION' => 'sqlite',
'DB_DATABASE' => $sqliteFile, 'DB_DATABASE' => $sqliteFile,
@ -84,16 +77,16 @@ class XboardInstall extends Command
'DB_USERNAME' => '', 'DB_USERNAME' => '',
'DB_PASSWORD' => '', 'DB_PASSWORD' => '',
]; ];
}else{ } else {
$isMysqlValid = false; $isMysqlValid = false;
while(!$isMysqlValid){ while (!$isMysqlValid) {
$envConfig = [ $envConfig = [
'DB_CONNECTION' => 'mysql', 'DB_CONNECTION' => 'mysql',
'DB_HOST' => text(label: "请输入数据库地址", default: '127.0.0.1', required: true), 'DB_HOST' => text(label: "请输入数据库地址", default: '127.0.0.1', required: true),
'DB_PORT' => text(label: '请输入数据库端口', default: '3306', required: true), 'DB_PORT' => text(label: '请输入数据库端口', default: '3306', required: true),
'DB_DATABASE' => text(label:'请输入数据库名', default:'xboard', required: true), 'DB_DATABASE' => text(label: '请输入数据库名', default: 'xboard', required: true),
'DB_USERNAME' => text(label:'请输入数据库用户名', required: true), 'DB_USERNAME' => text(label: '请输入数据库用户名', required: true),
'DB_PASSWORD' => text(label:'请输入数据库密码', required: false), 'DB_PASSWORD' => text(label: '请输入数据库密码', required: false),
]; ];
try { try {
\Config::set("database.connections.mysql.host", $envConfig['DB_HOST']); \Config::set("database.connections.mysql.host", $envConfig['DB_HOST']);
@ -114,15 +107,15 @@ class XboardInstall extends Command
$envConfig['APP_KEY'] = 'base64:' . base64_encode(Encrypter::generateKey('AES-256-CBC')); $envConfig['APP_KEY'] = 'base64:' . base64_encode(Encrypter::generateKey('AES-256-CBC'));
$envConfig['INSTALLED'] = true; $envConfig['INSTALLED'] = true;
$isReidsValid = false; $isReidsValid = false;
while(!$isReidsValid){ while (!$isReidsValid) {
// 判断是否为Docker环境 // 判断是否为Docker环境
if ($isDocker == 'true' && (confirm(label: '是否启用Docker内置的Redis', default: true, yes:'启用', no:'不启用'))){ if ($isDocker == 'true' && (confirm(label: '是否启用Docker内置的Redis', default: true, yes: '启用', no: '不启用'))) {
$envConfig['REDIS_HOST'] = '/run/redis-socket/redis.sock'; $envConfig['REDIS_HOST'] = '/run/redis-socket/redis.sock';
$envConfig['REDIS_PORT'] = 0; $envConfig['REDIS_PORT'] = 0;
$envConfig['REDIS_PASSWORD'] = null; $envConfig['REDIS_PASSWORD'] = null;
}else{ } else {
$envConfig['REDIS_HOST'] = text(label: '请输入Redis地址', default: '127.0.0.1',required: true); $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_PORT'] = text(label: '请输入Redis端口', default: '6379', required: true);
$envConfig['REDIS_PASSWORD'] = text(label: '请输入redis密码(默认: null)', default: ''); $envConfig['REDIS_PASSWORD'] = text(label: '请输入redis密码(默认: null)', default: '');
} }
$redisConfig = [ $redisConfig = [
@ -134,11 +127,11 @@ class XboardInstall extends Command
'database' => 0, 'database' => 0,
], ],
]; ];
try{ try {
$redis = new \Illuminate\Redis\RedisManager(app(), 'phpredis', $redisConfig); $redis = new \Illuminate\Redis\RedisManager(app(), 'phpredis', $redisConfig);
$redis->ping(); $redis->ping();
$isReidsValid = true; $isReidsValid = true;
}catch(\Exception $e){ } catch (\Exception $e) {
// 连接失败,输出错误消息 // 连接失败,输出错误消息
$this->error("redis连接失败" . $e->getMessage()); $this->error("redis连接失败" . $e->getMessage());
$this->info("请重新输入REDIS配置"); $this->info("请重新输入REDIS配置");
@ -147,12 +140,16 @@ class XboardInstall extends Command
if (!copy(base_path() . '/.env.example', base_path() . '/.env')) { if (!copy(base_path() . '/.env.example', base_path() . '/.env')) {
abort(500, '复制环境文件失败,请检查目录权限'); abort(500, '复制环境文件失败,请检查目录权限');
}; }
$email = text(label: '请输入管理员账号',required: true, ;
validate: fn (string $email): ?string => match (true) { $email = text(
! filter_var($email, FILTER_VALIDATE_EMAIL) => '请输入有效的邮箱地址.', label: '请输入管理员账号',
required: true,
validate: fn(string $email): ?string => match (true) {
!filter_var($email, FILTER_VALIDATE_EMAIL) => '请输入有效的邮箱地址.',
default => null, default => null,
}); }
);
$password = Helper::guid(false); $password = Helper::guid(false);
$this->saveToEnv($envConfig); $this->saveToEnv($envConfig);
@ -197,32 +194,30 @@ class XboardInstall extends Command
private function saveToEnv($data = []) private function saveToEnv($data = [])
{ {
function set_env_var($key, $value) foreach ($data as $key => $value) {
{ function ($key, $value) {
if (! is_bool(strpos($value, ' '))) { if (!is_bool(strpos($value, ' '))) {
$value = '"' . $value . '"'; $value = '"' . $value . '"';
} }
$key = strtoupper($key); $key = strtoupper($key);
$envPath = app()->environmentFilePath(); $envPath = app()->environmentFilePath();
$contents = file_get_contents($envPath); $contents = file_get_contents($envPath);
preg_match("/^{$key}=[^\r\n]*/m", $contents, $matches); preg_match("/^{$key}=[^\r\n]*/m", $contents, $matches);
$oldValue = count($matches) ? $matches[0] : ''; $oldValue = count($matches) ? $matches[0] : '';
if ($oldValue) { if ($oldValue) {
$contents = str_replace("{$oldValue}", "{$key}={$value}", $contents); $contents = str_replace("{$oldValue}", "{$key}={$value}", $contents);
} else { } else {
$contents = $contents . "\n{$key}={$value}\n"; $contents = $contents . "\n{$key}={$value}\n";
} }
$file = fopen($envPath, 'w'); $file = fopen($envPath, 'w');
fwrite($file, $contents); fwrite($file, $contents);
return fclose($file); return fclose($file);
} };
foreach($data as $key => $value) {
set_env_var($key, $value);
} }
return true; return true;
} }

View File

@ -5,10 +5,8 @@ namespace App\Exceptions;
use App\Helpers\ApiResponse; use App\Helpers\ApiResponse;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Illuminate\View\ViewException;
use Throwable; use Throwable;
use Facade\Ignition\Exceptions\ViewException;
use Illuminate\Support\Facades\Log;
class Handler extends ExceptionHandler class Handler extends ExceptionHandler
{ {

View File

@ -1,4 +1,5 @@
<?php <?php
use App\Support\Setting;
if (! function_exists("get_request_content")){ if (! function_exists("get_request_content")){
@ -20,14 +21,14 @@ if (! function_exists('admin_setting')) {
function admin_setting($key = null, $default = null) function admin_setting($key = null, $default = null)
{ {
if ($key === null) { if ($key === null) {
return app('setting'); return App::make(Setting::class)->toArray();
} }
if (is_array($key)) { if (is_array($key)) {
app('setting')->save($key); App::make(Setting::class)->save($key);
return; return '';
} }
$default = config('v2board.'. $key) ?? $default; $default = config('v2board.'. $key) ?? $default;
return app('setting')->get($key) ?? $default ; return App::make(Setting::class)->get($key) ?? $default ;
} }
} }

View File

@ -4,8 +4,8 @@ namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ConfigSave; use App\Http\Requests\Admin\ConfigSave;
use App\Jobs\SendEmailJob;
use App\Models\Setting; use App\Models\Setting;
use App\Services\MailService;
use App\Services\TelegramService; use App\Services\TelegramService;
use App\Utils\Dict; use App\Utils\Dict;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -33,7 +33,7 @@ class ConfigController extends Controller
public function testSendMail(Request $request) public function testSendMail(Request $request)
{ {
$obj = new SendEmailJob([ $mailLog = MailService::sendEmail([
'email' => $request->user['email'], 'email' => $request->user['email'],
'subject' => 'This is xboard test email', 'subject' => 'This is xboard test email',
'template_name' => 'notify', 'template_name' => 'notify',
@ -45,7 +45,7 @@ class ConfigController extends Controller
]); ]);
return response([ return response([
'data' => true, 'data' => true,
'log' => $obj->handle() 'log' => $mailLog
]); ]);
} }

View File

@ -160,13 +160,13 @@ class OrderController extends Controller
$order->total_amount = $request->input('total_amount'); $order->total_amount = $request->input('total_amount');
if ($order->period === 'reset_price') { if ($order->period === 'reset_price') {
$order->type = 4; $order->type = Order::TYPE_RESET_TRAFFIC;
} else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id) { } else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id) {
$order->type = 3; $order->type = Order::TYPE_UPGRADE;
} else if ($user->expired_at > time() && $order->plan_id == $user->plan_id) { } else if ($user->expired_at > time() && $order->plan_id == $user->plan_id) {
$order->type = 2; $order->type = Order::TYPE_RENEWAL;
} else { } else {
$order->type = 1; $order->type = Order::TYPE_NEW_PURCHASE;
} }
$orderService->setInvite($user); $orderService->setInvite($user);

View File

@ -19,8 +19,7 @@ class GroupController extends Controller
return $this->success([ServerGroup::find($request->input('group_id'))]); return $this->success([ServerGroup::find($request->input('group_id'))]);
} }
$serverGroups = ServerGroup::get(); $serverGroups = ServerGroup::get();
$serverService = new ServerService(); $servers = ServerService::getAllServers();
$servers = $serverService->getAllServers();
foreach ($serverGroups as $k => $v) { foreach ($serverGroups as $k => $v) {
$serverGroups[$k]['user_count'] = User::where('group_id', $v['id'])->count(); $serverGroups[$k]['user_count'] = User::where('group_id', $v['id'])->count();
$serverGroups[$k]['server_count'] = 0; $serverGroups[$k]['server_count'] = 0;

View File

@ -12,8 +12,7 @@ class ManageController extends Controller
{ {
public function getNodes(Request $request) public function getNodes(Request $request)
{ {
$serverService = new ServerService(); return $this->success(ServerService::getAllServers());
return $this->success($serverService->getAllServers());
} }
public function sort(Request $request) public function sort(Request $request)

View File

@ -2,12 +2,10 @@
namespace App\Http\Controllers\V1\Admin\Server; namespace App\Http\Controllers\V1\Admin\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ServerTrojanSave; use App\Http\Requests\Admin\ServerTrojanSave;
use App\Http\Requests\Admin\ServerTrojanUpdate; use App\Http\Requests\Admin\ServerTrojanUpdate;
use App\Models\ServerTrojan; use App\Models\ServerTrojan;
use App\Services\ServerService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class TrojanController extends Controller class TrojanController extends Controller
@ -75,10 +73,4 @@ class TrojanController extends Controller
ServerTrojan::create($server->toArray()); ServerTrojan::create($server->toArray());
return $this->success(true); return $this->success(true);
} }
public function viewConfig(Request $request)
{
$serverService = new ServerService();
$config = $serverService->getTrojanConfig($request->input('node_id'), 23333);
return $this->success($config);
}
} }

View File

@ -80,7 +80,7 @@ class TicketController extends Controller
]); ]);
try { try {
$ticket = Ticket::findOrFail($request->input('id')); $ticket = Ticket::findOrFail($request->input('id'));
$ticket->status = 1; $ticket->status = Ticket::STATUS_CLOSED;
$ticket->save(); $ticket->save();
return $this->success(true); return $this->success(true);
} catch (ModelNotFoundException $e) { } catch (ModelNotFoundException $e) {

View File

@ -17,8 +17,7 @@ class AppController extends Controller
$user = $request->user; $user = $request->user;
$userService = new UserService(); $userService = new UserService();
if ($userService->isAvailable($user)) { if ($userService->isAvailable($user)) {
$serverService = new ServerService(); $servers = ServerService::getAvailableServers($user);
$servers = $serverService->getAvailableServers($user);
} }
$defaultConfig = base_path() . '/resources/rules/app.clash.yaml'; $defaultConfig = base_path() . '/resources/rules/app.clash.yaml';
$customConfig = base_path() . '/resources/rules/custom.app.clash.yaml'; $customConfig = base_path() . '/resources/rules/custom.app.clash.yaml';

View File

@ -65,8 +65,7 @@ class ClientController extends Controller
$region = $geo['region'] ?? null; $region = $geo['region'] ?? null;
// 获取服务器列表 // 获取服务器列表
$serverService = new ServerService(); $servers = ServerService::getAvailableServers($user);
$servers = $serverService->getAvailableServers($user);
// 判断不满足,不满足的直接过滤掉 // 判断不满足,不满足的直接过滤掉
$serversFiltered = collect($servers)->reject(function ($server) use ($typesArr, $filterArr, $region, $supportHy2){ $serversFiltered = collect($servers)->reject(function ($server) use ($typesArr, $filterArr, $region, $supportHy2){

View File

@ -17,14 +17,15 @@ class PaymentController extends Controller
try { try {
$paymentService = new PaymentService($method, null, $uuid); $paymentService = new PaymentService($method, null, $uuid);
$verify = $paymentService->notify($request->input()); $verify = $paymentService->notify($request->input());
if (!$verify) return $this->fail([422,'verify error']); if (!$verify)
return $this->fail([422, 'verify error']);
if (!$this->handle($verify['trade_no'], $verify['callback_no'])) { if (!$this->handle($verify['trade_no'], $verify['callback_no'])) {
return $this->fail([400,'handle error']); return $this->fail([400, 'handle error']);
} }
return (isset($verify['custom_result']) ? $verify['custom_result'] : 'success'); return (isset($verify['custom_result']) ? $verify['custom_result'] : 'success');
} catch (\Exception $e) { } catch (\Exception $e) {
\Log::error($e); \Log::error($e);
return $this->fail([500,'fail']); return $this->fail([500, 'fail']);
} }
} }
@ -32,9 +33,10 @@ class PaymentController extends Controller
{ {
$order = Order::where('trade_no', $tradeNo)->first(); $order = Order::where('trade_no', $tradeNo)->first();
if (!$order) { if (!$order) {
return $this->fail([400202,'order is not found']); return $this->fail([400202, 'order is not found']);
} }
if ($order->status !== 0) return true; if ($order->status !== Order::STATUS_PENDING)
return true;
$orderService = new OrderService($order); $orderService = new OrderService($order);
if (!$orderService->paid($callbackNo)) { if (!$orderService->paid($callbackNo)) {
return false; return false;

View File

@ -13,24 +13,23 @@ class TelegramController extends Controller
protected $commands = []; protected $commands = [];
protected $telegramService; protected $telegramService;
public function __construct(Request $request) public function __construct(TelegramService $telegramService)
{ {
if ($request->input('access_token') !== md5(admin_setting('telegram_bot_token'))) { $this->telegramService = $telegramService;
throw new ApiException('access_token is error', 401);
}
$this->telegramService = new TelegramService();
} }
public function webhook(Request $request) public function webhook(Request $request)
{ {
if ($request->input('access_token') !== md5(admin_setting('telegram_bot_token'))) {
throw new ApiException('access_token is error', 401);
}
$data = json_decode(get_request_content(),true); $data = json_decode(get_request_content(),true);
$this->formatMessage($data); $this->formatMessage($data);
$this->formatChatJoinRequest($data); $this->formatChatJoinRequest($data);
$this->handle(); $this->handle();
} }
public function handle() private function handle()
{ {
if (!$this->msg) return; if (!$this->msg) return;
$msg = $this->msg; $msg = $this->msg;
@ -68,7 +67,7 @@ class TelegramController extends Controller
} }
} }
public function getBotName() private function getBotName()
{ {
$response = $this->telegramService->getMe(); $response = $this->telegramService->getMe();
return $response->result->username; return $response->result->username;

View File

@ -2,7 +2,6 @@
namespace App\Http\Controllers\V1\Passport; namespace App\Http\Controllers\V1\Passport;
use App\Exceptions\ApiException;
use App\Helpers\ResponseEnum; use App\Helpers\ResponseEnum;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Passport\AuthForget; use App\Http\Requests\Passport\AuthForget;

View File

@ -18,16 +18,6 @@ use Illuminate\Support\Facades\Cache;
class DeepbworkController extends Controller class DeepbworkController extends Controller
{ {
CONST V2RAY_CONFIG = '{"log":{"loglevel":"debug","access":"access.log","error":"error.log"},"api":{"services":["HandlerService","StatsService"],"tag":"api"},"dns":{},"stats":{},"inbounds":[{"port":443,"protocol":"vmess","settings":{"clients":[]},"sniffing":{"enabled":true,"destOverride":["http","tls"]},"streamSettings":{"network":"tcp"},"tag":"proxy"},{"listen":"127.0.0.1","port":23333,"protocol":"dokodemo-door","settings":{"address":"0.0.0.0"},"tag":"api"}],"outbounds":[{"protocol":"freedom","settings":{}},{"protocol":"blackhole","settings":{},"tag":"block"}],"routing":{"rules":[{"type":"field","inboundTag":"api","outboundTag":"api"}]},"policy":{"levels":{"0":{"handshake":4,"connIdle":300,"uplinkOnly":5,"downlinkOnly":30,"statsUserUplink":true,"statsUserDownlink":true}}}}'; CONST V2RAY_CONFIG = '{"log":{"loglevel":"debug","access":"access.log","error":"error.log"},"api":{"services":["HandlerService","StatsService"],"tag":"api"},"dns":{},"stats":{},"inbounds":[{"port":443,"protocol":"vmess","settings":{"clients":[]},"sniffing":{"enabled":true,"destOverride":["http","tls"]},"streamSettings":{"network":"tcp"},"tag":"proxy"},{"listen":"127.0.0.1","port":23333,"protocol":"dokodemo-door","settings":{"address":"0.0.0.0"},"tag":"api"}],"outbounds":[{"protocol":"freedom","settings":{}},{"protocol":"blackhole","settings":{},"tag":"block"}],"routing":{"rules":[{"type":"field","inboundTag":"api","outboundTag":"api"}]},"policy":{"levels":{"0":{"handshake":4,"connIdle":300,"uplinkOnly":5,"downlinkOnly":30,"statsUserUplink":true,"statsUserDownlink":true}}}}';
public function __construct(Request $request)
{
$token = $request->input('token');
if (empty($token)) {
throw new ApiException('token is null');
}
if ($token !== admin_setting('server_token')) {
throw new ApiException('token is error');
}
}
// 后端获取用户 // 后端获取用户
public function user(Request $request) public function user(Request $request)
@ -39,8 +29,7 @@ class DeepbworkController extends Controller
return $this->fail([400,'节点不存在']); return $this->fail([400,'节点不存在']);
} }
Cache::put(CacheKey::get('SERVER_VMESS_LAST_CHECK_AT', $server->id), time(), 3600); Cache::put(CacheKey::get('SERVER_VMESS_LAST_CHECK_AT', $server->id), time(), 3600);
$serverService = new ServerService(); $users = ServerService::getAvailableUsers($server->group_id);
$users = $serverService->getAvailableUsers($server->group_id);
$result = []; $result = [];
foreach ($users as $user) { foreach ($users as $user) {
$user->v2ray_user = [ $user->v2ray_user = [

View File

@ -17,17 +17,6 @@ use Illuminate\Support\Facades\Cache;
*/ */
class ShadowsocksTidalabController extends Controller class ShadowsocksTidalabController extends Controller
{ {
public function __construct(Request $request)
{
$token = $request->input('token');
if (empty($token)) {
throw new ApiException('token is null');
}
if ($token !== admin_setting('server_token')) {
throw new ApiException('token is error');
}
}
// 后端获取用户 // 后端获取用户
public function user(Request $request) public function user(Request $request)
{ {
@ -38,8 +27,7 @@ class ShadowsocksTidalabController extends Controller
return $this->fail([400,'节点不存在']); return $this->fail([400,'节点不存在']);
} }
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_CHECK_AT', $server->id), time(), 3600); Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_CHECK_AT', $server->id), time(), 3600);
$serverService = new ServerService(); $users = ServerService::getAvailableUsers($server->group_id);
$users = $serverService->getAvailableUsers($server->group_id);
$result = []; $result = [];
foreach ($users as $user) { foreach ($users as $user) {
array_push($result, [ array_push($result, [

View File

@ -17,17 +17,7 @@ use Illuminate\Support\Facades\Cache;
*/ */
class TrojanTidalabController extends Controller class TrojanTidalabController extends Controller
{ {
CONST TROJAN_CONFIG = '{"run_type":"server","local_addr":"0.0.0.0","local_port":443,"remote_addr":"www.taobao.com","remote_port":80,"password":[],"ssl":{"cert":"server.crt","key":"server.key","sni":"domain.com"},"api":{"enabled":true,"api_addr":"127.0.0.1","api_port":10000}}'; const TROJAN_CONFIG = '{"run_type":"server","local_addr":"0.0.0.0","local_port":443,"remote_addr":"www.taobao.com","remote_port":80,"password":[],"ssl":{"cert":"server.crt","key":"server.key","sni":"domain.com"},"api":{"enabled":true,"api_addr":"127.0.0.1","api_port":10000}}';
public function __construct(Request $request)
{
$token = $request->input('token');
if (empty($token)) {
throw new ApiException('token is null');
}
if ($token !== admin_setting('server_token')) {
throw new ApiException('token is error');
}
}
// 后端获取用户 // 后端获取用户
public function user(Request $request) public function user(Request $request)
@ -36,11 +26,10 @@ class TrojanTidalabController extends Controller
$nodeId = $request->input('node_id'); $nodeId = $request->input('node_id');
$server = ServerTrojan::find($nodeId); $server = ServerTrojan::find($nodeId);
if (!$server) { if (!$server) {
return $this->fail([400,'节点不存在']); return $this->fail([400, '节点不存在']);
} }
Cache::put(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $server->id), time(), 3600); Cache::put(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $server->id), time(), 3600);
$serverService = new ServerService(); $users = ServerService::getAvailableUsers($server->group_id);
$users = $serverService->getAvailableUsers($server->group_id);
$result = []; $result = [];
foreach ($users as $user) { foreach ($users as $user) {
$user->trojan_user = [ $user->trojan_user = [
@ -50,8 +39,8 @@ class TrojanTidalabController extends Controller
array_push($result, $user); array_push($result, $user);
} }
$eTag = sha1(json_encode($result)); $eTag = sha1(json_encode($result));
if (strpos($request->header('If-None-Match'), $eTag) !== false ) { if (strpos($request->header('If-None-Match'), $eTag) !== false) {
return response(null,304); return response(null, 304);
} }
return response([ return response([
'msg' => 'ok', 'msg' => 'ok',
@ -92,7 +81,7 @@ class TrojanTidalabController extends Controller
$request->validate([ $request->validate([
'node_id' => 'required', 'node_id' => 'required',
'local_port' => 'required' 'local_port' => 'required'
],[ ], [
'node_id.required' => '节点ID不能为空', 'node_id.required' => '节点ID不能为空',
'local_port.required' => '本地端口不能为空' 'local_port.required' => '本地端口不能为空'
]); ]);
@ -100,10 +89,10 @@ class TrojanTidalabController extends Controller
$json = $this->getTrojanConfig($request->input('node_id'), $request->input('local_port')); $json = $this->getTrojanConfig($request->input('node_id'), $request->input('local_port'));
} catch (\Exception $e) { } catch (\Exception $e) {
\Log::error($e); \Log::error($e);
return $this->fail([500,'配置获取失败']); return $this->fail([500, '配置获取失败']);
} }
return(json_encode($json, JSON_UNESCAPED_UNICODE)); return (json_encode($json, JSON_UNESCAPED_UNICODE));
} }
private function getTrojanConfig(int $nodeId, int $localPort) private function getTrojanConfig(int $nodeId, int $localPort)

View File

@ -2,7 +2,6 @@
namespace App\Http\Controllers\V1\Server; namespace App\Http\Controllers\V1\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Services\ServerService; use App\Services\ServerService;
use App\Services\UserService; use App\Services\UserService;
@ -10,40 +9,23 @@ use App\Utils\CacheKey;
use App\Utils\Helper; use App\Utils\Helper;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class UniProxyController extends Controller class UniProxyController extends Controller
{ {
private $nodeType;
private $nodeInfo;
private $nodeId;
private $serverService;
public function __construct(ServerService $serverService, Request $request)
{
$this->serverService = $serverService;
$this->nodeId = $request->input('node_id');
$this->nodeType = $request->input('node_type');
$this->nodeInfo = $this->serverService->getServer($this->nodeId, $this->nodeType);
if(!$this->nodeInfo) {
throw new ApiException('server is not exist', 500);
};
}
// 后端获取用户 // 后端获取用户
public function user(Request $request) public function user(Request $request)
{ {
ini_set('memory_limit', -1); ini_set('memory_limit', -1);
Cache::put(CacheKey::get('SERVER_' . strtoupper($this->nodeType) . '_LAST_CHECK_AT', $this->nodeInfo->id), time(), 3600); Cache::put(CacheKey::get('SERVER_' . strtoupper($request->input('node_type')) . '_LAST_CHECK_AT', $request->input('node_id')), time(), 3600);
$users = $this->serverService->getAvailableUsers($this->nodeInfo->group_id); $users = ServerService::getAvailableUsers($request->input('node_info')->group_id)->toArray();
$users = $users->toArray();
$response['users'] = $users; $response['users'] = $users;
$eTag = sha1(json_encode($response)); $eTag = sha1(json_encode($response));
if (strpos($request->header('If-None-Match'), $eTag) !== false ) { if (strpos($request->header('If-None-Match'), $eTag) !== false) {
return response(null, 304); return response(null, 304);
}; }
return response($response)->header('ETag', "\"{$eTag}\""); return response($response)->header('ETag', "\"{$eTag}\"");
} }
@ -51,14 +33,18 @@ class UniProxyController extends Controller
// 后端提交数据 // 后端提交数据
public function push(Request $request) public function push(Request $request)
{ {
$data = get_request_content(); $data = $request->validate([
$data = json_decode($data, true); "*.0" => 'integer',
"*.1" => 'integer'
]);
$nodeType = $request->input('node_type');
$nodeId = $request->input('node_id');
// 增加单节点多服务器统计在线人数 // 增加单节点多服务器统计在线人数
$ip = $request->ip(); $ip = $request->ip();
$id = $request->input("id"); $id = $request->input("id");
$time = time(); $time = time();
$cacheKey = CacheKey::get('MULTI_SERVER_' . strtoupper($this->nodeType) . '_ONLINE_USER', $this->nodeInfo->id); $cacheKey = CacheKey::get('MULTI_SERVER_' . strtoupper($nodeType) . '_ONLINE_USER', $nodeId);
// 1、获取节点节点在线人数缓存 // 1、获取节点节点在线人数缓存
$onlineUsers = Cache::get($cacheKey) ?? []; $onlineUsers = Cache::get($cacheKey) ?? [];
@ -87,91 +73,92 @@ class UniProxyController extends Controller
Cache::put($cacheKey, $onlineUsers, 3600); Cache::put($cacheKey, $onlineUsers, 3600);
$online_user = $onlineCollection->sum('online_user'); $online_user = $onlineCollection->sum('online_user');
Cache::put(CacheKey::get('SERVER_' . strtoupper($this->nodeType) . '_ONLINE_USER', $this->nodeInfo->id), $online_user, 3600); Cache::put(CacheKey::get('SERVER_' . strtoupper($nodeType) . '_ONLINE_USER', $nodeId), $online_user, 3600);
Cache::put(CacheKey::get('SERVER_' . strtoupper($this->nodeType) . '_LAST_PUSH_AT', $this->nodeInfo->id), time(), 3600); Cache::put(CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_PUSH_AT', $nodeId), time(), 3600);
$userService = new UserService(); $userService = new UserService();
$userService->trafficFetch($this->nodeInfo->toArray(), $this->nodeType, $data , $ip); $userService->trafficFetch($request->input('node_info')->toArray(), $nodeType, $data, $ip);
return $this->success(true); return $this->success(true);
} }
// 后端获取配置 // 后端获取配置
public function config(Request $request) public function config(Request $request)
{ {
switch ($this->nodeType) { $nodeType = $request->input('node_type');
$nodeInfo = $request->input('node_info');
switch ($nodeType) {
case 'shadowsocks': case 'shadowsocks':
$response = [ $response = [
'server_port' => $this->nodeInfo->server_port, 'server_port' => $nodeInfo->server_port,
'cipher' => $this->nodeInfo->cipher, 'cipher' => $nodeInfo->cipher,
'obfs' => $this->nodeInfo->obfs, 'obfs' => $nodeInfo->obfs,
'obfs_settings' => $this->nodeInfo->obfs_settings 'obfs_settings' => $nodeInfo->obfs_settings
]; ];
if ($this->nodeInfo->cipher === '2022-blake3-aes-128-gcm') { if ($nodeInfo->cipher === '2022-blake3-aes-128-gcm') {
$response['server_key'] = Helper::getServerKey($this->nodeInfo->created_at, 16); $response['server_key'] = Helper::getServerKey($nodeInfo->created_at, 16);
} }
if ($this->nodeInfo->cipher === '2022-blake3-aes-256-gcm') { if ($nodeInfo->cipher === '2022-blake3-aes-256-gcm') {
$response['server_key'] = Helper::getServerKey($this->nodeInfo->created_at, 32); $response['server_key'] = Helper::getServerKey($nodeInfo->created_at, 32);
} }
break; break;
case 'vmess': case 'vmess':
$response = [ $response = [
'server_port' => $this->nodeInfo->server_port, 'server_port' => $nodeInfo->server_port,
'network' => $this->nodeInfo->network, 'network' => $nodeInfo->network,
'networkSettings' => $this->nodeInfo->networkSettings, 'networkSettings' => $nodeInfo->networkSettings,
'tls' => $this->nodeInfo->tls 'tls' => $nodeInfo->tls
]; ];
break; break;
case 'trojan': case 'trojan':
$response = [ $response = [
'host' => $this->nodeInfo->host, 'host' => $nodeInfo->host,
'server_port' => $this->nodeInfo->server_port, 'server_port' => $nodeInfo->server_port,
'server_name' => $this->nodeInfo->server_name, 'server_name' => $nodeInfo->server_name,
'network' => $this->nodeInfo->network, 'network' => $nodeInfo->network,
'networkSettings' => $this->nodeInfo->networkSettings, 'networkSettings' => $nodeInfo->networkSettings,
]; ];
break; break;
case 'hysteria': case 'hysteria':
$response = [ $response = [
'version' => $this->nodeInfo->version, 'version' => $nodeInfo->version,
'host' => $this->nodeInfo->host, 'host' => $nodeInfo->host,
'server_port' => $this->nodeInfo->server_port, 'server_port' => $nodeInfo->server_port,
'server_name' => $this->nodeInfo->server_name, 'server_name' => $nodeInfo->server_name,
'up_mbps' => $this->nodeInfo->up_mbps, 'up_mbps' => $nodeInfo->up_mbps,
'down_mbps' => $this->nodeInfo->down_mbps, 'down_mbps' => $nodeInfo->down_mbps,
'obfs' => $this->nodeInfo->is_obfs ? Helper::getServerKey($this->nodeInfo->created_at, 16) : null 'obfs' => $nodeInfo->is_obfs ? Helper::getServerKey($nodeInfo->created_at, 16) : null
]; ];
break; break;
case "vless": case "vless":
$response = [ $response = [
'server_port' => $this->nodeInfo->server_port, 'server_port' => $nodeInfo->server_port,
'network' => $this->nodeInfo->network, 'network' => $nodeInfo->network,
'network_settings' => $this->nodeInfo->network_settings, 'network_settings' => $nodeInfo->network_settings,
'networkSettings' => $this->nodeInfo->network_settings, 'networkSettings' => $nodeInfo->network_settings,
'tls' => $this->nodeInfo->tls, 'tls' => $nodeInfo->tls,
'flow' => $this->nodeInfo->flow, 'flow' => $nodeInfo->flow,
'tls_settings' => $this->nodeInfo->tls_settings 'tls_settings' => $nodeInfo->tls_settings
]; ];
break; break;
} }
$response['base_config'] = [ $response['base_config'] = [
'push_interval' => (int)admin_setting('server_push_interval', 60), 'push_interval' => (int) admin_setting('server_push_interval', 60),
'pull_interval' => (int)admin_setting('server_pull_interval', 60) 'pull_interval' => (int) admin_setting('server_pull_interval', 60)
]; ];
if ($this->nodeInfo['route_id']) { if ($nodeInfo['route_id']) {
$response['routes'] = $this->serverService->getRoutes($this->nodeInfo['route_id']); $response['routes'] = ServerService::getRoutes($nodeInfo['route_id']);
} }
$eTag = sha1(json_encode($response)); $eTag = sha1(json_encode($response));
if (strpos($request->header('If-None-Match'), $eTag) !== false ) { if (strpos($request->header('If-None-Match'), $eTag) !== false) {
return response(null,304); return response(null, 304);
} }
return response($response)->header('ETag', "\"{$eTag}\""); return response($response)->header('ETag', "\"{$eTag}\"");
} }
// 后端提交在线数据 // 后端提交在线数据
public function alive(Request $request) public function alive(Request $request)
{ {
return $this->success(true); return $this->success(true);
} }
} }

View File

@ -73,7 +73,7 @@ class TicketController extends Controller
if (!$ticket) { if (!$ticket) {
return $this->fail([400202,'工单不存在']); return $this->fail([400202,'工单不存在']);
} }
$ticket->status = 1; $ticket->status = Ticket::STATUS_CLOSED;
if (!$ticket->save()) { if (!$ticket->save()) {
return $this->fail([500, '工单关闭失败']); return $this->fail([500, '工单关闭失败']);
} }

View File

@ -8,8 +8,6 @@ use App\Models\User;
use App\Services\ServerService; use App\Services\ServerService;
use App\Services\UserService; use App\Services\UserService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class ServerController extends Controller class ServerController extends Controller
{ {
@ -19,8 +17,7 @@ class ServerController extends Controller
$servers = []; $servers = [];
$userService = new UserService(); $userService = new UserService();
if ($userService->isAvailable($user)) { if ($userService->isAvailable($user)) {
$serverService = new ServerService(); $servers = ServerService::getAvailableServers($user);
$servers = $serverService->getAvailableServers($user);
} }
$eTag = sha1(json_encode(array_column($servers, 'cache_key'))); $eTag = sha1(json_encode(array_column($servers, 'cache_key')));
if (strpos($request->header('If-None-Match'), $eTag) !== false ) { if (strpos($request->header('If-None-Match'), $eTag) !== false ) {

View File

@ -118,7 +118,7 @@ class TicketController extends Controller
if (!$ticket) { if (!$ticket) {
return $this->fail([400, __('Ticket does not exist')]); return $this->fail([400, __('Ticket does not exist')]);
} }
$ticket->status = 1; $ticket->status = Ticket::STATUS_CLOSED;
if (!$ticket->save()) { if (!$ticket->save()) {
return $this->fail([500, __('Close failed')]); return $this->fail([500, __('Close failed')]);
} }

View File

@ -56,7 +56,6 @@ class Kernel extends HttpKernel
* @var array * @var array
*/ */
protected $middlewareAliases = [ protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
@ -83,7 +82,6 @@ class Kernel extends HttpKernel
protected $middlewarePriority = [ protected $middlewarePriority = [
\Illuminate\Session\Middleware\StartSession::class, \Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\Authenticate::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class, \Illuminate\Routing\Middleware\ThrottleRequests::class,
\Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,

View File

@ -1,21 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function redirectTo($request)
{
if (!$request->expectsJson()) {
return route('login');
}
}
}

View File

@ -2,6 +2,8 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use App\Exceptions\ApiException;
use App\Services\ServerService;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -22,27 +24,22 @@ class Server
'hysteria2' => 'hysteria' 'hysteria2' => 'hysteria'
]; ];
$request->validate([ $request->validate([
'token' => ['required','string',function ($attribute, $value, $fail) { 'token' => ['required', 'string', 'in:' . admin_setting('server_token')],
if ($value != admin_setting('server_token')) {
$fail("The $attribute is error.");
}
}],
'node_id' => 'required', 'node_id' => 'required',
'node_type' => [ 'node_type' => [
'nullable',
'string',
'regex:/^(?i)(hysteria|hysteria2|vless|trojan|vmess|v2ray|tuic|shadowsocks|shadowsocks-plugin)$/', 'regex:/^(?i)(hysteria|hysteria2|vless|trojan|vmess|v2ray|tuic|shadowsocks|shadowsocks-plugin)$/',
function ($attribute, $value, $fail)use($aliasTypes) { function ($attribute, $value, $fail) use ($aliasTypes, $request) {
// 将值转换为小写 $request->merge([$attribute => strtolower(isset ($aliasTypes[$value]) ? $aliasTypes[$value] : $value)]);
request()->merge([$attribute => strtolower($value)]);
// 类别别名
if (in_array($value, array_keys($aliasTypes))){
request()->merge([$attribute => $aliasTypes[$value]]);
}
}, },
] ]
], [
'token.in' => 'Token is error!',
'node_type.regex' => 'node_type is error!'
]); ]);
$nodeInfo = ServerService::getServer($request->input('node_id'), $request->input('node_type'));
if (!$nodeInfo)
throw new ApiException('server is not exist!');
$request->merge(['node_info' => $nodeInfo]);
return $next($request); return $next($request);
} }
} }

View File

@ -1,7 +1,10 @@
<?php <?php
namespace App\Http\Routes\V1; namespace App\Http\Routes\V1;
use App\Exceptions\ApiException; use App\Http\Controllers\V1\Server\DeepbworkController;
use App\Http\Controllers\V1\Server\ShadowsocksTidalabController;
use App\Http\Controllers\V1\Server\TrojanTidalabController;
use App\Http\Controllers\V1\Server\UniProxyController;
use Illuminate\Contracts\Routing\Registrar; use Illuminate\Contracts\Routing\Registrar;
class ServerRoute class ServerRoute
@ -12,13 +15,25 @@ class ServerRoute
'prefix' => 'server', 'prefix' => 'server',
'middleware' => 'server' 'middleware' => 'server'
], function ($router) { ], function ($router) {
$router->any('/{class}/{action}', function($class, $action) { $router->prefix('UniProxy')->group(function ($route) {
$controllerClass = "\\App\\Http\\Controllers\\V1\\Server\\" . ucfirst($class) . "Controller"; $route->get('config', [UniProxyController::class, 'config']);
if(!(class_exists($controllerClass) && method_exists($controllerClass, $action))){ $route->get('user', [UniProxyController::class, 'user']);
throw new ApiException('Not Found',404); $route->post('push', [UniProxyController::class, 'push']);
}; $route->post('alive', [UniProxyController::class, 'alive']);
$ctrl = \App::make($controllerClass); });
return \App::call([$ctrl, $action]); $router->prefix('Deepbwork')->group(function ($route) {
$route->get('config', [DeepbworkController::class, 'config']);
$route->get('user', [DeepbworkController::class, 'user']);
$route->post('submit', [DeepbworkController::class, 'submit']);
});
$router->prefix('ShadowsocksTidalab')->group(function ($route) {
$route->get('user', [ShadowsocksTidalabController::class, 'user']);
$route->post('submit', [ShadowsocksTidalabController::class, 'submit']);
});
$router->prefix('TrojanTidalab')->group(function ($route) {
$route->get('config', [TrojanTidalabController::class, 'config']);
$route->get('user', [TrojanTidalabController::class, 'user']);
$route->post('submit', [TrojanTidalabController::class, 'submit']);
}); });
}); });
} }

View File

@ -2,14 +2,12 @@
namespace App\Jobs; namespace App\Jobs;
use App\Services\MailService;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Mail;
use App\Models\MailLog;
class SendEmailJob implements ShouldQueue class SendEmailJob implements ShouldQueue
{ {
@ -36,40 +34,9 @@ class SendEmailJob implements ShouldQueue
*/ */
public function handle() public function handle()
{ {
if (admin_setting('email_host')) { $mailLog = MailService::sendEmail($this->params);
Config::set('mail.host', admin_setting('email_host', config('mail.host'))); if($mailLog['error']){
Config::set('mail.port', admin_setting('email_port', config('mail.port'))); $this->release(); //发送失败将触发重试
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'));
} }
$params = $this->params;
$email = $params['email'];
$subject = $params['subject'];
$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);
}
);
} catch (\Exception $e) {
$error = $e->getMessage();
}
$log = [
'email' => $params['email'],
'subject' => $params['subject'],
'template_name' => $params['template_name'],
'error' => isset($error) ? $error : NULL
];
MailLog::create($log);
$log['config'] = config('mail');
return $log;
} }
} }

View File

@ -2,9 +2,6 @@
namespace App\Jobs; namespace App\Jobs;
use App\Models\User;
use App\Services\MailService;
use App\Services\ServerService;
use App\Services\StatisticalService; use App\Services\StatisticalService;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;

View File

@ -14,4 +14,30 @@ class Order extends Model
'updated_at' => 'timestamp', 'updated_at' => 'timestamp',
'surplus_order_ids' => 'array' 'surplus_order_ids' => 'array'
]; ];
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 => '流量重置',
];
} }

View File

@ -14,6 +14,12 @@ class Ticket extends Model
'updated_at' => 'timestamp' 'updated_at' => 'timestamp'
]; ];
const STATUS_OPENING = 0;
const STATUS_CLOSED = 1;
public static $statusMap = [
self::STATUS_OPENING => '开启',
self::STATUS_CLOSED => '关闭'
];
public function message() public function message()
{ {

View File

@ -7,6 +7,7 @@ namespace App\Payments;
use App\Exceptions\ApiException; use App\Exceptions\ApiException;
class AlipayF2F { class AlipayF2F {
protected $config;
public function __construct($config) public function __construct($config)
{ {
$this->config = $config; $this->config = $config;

View File

@ -1,11 +1,15 @@
<?php <?php
namespace App\Payments; namespace App\Payments;
use App\Exceptions\ApiException; use App\Exceptions\ApiException;
class BTCPay { class BTCPay
public function __construct($config) { {
protected $config;
public function __construct($config)
{
$this->config = $config; $this->config = $config;
} }
@ -35,7 +39,8 @@ class BTCPay {
]; ];
} }
public function pay($order) { public function pay($order)
{
$params = [ $params = [
'jsonResponse' => true, 'jsonResponse' => true,
@ -52,7 +57,7 @@ class BTCPay {
$ret = @json_decode($ret_raw, true); $ret = @json_decode($ret_raw, true);
if(empty($ret['checkoutLink'])) { if (empty($ret['checkoutLink'])) {
throw new ApiException("error!"); throw new ApiException("error!");
} }
return [ return [
@ -61,7 +66,8 @@ class BTCPay {
]; ];
} }
public function notify($params) { public function notify($params)
{
$payload = trim(get_request_content()); $payload = trim(get_request_content());
$headers = getallheaders(); $headers = getallheaders();
@ -76,7 +82,7 @@ class BTCPay {
$computedSignature = "sha256=" . \hash_hmac('sha256', $payload, $this->config['btcpay_webhook_key']); $computedSignature = "sha256=" . \hash_hmac('sha256', $payload, $this->config['btcpay_webhook_key']);
if (!self::hashEqual($signraturHeader, $computedSignature)) { if (!self::hashEqual($signraturHeader, $computedSignature)) {
throw new ApiException('HMAC signature does not match',400); throw new ApiException('HMAC signature does not match', 400);
return false; return false;
} }
@ -93,16 +99,16 @@ class BTCPay {
$out_trade_no = $invoiceDetail['metadata']["orderId"]; $out_trade_no = $invoiceDetail['metadata']["orderId"];
$pay_trade_no=$json_param['invoiceId']; $pay_trade_no = $json_param['invoiceId'];
return [ return [
'trade_no' => $out_trade_no, 'trade_no' => $out_trade_no,
'callback_no' => $pay_trade_no 'callback_no' => $pay_trade_no
]; ];
} }
private function _curlPost($url,$params=false){ private function _curlPost($url, $params = false)
{
$ch = curl_init(); $ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_URL, $url);
@ -111,7 +117,9 @@ class BTCPay {
curl_setopt($ch, CURLOPT_TIMEOUT, 300); curl_setopt($ch, CURLOPT_TIMEOUT, 300);
curl_setopt($ch, CURLOPT_POSTFIELDS, $params); curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
curl_setopt( curl_setopt(
$ch, CURLOPT_HTTPHEADER, array('Authorization:' .'token '.$this->config['btcpay_api_key'], 'Content-Type: application/json') $ch,
CURLOPT_HTTPHEADER,
array('Authorization:' . 'token ' . $this->config['btcpay_api_key'], 'Content-Type: application/json')
); );
$result = curl_exec($ch); $result = curl_exec($ch);
curl_close($ch); curl_close($ch);
@ -143,6 +151,4 @@ class BTCPay {
return !$ret; return !$ret;
} }
} }
} }

View File

@ -1,10 +1,14 @@
<?php <?php
namespace App\Payments; namespace App\Payments;
use App\Exceptions\ApiException; use App\Exceptions\ApiException;
class CoinPayments { class CoinPayments
public function __construct($config) { {
protected $config;
public function __construct($config)
{
$this->config = $config; $this->config = $config;
} }
@ -99,7 +103,7 @@ class CoinPayments {
throw new ApiException('Payment Timed Out or Error'); throw new ApiException('Payment Timed Out or Error');
} else { } else {
//payment is pending, you can optionally add a note to the order page //payment is pending, you can optionally add a note to the order page
return('IPN OK: pending'); return ('IPN OK: pending');
} }
} }
} }

View File

@ -1,10 +1,14 @@
<?php <?php
namespace App\Payments; namespace App\Payments;
use App\Exceptions\ApiException; use App\Exceptions\ApiException;
class Coinbase { class Coinbase
public function __construct($config) { {
protected $config;
public function __construct($config)
{
$this->config = $config; $this->config = $config;
} }
@ -29,7 +33,8 @@ class Coinbase {
]; ];
} }
public function pay($order) { public function pay($order)
{
$params = [ $params = [
'name' => '订阅套餐', 'name' => '订阅套餐',
@ -50,7 +55,7 @@ class Coinbase {
$ret = @json_decode($ret_raw, true); $ret = @json_decode($ret_raw, true);
if(empty($ret['data']['hosted_url'])) { if (empty($ret['data']['hosted_url'])) {
throw new ApiException("error!"); throw new ApiException("error!");
} }
return [ return [
@ -59,7 +64,8 @@ class Coinbase {
]; ];
} }
public function notify($params) { public function notify($params)
{
$payload = trim(get_request_content()); $payload = trim(get_request_content());
$json_param = json_decode($payload, true); $json_param = json_decode($payload, true);
@ -71,20 +77,20 @@ class Coinbase {
$computedSignature = \hash_hmac('sha256', $payload, $this->config['coinbase_webhook_key']); $computedSignature = \hash_hmac('sha256', $payload, $this->config['coinbase_webhook_key']);
if (!self::hashEqual($signatureHeader, $computedSignature)) { if (!self::hashEqual($signatureHeader, $computedSignature)) {
throw new ApiException( 'HMAC signature does not match', 400); throw new ApiException('HMAC signature does not match', 400);
} }
$out_trade_no = $json_param['event']['data']['metadata']['outTradeNo']; $out_trade_no = $json_param['event']['data']['metadata']['outTradeNo'];
$pay_trade_no=$json_param['event']['id']; $pay_trade_no = $json_param['event']['id'];
return [ return [
'trade_no' => $out_trade_no, 'trade_no' => $out_trade_no,
'callback_no' => $pay_trade_no 'callback_no' => $pay_trade_no
]; ];
} }
private function _curlPost($url,$params=false){ private function _curlPost($url, $params = false)
{
$ch = curl_init(); $ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_URL, $url);
@ -93,7 +99,9 @@ class Coinbase {
curl_setopt($ch, CURLOPT_TIMEOUT, 300); curl_setopt($ch, CURLOPT_TIMEOUT, 300);
curl_setopt($ch, CURLOPT_POSTFIELDS, $params); curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
curl_setopt( curl_setopt(
$ch, CURLOPT_HTTPHEADER, array('X-CC-Api-Key:' .$this->config['coinbase_api_key'], 'X-CC-Version: 2018-03-22') $ch,
CURLOPT_HTTPHEADER,
array('X-CC-Api-Key:' . $this->config['coinbase_api_key'], 'X-CC-Version: 2018-03-22')
); );
$result = curl_exec($ch); $result = curl_exec($ch);
curl_close($ch); curl_close($ch);
@ -124,6 +132,4 @@ class Coinbase {
return !$ret; return !$ret;
} }
} }
} }

View File

@ -2,7 +2,9 @@
namespace App\Payments; namespace App\Payments;
class EPay { class EPay
{
protected $config;
public function __construct($config) public function __construct($config)
{ {
$this->config = $config; $this->config = $config;

View File

@ -1,118 +0,0 @@
<?php
/**
* 自己写别抄抄NMB抄
*/
namespace App\Payments;
use App\Exceptions\ApiException;
use Stripe\Source;
use Stripe\Stripe;
class StripeAlipay {
public function __construct($config)
{
$this->config = $config;
}
public function form()
{
return [
'currency' => [
'label' => '货币单位',
'description' => '',
'type' => 'input',
],
'stripe_sk_live' => [
'label' => 'SK_LIVE',
'description' => '',
'type' => 'input',
],
'stripe_webhook_key' => [
'label' => 'WebHook密钥签名',
'description' => '',
'type' => 'input',
]
];
}
public function pay($order)
{
$currency = $this->config['currency'];
$exchange = $this->exchange('CNY', strtoupper($currency));
if (!$exchange) {
throw new ApiException(__('Currency conversion has timed out, please try again later'));
}
Stripe::setApiKey($this->config['stripe_sk_live']);
$source = Source::create([
'amount' => floor($order['total_amount'] * $exchange),
'currency' => $currency,
'type' => 'alipay',
'statement_descriptor' => $order['trade_no'],
'metadata' => [
'user_id' => $order['user_id'],
'out_trade_no' => $order['trade_no'],
'identifier' => ''
],
'redirect' => [
'return_url' => $order['return_url']
]
]);
if (!$source['redirect']['url']) {
throw new ApiException(__('Payment gateway request failed'));
}
return [
'type' => 1,
'data' => $source['redirect']['url']
];
}
public function notify($params)
{
\Stripe\Stripe::setApiKey($this->config['stripe_sk_live']);
try {
$event = \Stripe\Webhook::constructEvent(
get_request_content(),
request()->header('HTTP_STRIPE_SIGNATURE'),
$this->config['stripe_webhook_key']
);
} catch (\Stripe\Error\SignatureVerification $e) {
abort(400);
}
switch ($event->type) {
case 'source.chargeable':
$object = $event->data->object;
\Stripe\Charge::create([
'amount' => $object->amount,
'currency' => $object->currency,
'source' => $object->id,
'metadata' => json_decode($object->metadata, true)
]);
break;
case 'charge.succeeded':
$object = $event->data->object;
if ($object->status === 'succeeded') {
if (!isset($object->metadata->out_trade_no) && !isset($object->source->metadata)) {
return('order error');
}
$metaData = isset($object->metadata->out_trade_no) ? $object->metadata : $object->source->metadata;
$tradeNo = $metaData->out_trade_no;
return [
'trade_no' => $tradeNo,
'callback_no' => $object->id
];
}
break;
default:
throw new ApiException('event is not support');
}
return('success');
}
private function exchange($from, $to)
{
$result = file_get_contents('https://api.exchangerate.host/latest?symbols=' . $to . '&base=' . $from);
$result = json_decode($result, true);
return $result['rates'][$to];
}
}

View File

@ -1,140 +0,0 @@
<?php
namespace App\Payments;
use App\Exceptions\ApiException;
use Stripe\Stripe;
use Stripe\Checkout\Session;
class StripeCheckout {
public function __construct($config)
{
$this->config = $config;
}
public function form()
{
return [
'currency' => [
'label' => '货币单位',
'description' => '',
'type' => 'input',
],
'stripe_sk_live' => [
'label' => 'SK_LIVE',
'description' => 'API 密钥',
'type' => 'input',
],
'stripe_pk_live' => [
'label' => 'PK_LIVE',
'description' => 'API 公钥',
'type' => 'input',
],
'stripe_webhook_key' => [
'label' => 'WebHook 密钥签名',
'description' => '',
'type' => 'input',
],
'stripe_custom_field_name' => [
'label' => '自定义字段名称',
'description' => '例如可设置为“联系方式”,以便及时与客户取得联系',
'type' => 'input',
]
];
}
public function pay($order)
{
$currency = $this->config['currency'];
$exchange = $this->exchange('CNY', strtoupper($currency));
if (!$exchange) {
throw new ApiException(__('Currency conversion has timed out, please try again later'));
}
$customFieldName = isset($this->config['stripe_custom_field_name']) ? $this->config['stripe_custom_field_name'] : 'Contact Infomation';
$params = [
'success_url' => $order['return_url'],
'cancel_url' => $order['return_url'],
'client_reference_id' => $order['trade_no'],
'line_items' => [
[
'price_data' => [
'currency' => $currency,
'product_data' => [
'name' => $order['trade_no']
],
'unit_amount' => floor($order['total_amount'] * $exchange)
],
'quantity' => 1
]
],
'mode' => 'payment',
'invoice_creation' => ['enabled' => true],
'phone_number_collection' => ['enabled' => true],
'custom_fields' => [
[
'key' => 'contactinfo',
'label' => ['type' => 'custom', 'custom' => $customFieldName],
'type' => 'text',
],
],
// 'customer_email' => $user['email'] not support
];
Stripe::setApiKey($this->config['stripe_sk_live']);
try {
$session = Session::create($params);
} catch (\Exception $e) {
info($e);
throw new ApiException("Failed to create order. Error: {$e->getMessage}");
}
return [
'type' => 1, // 0:qrcode 1:url
'data' => $session->url
];
}
public function notify($params)
{
\Stripe\Stripe::setApiKey($this->config['stripe_sk_live']);
try {
$event = \Stripe\Webhook::constructEvent(
get_request_content(),
request()->header('HTTP_STRIPE_SIGNATURE'),
$this->config['stripe_webhook_key']
);
} catch (\Stripe\Error\SignatureVerification $e) {
abort(400);
}
switch ($event->type) {
case 'checkout.session.completed':
$object = $event->data->object;
if ($object->payment_status === 'paid') {
return [
'trade_no' => $object->client_reference_id,
'callback_no' => $object->payment_intent
];
}
break;
case 'checkout.session.async_payment_succeeded':
$object = $event->data->object;
return [
'trade_no' => $object->client_reference_id,
'callback_no' => $object->payment_intent
];
break;
default:
throw new ApiException('event is not support');
}
return('success');
}
private function exchange($from, $to)
{
$result = file_get_contents('https://api.exchangerate.host/latest?symbols=' . $to . '&base=' . $from);
$result = json_decode($result, true);
return $result['rates'][$to];
}
}

View File

@ -1,126 +0,0 @@
<?php
/**
* 自己写别抄抄NMB抄
*/
namespace App\Payments;
use App\Exceptions\ApiException;
use Stripe\Source;
use Stripe\Stripe;
class StripeCredit {
public function __construct($config)
{
$this->config = $config;
}
public function form()
{
return [
'currency' => [
'label' => '货币单位',
'description' => '',
'type' => 'input',
],
'stripe_sk_live' => [
'label' => 'SK_LIVE',
'description' => '',
'type' => 'input',
],
'stripe_pk_live' => [
'label' => 'PK_LIVE',
'description' => '',
'type' => 'input',
],
'stripe_webhook_key' => [
'label' => 'WebHook密钥签名',
'description' => '',
'type' => 'input',
]
];
}
public function pay($order)
{
info($order);
$currency = $this->config['currency'];
$exchange = $this->exchange('CNY', strtoupper($currency));
if (!$exchange) {
throw new ApiException(__('Currency conversion has timed out, please try again later'));
}
Stripe::setApiKey($this->config['stripe_sk_live']);
try {
$charge = \Stripe\Charge::create([
'amount' => floor($order['total_amount'] * $exchange),
'currency' => $currency,
'source' => $order['stripe_token'],
'metadata' => [
'user_id' => $order['user_id'],
'out_trade_no' => $order['trade_no'],
'identifier' => ''
]
]);
} catch (\Exception $e) {
info($e);
throw new ApiException(__('Payment failed. Please check your credit card information'));
}
if (!$charge->paid) {
throw new ApiException(__('Payment failed. Please check your credit card information'));
}
return [
'type' => 2,
'data' => $charge->paid
];
}
public function notify($params)
{
\Stripe\Stripe::setApiKey($this->config['stripe_sk_live']);
try {
$event = \Stripe\Webhook::constructEvent(
get_request_content(),
request()->header('HTTP_STRIPE_SIGNATURE'),
$this->config['stripe_webhook_key']
);
} catch (\Stripe\Error\SignatureVerification $e) {
\Log::error($e);
abort(400);
}
switch ($event->type) {
case 'source.chargeable':
$object = $event->data->object;
\Stripe\Charge::create([
'amount' => $object->amount,
'currency' => $object->currency,
'source' => $object->id,
'metadata' => json_decode($object->metadata, true)
]);
break;
case 'charge.succeeded':
$object = $event->data->object;
if ($object->status === 'succeeded') {
if (!isset($object->metadata->out_trade_no) && !isset($object->source->metadata)) {
return('order error');
}
$metaData = isset($object->metadata->out_trade_no) ? $object->metadata : $object->source->metadata;
$tradeNo = $metaData->out_trade_no;
return [
'trade_no' => $tradeNo,
'callback_no' => $object->id
];
}
break;
default:
throw new ApiException('event is not support');
}
return('success');
}
private function exchange($from, $to)
{
$result = file_get_contents('https://api.exchangerate.host/latest?symbols=' . $to . '&base=' . $from);
$result = json_decode($result, true);
return $result['rates'][$to];
}
}

View File

@ -1,118 +0,0 @@
<?php
/**
* 自己写别抄抄NMB抄
*/
namespace App\Payments;
use App\Exceptions\ApiException;
use Stripe\Source;
use Stripe\Stripe;
class StripeWepay {
public function __construct($config)
{
$this->config = $config;
}
public function form()
{
return [
'currency' => [
'label' => '货币单位',
'description' => '',
'type' => 'input',
],
'stripe_sk_live' => [
'label' => 'SK_LIVE',
'description' => '',
'type' => 'input',
],
'stripe_webhook_key' => [
'label' => 'WebHook密钥签名',
'description' => '',
'type' => 'input',
]
];
}
public function pay($order)
{
$currency = $this->config['currency'];
$exchange = $this->exchange('CNY', strtoupper($currency));
if (!$exchange) {
throw new ApiException(__('Currency conversion has timed out, please try again later'));
}
Stripe::setApiKey($this->config['stripe_sk_live']);
$source = Source::create([
'amount' => floor($order['total_amount'] * $exchange),
'currency' => $currency,
'type' => 'wechat',
'statement_descriptor' => $order['trade_no'],
'metadata' => [
'user_id' => $order['user_id'],
'out_trade_no' => $order['trade_no'],
'identifier' => ''
],
'redirect' => [
'return_url' => $order['return_url']
]
]);
if (!$source['wechat']['qr_code_url']) {
throw new ApiException(__('Payment gateway request failed'));
}
return [
'type' => 0,
'data' => $source['wechat']['qr_code_url']
];
}
public function notify($params)
{
\Stripe\Stripe::setApiKey($this->config['stripe_sk_live']);
try {
$event = \Stripe\Webhook::constructEvent(
get_request_content(),
request()->header('HTTP_STRIPE_SIGNATURE'),
$this->config['stripe_webhook_key']
);
} catch (\Stripe\Error\SignatureVerification $e) {
abort(400);
}
switch ($event->type) {
case 'source.chargeable':
$object = $event->data->object;
\Stripe\Charge::create([
'amount' => $object->amount,
'currency' => $object->currency,
'source' => $object->id,
'metadata' => json_decode($object->metadata, true)
]);
break;
case 'charge.succeeded':
$object = $event->data->object;
if ($object->status === 'succeeded') {
if (!isset($object->metadata->out_trade_no) && !isset($object->source->metadata)) {
return('order error');
}
$metaData = isset($object->metadata->out_trade_no) ? $object->metadata : $object->source->metadata;
$tradeNo = $metaData->out_trade_no;
return [
'trade_no' => $tradeNo,
'callback_no' => $object->id
];
}
break;
default:
throw new ApiException('event is not support');
}
return('success');
}
private function exchange($from, $to)
{
$result = file_get_contents('https://api.exchangerate.host/latest?symbols=' . $to . '&base=' . $from);
$result = json_decode($result, true);
return $result['rates'][$to];
}
}

View File

@ -1,85 +0,0 @@
<?php
namespace App\Payments;
use App\Exceptions\ApiException;
use Omnipay\Omnipay;
use Omnipay\WechatPay\Helper;
class WechatPayNative {
public function __construct($config)
{
$this->config = $config;
}
public function form()
{
return [
'app_id' => [
'label' => 'APPID',
'description' => '绑定微信支付商户的APPID',
'type' => 'input',
],
'mch_id' => [
'label' => '商户号',
'description' => '微信支付商户号',
'type' => 'input',
],
'api_key' => [
'label' => 'APIKEY(v1)',
'description' => '',
'type' => 'input',
]
];
}
public function pay($order)
{
$gateway = Omnipay::create('WechatPay_Native');
$gateway->setAppId($this->config['app_id']);
$gateway->setMchId($this->config['mch_id']);
$gateway->setApiKey($this->config['api_key']);
$gateway->setNotifyUrl($order['notify_url']);
$params = [
'body' => $order['trade_no'],
'out_trade_no' => $order['trade_no'],
'total_fee' => $order['total_amount'],
'spbill_create_ip' => '0.0.0.0',
'fee_type' => 'CNY'
];
$request = $gateway->purchase($params);
$response = $request->send();
$response = $response->getData();
if ($response['return_code'] !== 'SUCCESS') {
throw new ApiException($response['return_msg']);
}
return [
'type' => 0,
'data' => $response['code_url'],
'custom_result' => '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>'
];
}
public function notify($params)
{
$data = Helper::xml2array(get_request_content());
$gateway = Omnipay::create('WechatPay');
$gateway->setAppId($this->config['app_id']);
$gateway->setMchId($this->config['mch_id']);
$gateway->setApiKey($this->config['api_key']);
$response = $gateway->completePurchase([
'request_params' => get_request_content()
])->send();
if (!$response->isPaid()) {
return('FAIL');
}
return [
'trade_no' => $data['out_trade_no'],
'callback_no' => $data['transaction_id']
];
}
}

View File

@ -4,6 +4,7 @@ namespace App\Providers;
use App\Support\Setting; use App\Support\Setting;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Foundation\Application;
class SettingServiceProvider extends ServiceProvider class SettingServiceProvider extends ServiceProvider
{ {
@ -14,9 +15,11 @@ class SettingServiceProvider extends ServiceProvider
*/ */
public function register() public function register()
{ {
$this->app->bind('setting', function ($app) {
return Setting::fromDatabase(); // 假设 AdminSetting 是您的设置类 $this->app->bind(Setting::class, function (Application $app) {
return new Setting();
}); });
} }
/** /**
@ -26,6 +29,5 @@ class SettingServiceProvider extends ServiceProvider
*/ */
public function boot() public function boot()
{ {
//
} }
} }

View File

@ -9,13 +9,17 @@ use Illuminate\Support\Facades\Cache;
class MailService class MailService
{ {
public function remindTraffic (User $user) public function remindTraffic(User $user)
{ {
if (!$user->remind_traffic) return; if (!$user->remind_traffic)
if (!$this->remindTrafficIsWarnValue($user->u, $user->d, $user->transfer_enable)) return; return;
if (!$this->remindTrafficIsWarnValue($user->u, $user->d, $user->transfer_enable))
return;
$flag = CacheKey::get('LAST_SEND_EMAIL_REMIND_TRAFFIC', $user->id); $flag = CacheKey::get('LAST_SEND_EMAIL_REMIND_TRAFFIC', $user->id);
if (Cache::get($flag)) return; if (Cache::get($flag))
if (!Cache::put($flag, 1, 24 * 3600)) return; return;
if (!Cache::put($flag, 1, 24 * 3600))
return;
SendEmailJob::dispatch([ SendEmailJob::dispatch([
'email' => $user->email, 'email' => $user->email,
'subject' => __('The traffic usage in :app_name has reached 80%', [ 'subject' => __('The traffic usage in :app_name has reached 80%', [
@ -31,11 +35,12 @@ class MailService
public function remindExpire(User $user) public function remindExpire(User $user)
{ {
if (!($user->expired_at !== NULL && ($user->expired_at - 86400) < time() && $user->expired_at > time())) return; if (!($user->expired_at !== NULL && ($user->expired_at - 86400) < time() && $user->expired_at > time()))
return;
SendEmailJob::dispatch([ SendEmailJob::dispatch([
'email' => $user->email, 'email' => $user->email,
'subject' => __('The service in :app_name is about to expire', [ 'subject' => __('The service in :app_name is about to expire', [
'app_name' => admin_setting('app_name', 'XBoard') 'app_name' => admin_setting('app_name', 'XBoard')
]), ]),
'template_name' => 'remindExpire', 'template_name' => 'remindExpire',
'template_value' => [ 'template_value' => [
@ -48,11 +53,67 @@ class MailService
private function remindTrafficIsWarnValue($u, $d, $transfer_enable) private function remindTrafficIsWarnValue($u, $d, $transfer_enable)
{ {
$ud = $u + $d; $ud = $u + $d;
if (!$ud) return false; if (!$ud)
if (!$transfer_enable) return false; return false;
if (!$transfer_enable)
return false;
$percentage = ($ud / $transfer_enable) * 100; $percentage = ($ud / $transfer_enable) * 100;
if ($percentage < 80) return false; if ($percentage < 80)
if ($percentage >= 100) return false; return false;
if ($percentage >= 100)
return false;
return true; 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'];
$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) {
$error = $e->getMessage();
}
$log = [
'email' => $params['email'],
'subject' => $params['subject'],
'template_name' => $params['template_name'],
'error' => $error,
'config' => config('mail')
];
\App\Models\MailLog::create($log);
return $log;
}
} }

View File

@ -40,7 +40,7 @@ class OrderService
DB::beginTransaction(); DB::beginTransaction();
if ($order->surplus_order_ids) { if ($order->surplus_order_ids) {
Order::whereIn('id', $order->surplus_order_ids)->update([ Order::whereIn('id', $order->surplus_order_ids)->update([
'status' => 4 'status' => Order::STATUS_DISCOUNTED
]); ]);
} }
switch ((string)$order->period) { switch ((string)$order->period) {
@ -71,7 +71,7 @@ class OrderService
if (!$this->user->save()) { if (!$this->user->save()) {
throw new \Exception('用户信息保存失败'); throw new \Exception('用户信息保存失败');
} }
$order->status = 3; $order->status = Order::STATUS_COMPLETED;
if (!$order->save()) { if (!$order->save()) {
throw new \Exception('订单信息保存失败'); throw new \Exception('订单信息保存失败');
} }
@ -88,10 +88,10 @@ class OrderService
{ {
$order = $this->order; $order = $this->order;
if ($order->period === 'reset_price') { if ($order->period === 'reset_price') {
$order->type = 4; $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)) { } 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('目前不允许更改订阅,请联系客服或提交工单操作'); if (!(int)admin_setting('plan_change_enable', 1)) throw new ApiException('目前不允许更改订阅,请联系客服或提交工单操作');
$order->type = 3; $order->type = Order::TYPE_UPGRADE;
if ((int)admin_setting('surplus_enable', 1)) $this->getSurplusValue($user, $order); if ((int)admin_setting('surplus_enable', 1)) $this->getSurplusValue($user, $order);
if ($order->surplus_amount >= $order->total_amount) { if ($order->surplus_amount >= $order->total_amount) {
$order->refund_amount = $order->surplus_amount - $order->total_amount; $order->refund_amount = $order->surplus_amount - $order->total_amount;
@ -100,9 +100,9 @@ class OrderService
$order->total_amount = $order->total_amount - $order->surplus_amount; $order->total_amount = $order->total_amount - $order->surplus_amount;
} }
} else if ($user->expired_at > time() && $order->plan_id == $user->plan_id) { // 用户订阅未过期且购买订阅与当前订阅相同 === 续费 } else if ($user->expired_at > time() && $order->plan_id == $user->plan_id) { // 用户订阅未过期且购买订阅与当前订阅相同 === 续费
$order->type = 2; $order->type = Order::TYPE_RENEWAL;
} else { // 新购 } else { // 新购
$order->type = 1; $order->type = Order::TYPE_NEW_PURCHASE;
} }
} }
@ -215,8 +215,8 @@ class OrderService
public function paid(string $callbackNo) public function paid(string $callbackNo)
{ {
$order = $this->order; $order = $this->order;
if ($order->status !== 0) return true; if ($order->status !== Order::STATUS_PENDING) return true;
$order->status = 1; $order->status = Order::STATUS_PROCESSING;
$order->paid_at = time(); $order->paid_at = time();
$order->callback_no = $callbackNo; $order->callback_no = $callbackNo;
if (!$order->save()) return false; if (!$order->save()) return false;
@ -233,7 +233,7 @@ class OrderService
$order = $this->order; $order = $this->order;
try { try {
DB::beginTransaction(); DB::beginTransaction();
$order->status = 2; $order->status = Order::STATUS_CANCELLED;
if (!$order->save()) { if (!$order->save()) {
throw new \Exception('Failed to save order status.'); throw new \Exception('Failed to save order status.');
} }

View File

@ -17,7 +17,8 @@ use Illuminate\Support\Facades\Cache;
class ServerService class ServerService
{ {
public function getAvailableVless(User $user):array // 获取可用的 VLESS 服务器列表
public static function getAvailableVless(User $user): array
{ {
$servers = []; $servers = [];
$model = ServerVless::orderBy('sort', 'ASC'); $model = ServerVless::orderBy('sort', 'ASC');
@ -41,15 +42,15 @@ class ServerService
unset($serverData['tls_settings']['private_key']); unset($serverData['tls_settings']['private_key']);
} }
} }
$servers[] = $serverData; $servers[] = $serverData;
} }
return $servers; return $servers;
} }
public function getAvailableVmess(User $user):array // 获取可用的 VMESS 服务器列表
public static function getAvailableVmess(User $user): array
{ {
$servers = []; $servers = [];
$model = ServerVmess::orderBy('sort', 'ASC'); $model = ServerVmess::orderBy('sort', 'ASC');
@ -69,11 +70,11 @@ class ServerService
$servers[] = $vmess[$key]->toArray(); $servers[] = $vmess[$key]->toArray();
} }
return $servers; return $servers;
} }
public function getAvailableTrojan(User $user):array // 获取可用的 TROJAN 服务器列表
public static function getAvailableTrojan(User $user): array
{ {
$servers = []; $servers = [];
$model = ServerTrojan::orderBy('sort', 'ASC'); $model = ServerTrojan::orderBy('sort', 'ASC');
@ -95,7 +96,8 @@ class ServerService
return $servers; return $servers;
} }
public function getAvailableHysteria(User $user) // 获取可用的 HYSTERIA 服务器列表
public static function getAvailableHysteria(User $user)
{ {
$availableServers = []; $availableServers = [];
$model = ServerHysteria::orderBy('sort', 'ASC'); $model = ServerHysteria::orderBy('sort', 'ASC');
@ -119,7 +121,8 @@ class ServerService
return $availableServers; return $availableServers;
} }
public function getAvailableShadowsocks(User $user) // 获取可用的 SHADOWSOCKS 服务器列表
public static function getAvailableShadowsocks(User $user)
{ {
$servers = []; $servers = [];
$model = ServerShadowsocks::orderBy('sort', 'ASC'); $model = ServerShadowsocks::orderBy('sort', 'ASC');
@ -141,15 +144,16 @@ class ServerService
return $servers; return $servers;
} }
public function getAvailableServers(User $user) // 获取可用的服务器列表
public static function getAvailableServers(User $user)
{ {
$servers = Cache::remember('serversAvailable_'. $user->id, 5, function() use($user){ $servers = Cache::remember('serversAvailable_'. $user->id, 5, function() use($user){
return array_merge( return array_merge(
$this->getAvailableShadowsocks($user), self::getAvailableShadowsocks($user),
$this->getAvailableVmess($user), self::getAvailableVmess($user),
$this->getAvailableTrojan($user), self::getAvailableTrojan($user),
$this->getAvailableHysteria($user), self::getAvailableHysteria($user),
$this->getAvailableVless($user) self::getAvailableVless($user)
); );
}); });
$tmp = array_column($servers, 'sort'); $tmp = array_column($servers, 'sort');
@ -162,7 +166,8 @@ class ServerService
}, $servers); }, $servers);
} }
public function getAvailableUsers($groupId): Collection // 获取可用的用户列表
public static function getAvailableUsers($groupId): Collection
{ {
return \DB::table('v2_user') return \DB::table('v2_user')
->whereIn('group_id', $groupId) ->whereIn('group_id', $groupId)
@ -180,7 +185,8 @@ class ServerService
->get(); ->get();
} }
public function log(int $userId, int $serverId, int $u, int $d, float $rate, string $method) // 记录流量日志
public static function log(int $userId, int $serverId, int $u, int $d, float $rate, string $method)
{ {
if (($u + $d) < 10240) return true; if (($u + $d) < 10240) return true;
$timestamp = strtotime(date('Y-m-d')); $timestamp = strtotime(date('Y-m-d'));
@ -212,7 +218,8 @@ class ServerService
} }
} }
public function getAllShadowsocks() // 获取所有 SHADOWSOCKS 服务器列表
public static function getAllShadowsocks()
{ {
$servers = ServerShadowsocks::orderBy('sort', 'ASC') $servers = ServerShadowsocks::orderBy('sort', 'ASC')
->get() ->get()
@ -223,7 +230,8 @@ class ServerService
return $servers; return $servers;
} }
public function getAllVMess() // 获取所有 VMESS 服务器列表
public static function getAllVMess()
{ {
$servers = ServerVmess::orderBy('sort', 'ASC') $servers = ServerVmess::orderBy('sort', 'ASC')
->get() ->get()
@ -234,7 +242,8 @@ class ServerService
return $servers; return $servers;
} }
public function getAllVLess() // 获取所有 VLESS 服务器列表
public static function getAllVLess()
{ {
$servers = ServerVless::orderBy('sort', 'ASC') $servers = ServerVless::orderBy('sort', 'ASC')
->get() ->get()
@ -245,7 +254,8 @@ class ServerService
return $servers; return $servers;
} }
public function getAllTrojan() // 获取所有 TROJAN 服务器列表
public static function getAllTrojan()
{ {
$servers = ServerTrojan::orderBy('sort', 'ASC') $servers = ServerTrojan::orderBy('sort', 'ASC')
->get() ->get()
@ -256,7 +266,8 @@ class ServerService
return $servers; return $servers;
} }
public function getAllHysteria() // 获取所有 HYSTERIA 服务器列表
public static function getAllHysteria()
{ {
$servers = ServerHysteria::orderBy('sort', 'ASC') $servers = ServerHysteria::orderBy('sort', 'ASC')
->get() ->get()
@ -267,7 +278,8 @@ class ServerService
return $servers; return $servers;
} }
private function mergeData(&$servers) // 合并数据
private static function mergeData(&$servers)
{ {
foreach ($servers as $k => $v) { foreach ($servers as $k => $v) {
$serverType = strtoupper($v['type']); $serverType = strtoupper($v['type']);
@ -291,22 +303,24 @@ class ServerService
} }
} }
public function getAllServers() // 获取所有服务器列表
public static function getAllServers()
{ {
$servers = array_merge( $servers = array_merge(
$this->getAllShadowsocks(), self::getAllShadowsocks(),
$this->getAllVMess(), self::getAllVMess(),
$this->getAllTrojan(), self::getAllTrojan(),
$this->getAllHysteria(), self::getAllHysteria(),
$this->getAllVLess() self::getAllVLess()
); );
$this->mergeData($servers); self::mergeData($servers);
$tmp = array_column($servers, 'sort'); $tmp = array_column($servers, 'sort');
array_multisort($tmp, SORT_ASC, $servers); array_multisort($tmp, SORT_ASC, $servers);
return $servers; return $servers;
} }
public function getRoutes(array $routeIds) // 获取路由规则
public static function getRoutes(array $routeIds)
{ {
$routes = ServerRoute::select(['id', 'match', 'action', 'action_value'])->whereIn('id', $routeIds)->get(); $routes = ServerRoute::select(['id', 'match', 'action', 'action_value'])->whereIn('id', $routeIds)->get();
// TODO: remove on 1.8.0 // TODO: remove on 1.8.0
@ -318,7 +332,8 @@ class ServerService
return $routes; return $routes;
} }
public function getServer($serverId, $serverType) // 获取服务器
public static function getServer($serverId, $serverType)
{ {
switch ($serverType) { switch ($serverType) {
case 'vmess': case 'vmess':
@ -336,8 +351,8 @@ class ServerService
} }
} }
// 根据节点IP和父级别节点ID查询节点 // 根据节点IP和父级别节点ID查询节点
public function getChildServer($serverId, $serverType, $nodeIp){ public static function getChildServer($serverId, $serverType, $nodeIp){
switch ($serverType) { switch ($serverType) {
case 'vmess': case 'vmess':
return ServerVmess::query() return ServerVmess::query()

View File

@ -21,9 +21,9 @@ class TicketService {
'message' => $message 'message' => $message
]); ]);
if ($userId !== $ticket->user_id) { if ($userId !== $ticket->user_id) {
$ticket->reply_status = 0; $ticket->reply_status = Ticket::STATUS_OPENING;
} else { } else {
$ticket->reply_status = 1; $ticket->reply_status = Ticket::STATUS_CLOSED;
} }
if (!$ticketMessage || !$ticket->save()) { if (!$ticketMessage || !$ticket->save()) {
throw new \Exception(); throw new \Exception();
@ -43,7 +43,7 @@ class TicketService {
if (!$ticket) { if (!$ticket) {
throw new ApiException('工单不存在'); throw new ApiException('工单不存在');
} }
$ticket->status = 0; $ticket->status = Ticket::STATUS_OPENING;
try{ try{
DB::beginTransaction(); DB::beginTransaction();
$ticketMessage = TicketMessage::create([ $ticketMessage = TicketMessage::create([
@ -52,9 +52,9 @@ class TicketService {
'message' => $message 'message' => $message
]); ]);
if ($userId !== $ticket->user_id) { if ($userId !== $ticket->user_id) {
$ticket->reply_status = 0; $ticket->reply_status = Ticket::STATUS_OPENING;
} else { } else {
$ticket->reply_status = 1; $ticket->reply_status = Ticket::STATUS_CLOSED;
} }
if (!$ticketMessage || !$ticket->save()) { if (!$ticketMessage || !$ticket->save()) {
throw new ApiException('工单回复失败'); throw new ApiException('工单回复失败');

View File

@ -2,8 +2,6 @@
namespace App\Services; namespace App\Services;
use App\Jobs\StatServerJob;
use App\Jobs\StatUserJob;
use App\Jobs\TrafficFetchJob; use App\Jobs\TrafficFetchJob;
use App\Models\Order; use App\Models\Order;
use App\Models\Plan; use App\Models\Plan;
@ -172,7 +170,7 @@ class UserService
{ {
// 获取子节点 // 获取子节点
$childServer = ($server['parent_id'] == null && !blank($nodeIp)) $childServer = ($server['parent_id'] == null && !blank($nodeIp))
? (new ServerService())->getChildServer($server['id'], $protocol, $nodeIp) ? ServerService::getChildServer($server['id'], $protocol, $nodeIp)
: null; : null;
$timestamp = strtotime(date('Y-m-d')); $timestamp = strtotime(date('Y-m-d'));
collect($data)->chunk(1000)->each(function($chunk) use ($timestamp,$server,$protocol, $childServer){ collect($data)->chunk(1000)->each(function($chunk) use ($timestamp,$server,$protocol, $childServer){

View File

@ -10,6 +10,10 @@ use Illuminate\Support\Fluent;
class Setting extends Fluent class Setting extends Fluent
{ {
public function __construct()
{
$this->attributes = self::fromDatabase();
}
/** /**
* 获取配置,并转化为数组. * 获取配置,并转化为数组.
* *
@ -129,21 +133,12 @@ class Setting extends Fluent
public static function fromDatabase() public static function fromDatabase()
{ {
$values = []; $values = [];
try { try {
if(env('ADMIN_SETTING_CACHE') > 0){ $values = Cache::remember('admin_settings', env('ADMIN_SETTING_CACHE', 0), function () {
$values = Cache::remember('admin_settings', env('ADMIN_SETTING_CACHE'), function () { return SettingModel::pluck('value', 'name')->toArray();
return SettingModel::pluck('value', 'name')->toArray(); });
}
);
}else{
$values = SettingModel::pluck('value', 'name')->toArray();
}
} catch (QueryException $e) { } catch (QueryException $e) {
return new static($values);
// throw new \Exception('配置获取失败、请检查数据库配置');
} }
return $values;
return new static($values);
} }
} }

View File

@ -22,10 +22,8 @@
"joanhey/adapterman": "^0.6.1", "joanhey/adapterman": "^0.6.1",
"laravel/framework": "^10.0", "laravel/framework": "^10.0",
"laravel/horizon": "^5.9.6", "laravel/horizon": "^5.9.6",
"laravel/pulse": "*",
"laravel/tinker": "^2.5", "laravel/tinker": "^2.5",
"linfo/linfo": "^4.0", "linfo/linfo": "^4.0",
"nunomaduro/collision": "^7.10",
"paragonie/sodium_compat": "^1.20", "paragonie/sodium_compat": "^1.20",
"php-curl-class/php-curl-class": "^8.6", "php-curl-class/php-curl-class": "^8.6",
"spatie/db-dumper": "^3.4", "spatie/db-dumper": "^3.4",
@ -39,6 +37,7 @@
"barryvdh/laravel-debugbar": "^3.9", "barryvdh/laravel-debugbar": "^3.9",
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",
"mockery/mockery": "^1.3.1", "mockery/mockery": "^1.3.1",
"nunomaduro/collision": "^7.10",
"orangehill/iseed": "^3.0", "orangehill/iseed": "^3.0",
"phpunit/phpunit": "^10.0", "phpunit/phpunit": "^10.0",
"spatie/laravel-ignition": "^2.0" "spatie/laravel-ignition": "^2.0"