Merge pull request #300 from v2board/dev

1.3.2
This commit is contained in:
tokumeikoi 2020-08-30 00:55:40 +08:00 committed by GitHub
commit c1f0521955
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 701 additions and 314 deletions

View File

@ -117,11 +117,13 @@ class CheckOrder extends Command
$user->expired_at = time(); $user->expired_at = time();
} }
$user->transfer_enable = $plan->transfer_enable * 1073741824; $user->transfer_enable = $plan->transfer_enable * 1073741824;
// 当续费清空流量或用户先前是一次性订阅
if ((int)config('v2board.renew_reset_traffic_enable', 1) || $user->expired_at === NULL) { // 续费重置&类型=续费
$user->u = 0; if ((int)config('v2board.renew_reset_traffic_enable', 1) && $order->type === 2) $this->buyReset($user);
$user->d = 0; // 购买前用户过期为NULL一次性
} if ($user->expired_at === NULL) $this->buyReset($user);
// 新购
if ($order->type === 1) $this->buyReset($user);
$user->plan_id = $plan->id; $user->plan_id = $plan->id;
$user->group_id = $plan->group_id; $user->group_id = $plan->group_id;
$user->expired_at = $this->getTime($order->cycle, $user->expired_at); $user->expired_at = $this->getTime($order->cycle, $user->expired_at);

View File

@ -7,6 +7,7 @@ use App\Models\User;
class ResetTraffic extends Command class ResetTraffic extends Command
{ {
protected $user;
/** /**
* The name and signature of the console command. * The name and signature of the console command.
* *
@ -29,6 +30,8 @@ class ResetTraffic extends Command
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
$this->user = User::where('expired_at', '!=', NULL)
->where('expired_at', '>', time());
} }
/** /**
@ -38,23 +41,22 @@ class ResetTraffic extends Command
*/ */
public function handle() public function handle()
{ {
$user = User::where('expired_at', '!=', NULL)
->where('expired_at', '>', time());
$resetTrafficMethod = config('v2board.reset_traffic_method', 0); $resetTrafficMethod = config('v2board.reset_traffic_method', 0);
switch ((int)$resetTrafficMethod) { switch ((int)$resetTrafficMethod) {
// 1 a month // 1 a month
case 0: case 0:
$this->resetByMonthFirstDay($user); $this->resetByMonthFirstDay();
break; break;
// expire day // expire day
case 1: case 1:
$this->resetByExpireDay($user); $this->resetByExpireDay();
break; break;
} }
} }
private function resetByMonthFirstDay($user):void private function resetByMonthFirstDay($user):void
{ {
$user = $this->user;
if ((string)date('d') === '01') { if ((string)date('d') === '01') {
$user->update([ $user->update([
'u' => 0, 'u' => 0,
@ -63,8 +65,9 @@ class ResetTraffic extends Command
} }
} }
private function resetByExpireDay($user):void private function resetByExpireDay():void
{ {
$user = $this->user;
$lastDay = date('d', strtotime('last day of +0 months')); $lastDay = date('d', strtotime('last day of +0 months'));
$users = []; $users = [];
foreach ($user->get() as $item) { foreach ($user->get() as $item) {

View File

@ -31,7 +31,7 @@ class Kernel extends ConsoleKernel
$schedule->command('check:commission')->everyMinute(); $schedule->command('check:commission')->everyMinute();
// reset // reset
$schedule->command('reset:traffic')->daily(); $schedule->command('reset:traffic')->daily();
$schedule->command('reset:serverLog')->monthly(); $schedule->command('reset:serverLog')->quarterly();
// send // send
$schedule->command('send:remindMail')->dailyAt('11:30'); $schedule->command('send:remindMail')->dailyAt('11:30');
} }

View File

@ -82,31 +82,47 @@ class ConfigController extends Controller
'stripe_webhook_key' => config('v2board.stripe_webhook_key'), 'stripe_webhook_key' => config('v2board.stripe_webhook_key'),
'stripe_currency' => config('v2board.stripe_currency', 'hkd'), 'stripe_currency' => config('v2board.stripe_currency', 'hkd'),
// bitpayx // bitpayx
'bitpayx_name' => config('v2board.bitpayx_name', '聚合支付'), 'bitpayx_name' => config('v2board.bitpayx_name', '在线支付'),
'bitpayx_enable' => (int)config('v2board.bitpayx_enable', 0), 'bitpayx_enable' => (int)config('v2board.bitpayx_enable', 0),
'bitpayx_appsecret' => config('v2board.bitpayx_appsecret'), 'bitpayx_appsecret' => config('v2board.bitpayx_appsecret'),
// paytaro // mGate
'paytaro_name' => config('v2board.paytaro_name', '聚合支付'), 'mgate_name' => config('v2board.mgate_name', '在线支付'),
'paytaro_enable' => (int)config('v2board.paytaro_enable', 0), 'mgate_enable' => (int)config('v2board.mgate_enable', 0),
'paytaro_app_id' => config('v2board.paytaro_app_id'), 'mgate_url' => config('v2board.mgate_url'),
'paytaro_app_secret' => config('v2board.paytaro_app_secret') 'mgate_app_id' => config('v2board.mgate_app_id'),
'mgate_app_secret' => config('v2board.mgate_app_secret'),
// Epay
'epay_name' => config('v2board.epay_name', '在线支付'),
'epay_enable' => (int)config('v2board.epay_enable', 0),
'epay_url' => config('v2board.epay_url'),
'epay_pid' => config('v2board.epay_pid'),
'epay_key' => config('v2board.epay_key'),
], ],
'frontend' => [ 'frontend' => [
'frontend_theme_sidebar' => config('v2board.frontend_theme_sidebar', 'light'), 'frontend_theme_sidebar' => config('v2board.frontend_theme_sidebar', 'light'),
'frontend_theme_header' => config('v2board.frontend_theme_header', 'dark'), 'frontend_theme_header' => config('v2board.frontend_theme_header', 'dark'),
'frontend_theme_color' => config('v2board.frontend_theme_color', 'default'), 'frontend_theme_color' => config('v2board.frontend_theme_color', 'default'),
'frontend_background_url' => config('v2board.frontend_background_url') 'frontend_background_url' => config('v2board.frontend_background_url'),
'frontend_admin_path' => config('v2board.frontend_admin_path', 'admin')
], ],
'server' => [ 'server' => [
'server_token' => config('v2board.server_token'), 'server_token' => config('v2board.server_token'),
'server_license' => config('v2board.server_license'), 'server_license' => config('v2board.server_license'),
'server_log_level' => config('v2board.server_log_level', 'none') 'server_log_enable' => config('v2board.server_log_enable', 0),
'server_v2ray_domain' => config('v2board.server_v2ray_domain'),
'server_v2ray_protocol' => config('v2board.server_v2ray_protocol'),
], ],
'tutorial' => [ 'tutorial' => [
'apple_id' => config('v2board.apple_id') 'apple_id' => config('v2board.apple_id')
], ],
'email' => [ 'email' => [
'email_template' => config('v2board.email_template', 'default') 'email_template' => config('v2board.email_template', 'default'),
'email_host' => config('v2board.email_host'),
'email_port' => config('v2board.email_port'),
'email_username' => config('v2board.email_username'),
'email_password' => config('v2board.email_password'),
'email_encryption' => config('v2board.email_encryption'),
'email_from_address' => config('v2board.email_from_address')
], ],
'telegram' => [ 'telegram' => [
'telegram_bot_enable' => config('v2board.telegram_bot_enable', 0), 'telegram_bot_enable' => config('v2board.telegram_bot_enable', 0),
@ -121,7 +137,7 @@ class ConfigController extends Controller
$data = $request->input(); $data = $request->input();
$array = \Config::get('v2board'); $array = \Config::get('v2board');
foreach ($data as $k => $v) { foreach ($data as $k => $v) {
if (!in_array($k, array_keys(ConfigSave::RULES))) { if (!in_array($k, array_keys($request->validated()))) {
abort(500, '参数' . $k . '不在规则内,禁止修改'); abort(500, '参数' . $k . '不在规则内,禁止修改');
} }
$array[$k] = $v; $array[$k] = $v;
@ -130,10 +146,10 @@ class ConfigController extends Controller
if (!\File::put(base_path() . '/config/v2board.php', "<?php\n return $data ;")) { if (!\File::put(base_path() . '/config/v2board.php', "<?php\n return $data ;")) {
abort(500, '修改失败'); abort(500, '修改失败');
} }
\Artisan::call('config:cache'); if (function_exists('opcache_reset')) {
if (function_exists('opcache')) {
opcache_reset(); opcache_reset();
} }
\Artisan::call('config:cache');
return response([ return response([
'data' => true 'data' => true
]); ]);

View File

@ -23,12 +23,12 @@ class CouponController extends Controller
public function save(CouponSave $request) public function save(CouponSave $request)
{ {
$params = $request->only(array_keys(CouponSave::RULES)); $params = $request->validated();
if (isset($params['limit_plan_ids'])) { if (isset($params['limit_plan_ids'])) {
$params['limit_plan_ids'] = json_encode($params['limit_plan_ids']); $params['limit_plan_ids'] = json_encode($params['limit_plan_ids']);
} }
if (!$request->input('id')) { if (!$request->input('id')) {
if (!$params['code']) { if (!isset($params['code'])) {
$params['code'] = Helper::randomChar(8); $params['code'] = Helper::randomChar(8);
} }
if (!Coupon::create($params)) { if (!Coupon::create($params)) {

View File

@ -42,7 +42,7 @@ class PlanController extends Controller
public function save(PlanSave $request) public function save(PlanSave $request)
{ {
$params = $request->only(array_keys(PlanSave::RULES)); $params = $request->validated();
if ($request->input('id')) { if ($request->input('id')) {
$plan = Plan::find($request->input('id')); $plan = Plan::find($request->input('id'));
if (!$plan) { if (!$plan) {

View File

@ -23,7 +23,7 @@ class TrojanController extends Controller
$server[$i]['tags'] = json_decode($server[$i]['tags']); $server[$i]['tags'] = json_decode($server[$i]['tags']);
} }
$server[$i]['group_id'] = json_decode($server[$i]['group_id']); $server[$i]['group_id'] = json_decode($server[$i]['group_id']);
$server[$i]['online'] = Cache::get(CacheKey::get('SERVER_TROJAN_ONLINE_USER', $server[$i]['id'])); $server[$i]['online'] = Cache::get(CacheKey::get('SERVER_TROJAN_ONLINE_USER', $server[$i]['parent_id'] ? $server[$i]['parent_id'] : $server[$i]['id']));
if ($server[$i]['parent_id']) { if ($server[$i]['parent_id']) {
$server[$i]['last_check_at'] = Cache::get(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $server[$i]['parent_id'])); $server[$i]['last_check_at'] = Cache::get(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $server[$i]['parent_id']));
} else { } else {
@ -37,7 +37,7 @@ class TrojanController extends Controller
public function save(ServerTrojanSave $request) public function save(ServerTrojanSave $request)
{ {
$params = $request->only(array_keys(ServerTrojanSave::RULES)); $params = $request->validated();
$params['group_id'] = json_encode($params['group_id']); $params['group_id'] = json_encode($params['group_id']);
if (isset($params['tags'])) { if (isset($params['tags'])) {
$params['tags'] = json_encode($params['tags']); $params['tags'] = json_encode($params['tags']);

View File

@ -37,7 +37,7 @@ class V2rayController extends Controller
public function save(ServerV2raySave $request) public function save(ServerV2raySave $request)
{ {
$params = $request->only(array_keys(ServerV2raySave::RULES)); $params = $request->validated();
$params['group_id'] = json_encode($params['group_id']); $params['group_id'] = json_encode($params['group_id']);
if (isset($params['tags'])) { if (isset($params['tags'])) {
$params['tags'] = json_encode($params['tags']); $params['tags'] = json_encode($params['tags']);

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Jobs\SendEmailJob; use App\Jobs\SendEmailJob;
use App\Services\TicketService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Ticket; use App\Models\Ticket;
@ -63,27 +64,12 @@ class TicketController extends Controller
if (empty($request->input('message'))) { if (empty($request->input('message'))) {
abort(500, '消息不能为空'); abort(500, '消息不能为空');
} }
$ticket = Ticket::where('id', $request->input('id')) $ticketService = new TicketService();
->first(); $ticketService->replyByAdmin(
if (!$ticket) { $request->input('id'),
abort(500, '工单不存在'); $request->input('message'),
} $request->session()->get('id')
if ($ticket->status) { );
abort(500, '工单已关闭,无法回复');
}
DB::beginTransaction();
$ticketMessage = TicketMessage::create([
'user_id' => $request->session()->get('id'),
'ticket_id' => $ticket->id,
'message' => $request->input('message')
]);
$ticket->last_reply_user_id = $request->session()->get('id');
if (!$ticketMessage || !$ticket->save()) {
DB::rollback();
abort(500, '工单回复失败');
}
DB::commit();
$this->sendEmailNotify($ticket, $ticketMessage);
return response([ return response([
'data' => true 'data' => true
]); ]);
@ -107,24 +93,4 @@ class TicketController extends Controller
'data' => true 'data' => true
]); ]);
} }
// 半小时内不再重复通知
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' => '您在' . config('v2board.app_name', 'V2Board') . '的工单得到了回复',
'template_name' => 'notify',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'url' => config('v2board.app_url'),
'content' => "主题:{$ticket->subject}\r\n回复内容:{$ticketMessage->message}"
]
]);
}
}
} }

View File

@ -20,7 +20,7 @@ class TutorialController extends Controller
public function save(TutorialSave $request) public function save(TutorialSave $request)
{ {
$params = $request->only(array_keys(TutorialSave::RULES)); $params = $request->validated();
if (!$request->input('id')) { if (!$request->input('id')) {
if (!Tutorial::create($params)) { if (!Tutorial::create($params)) {

View File

@ -53,7 +53,7 @@ class UserController extends Controller
public function update(UserUpdate $request) public function update(UserUpdate $request)
{ {
$params = $request->only(array_keys(UserUpdate::RULES)); $params = $request->validated();
$user = User::find($request->input('id')); $user = User::find($request->input('id'));
if (!$user) { if (!$user) {
abort(500, '用户不存在'); abort(500, '用户不存在');

View File

@ -49,7 +49,10 @@ class AppController extends Controller
public function getVersion() public function getVersion()
{ {
return response([ return response([
'data' => '4.0.0' 'data' => [
'version' => '4.0.0',
'download_url' => ''
]
]); ]);
} }

View File

@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
use App\Services\ServerService; use App\Services\ServerService;
use App\Utils\Clash; use App\Utils\Clash;
use App\Utils\QuantumultX; use App\Utils\QuantumultX;
use App\Utils\Shadowrocket;
use App\Utils\Surge; use App\Utils\Surge;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Models\Server; use App\Models\Server;
@ -41,6 +42,9 @@ class ClientController extends Controller
if (strpos($_SERVER['HTTP_USER_AGENT'], 'surge') !== false) { if (strpos($_SERVER['HTTP_USER_AGENT'], 'surge') !== false) {
die($this->surge($user, $servers['vmess'], $servers['trojan'])); die($this->surge($user, $servers['vmess'], $servers['trojan']));
} }
if (strpos($_SERVER['HTTP_USER_AGENT'], 'shadowrocket') !== false) {
die($this->shadowrocket($user, $servers['vmess'], $servers['trojan']));
}
} }
die($this->origin($user, $servers['vmess'], $servers['trojan'])); die($this->origin($user, $servers['vmess'], $servers['trojan']));
} }
@ -66,9 +70,28 @@ class ClientController extends Controller
return base64_encode($uri); return base64_encode($uri);
} }
private function shadowrocket($user, $vmess = [], $trojan = [])
{
$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 = date('Y-m-d', $user->expired_at);
$uri .= "STATUS=🚀↑:{$upload}GB,↓:{$download}GB,TOT:{$totalTraffic}GB💡Expires:{$expiredDate}\r\n";
foreach ($vmess as $item) {
$uri .= Shadowrocket::buildVmess($user->uuid, $item);
}
foreach ($trojan as $item) {
$uri .= Shadowrocket::buildTrojan($user->uuid, $item);
}
return base64_encode($uri);
}
private function quantumultX($user, $vmess = [], $trojan = []) private function quantumultX($user, $vmess = [], $trojan = [])
{ {
$uri = ''; $uri = '';
header("subscription-userinfo: upload={$user->u}; download={$user->d}; total={$user->transfer_enable}; expire={$user->expired_at}");
foreach ($vmess as $item) { foreach ($vmess as $item) {
$uri .= QuantumultX::buildVmess($user->uuid, $item); $uri .= QuantumultX::buildVmess($user->uuid, $item);
} }

View File

@ -3,14 +3,16 @@
namespace App\Http\Controllers\Guest; namespace App\Http\Controllers\Guest;
use App\Services\OrderService; use App\Services\OrderService;
use App\Services\TelegramService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Order; use App\Models\Order;
use Library\Epay;
use Omnipay\Omnipay; use Omnipay\Omnipay;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Library\BitpayX; use Library\BitpayX;
use Library\PayTaro; use Library\MGate;
class OrderController extends Controller class OrderController extends Controller
{ {
@ -128,12 +130,22 @@ class OrderController extends Controller
])); ]));
} }
public function payTaroNotify(Request $request) public function mgateNotify(Request $request)
{ {
// Log::info('payTaroNotify: ' . json_encode($request->input())); $mgate = new MGate(config('v2board.mgate_url'), config('v2board.mgate_app_id'), config('v2board.mgate_app_secret'));
if (!$mgate->verify($request->input())) {
abort(500, 'fail');
}
if (!$this->handle($request->input('out_trade_no'), $request->input('trade_no'))) {
abort(500, 'fail');
}
die('success');
}
$payTaro = new PayTaro(config('v2board.paytaro_app_id'), config('v2board.paytaro_app_secret')); public function epayNotify(Request $request)
if (!$payTaro->verify($request->input())) { {
$epay = new Epay(config('v2board.epay_url'), config('v2board.epay_pid'), config('v2board.epay_key'));
if (!$epay->verify($request->input())) {
abort(500, 'fail'); abort(500, 'fail');
} }
if (!$this->handle($request->input('out_trade_no'), $request->input('trade_no'))) { if (!$this->handle($request->input('out_trade_no'), $request->input('trade_no'))) {
@ -149,6 +161,16 @@ class OrderController extends Controller
abort(500, 'order is not found'); abort(500, 'order is not found');
} }
$orderService = new OrderService($order); $orderService = new OrderService($order);
return $orderService->success($callbackNo); if (!$orderService->success($callbackNo)) {
return false;
}
$telegramService = new TelegramService();
$message = sprintf(
"💰成功收款%s元\n———————————————\n订单号:%s",
$order->total_amount / 100,
$order->trade_no
);
$telegramService->sendMessageWithAdmin($message);
return true;
} }
} }

View File

@ -7,6 +7,7 @@ use Illuminate\Http\Request;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\User; use App\Models\User;
use App\Utils\Helper; use App\Utils\Helper;
use App\Services\TicketService;
class TelegramController extends Controller class TelegramController extends Controller
{ {
@ -24,6 +25,22 @@ class TelegramController extends Controller
$this->msg = $this->getMessage($request->input()); $this->msg = $this->getMessage($request->input());
if (!$this->msg) return; if (!$this->msg) return;
try { try {
switch($this->msg->message_type) {
case 'send':
$this->fromSend();
break;
case 'reply':
$this->fromReply();
break;
}
} catch (\Exception $e) {
$telegramService = new TelegramService();
$telegramService->sendMessage($this->msg->chat_id, $e->getMessage());
}
}
private function fromSend()
{
switch($this->msg->command) { switch($this->msg->command) {
case '/bind': $this->bind(); case '/bind': $this->bind();
break; break;
@ -31,11 +48,17 @@ class TelegramController extends Controller
break; break;
case '/getlatesturl': $this->getLatestUrl(); case '/getlatesturl': $this->getLatestUrl();
break; break;
case '/unbind': $this->unbind();
break;
default: $this->help(); default: $this->help();
} }
} catch (\Exception $e) { }
$telegramService = new TelegramService();
$telegramService->sendMessage($this->msg->chat_id, $e->getMessage()); private function fromReply()
{
// ticket
if (preg_match("/[#](.*)/", $this->msg->reply_text, $match)) {
$this->replayTicket($match[1]);
} }
} }
@ -50,6 +73,11 @@ class TelegramController extends Controller
$obj->args = array_slice($text, 1); $obj->args = array_slice($text, 1);
$obj->chat_id = $data['message']['chat']['id']; $obj->chat_id = $data['message']['chat']['id'];
$obj->message_id = $data['message']['message_id']; $obj->message_id = $data['message']['message_id'];
$obj->message_type = !isset($data['message']['reply_to_message']) ? 'send' : 'reply';
$obj->text = $data['message']['text'];
if ($obj->message_type === 'reply') {
$obj->reply_text = $data['message']['reply_to_message']['text'];
}
return $obj; return $obj;
} }
@ -71,6 +99,9 @@ class TelegramController extends Controller
if (!$user) { if (!$user) {
abort(500, '用户不存在'); abort(500, '用户不存在');
} }
if ($user->telegram_id) {
abort(500, '该账号已经绑定了Telegram账号');
}
$user->telegram_id = $msg->chat_id; $user->telegram_id = $msg->chat_id;
if (!$user->save()) { if (!$user->save()) {
abort(500, '设置失败'); abort(500, '设置失败');
@ -79,6 +110,24 @@ class TelegramController extends Controller
$telegramService->sendMessage($msg->chat_id, '绑定成功'); $telegramService->sendMessage($msg->chat_id, '绑定成功');
} }
private function unbind()
{
$msg = $this->msg;
if (!$msg->is_private) return;
$user = User::where('telegram_id', $msg->chat_id)->first();
$telegramService = new TelegramService();
if (!$user) {
$this->help();
$telegramService->sendMessage($msg->chat_id, '没有查询到您的用户信息,请先绑定账号', 'markdown');
return;
}
$user->telegram_id = NULL;
if (!$user->save()) {
abort(500, '解绑失败');
}
$telegramService->sendMessage($msg->chat_id, '解绑成功', 'markdown');
}
private function help() private function help()
{ {
$msg = $this->msg; $msg = $this->msg;
@ -87,7 +136,8 @@ class TelegramController extends Controller
$commands = [ $commands = [
'/bind 订阅地址 - 绑定你的' . config('v2board.app_name', 'V2Board') . '账号', '/bind 订阅地址 - 绑定你的' . config('v2board.app_name', 'V2Board') . '账号',
'/traffic - 查询流量信息', '/traffic - 查询流量信息',
'/getlatesturl - 获取最新的' . config('v2board.app_name', 'V2Board') . '网址' '/getlatesturl - 获取最新的' . config('v2board.app_name', 'V2Board') . '网址',
'/unbind - 解除绑定'
]; ];
$text = implode(PHP_EOL, $commands); $text = implode(PHP_EOL, $commands);
$telegramService->sendMessage($msg->chat_id, "你可以使用以下命令进行操作:\n\n$text", 'markdown'); $telegramService->sendMessage($msg->chat_id, "你可以使用以下命令进行操作:\n\n$text", 'markdown');
@ -124,4 +174,24 @@ class TelegramController extends Controller
); );
$telegramService->sendMessage($msg->chat_id, $text, 'markdown'); $telegramService->sendMessage($msg->chat_id, $text, 'markdown');
} }
private function replayTicket($ticketId)
{
$msg = $this->msg;
if (!$msg->is_private) return;
$user = User::where('telegram_id', $msg->chat_id)->first();
if (!$user) {
abort(500, '用户不存在');
}
$ticketService = new TicketService();
if ($user->is_admin) {
$ticketService->replyByAdmin(
$ticketId,
$msg->text,
$user->id
);
}
$telegramService = new TelegramService();
$telegramService->sendMessage($msg->chat_id, "#`{$ticketId}` 的工单已回复成功", 'markdown');
}
} }

View File

@ -136,17 +136,11 @@ class AuthController extends Controller
]); ]);
} }
// 准备废弃
public function token2Login(Request $request) public function token2Login(Request $request)
{ {
if ($request->input('token')) { if ($request->input('token')) {
$user = User::where('token', $request->input('token'))->first(); $redirect = '/#/login?verify=' . $request->input('token') . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard');
if (!$user) {
return header('Location:' . config('v2board.app_url'));
}
$code = Helper::guid();
$key = 'token2Login_' . $code;
Cache::put($key, $user->id, 600);
$redirect = '/#/login?verify=' . $code . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard');
if (config('v2board.app_url')) { if (config('v2board.app_url')) {
$location = config('v2board.app_url') . $redirect; $location = config('v2board.app_url') . $redirect;
} else { } else {
@ -156,7 +150,7 @@ class AuthController extends Controller
} }
if ($request->input('verify')) { if ($request->input('verify')) {
$key = 'token2Login_' . $request->input('verify'); $key = CacheKey::get('TEMP_TOKEN', $request->input('verify'));
$userId = Cache::get($key); $userId = Cache::get($key);
if (!$userId) { if (!$userId) {
abort(500, '令牌有误'); abort(500, '令牌有误');
@ -180,6 +174,21 @@ class AuthController extends Controller
} }
} }
public function getTempToken(Request $request)
{
$user = User::where('token', $request->input('token'))->first();
if (!$user) {
abort(500, '用户不存在');
}
$code = Helper::guid();
$key = CacheKey::get('TEMP_TOKEN', $code);
Cache::put($key, $user->id, 60);
return response([
'data' => $code
]);
}
public function check(Request $request) public function check(Request $request)
{ {
$data = [ $data = [

View File

@ -18,7 +18,8 @@ use Omnipay\Omnipay;
use Stripe\Stripe; use Stripe\Stripe;
use Stripe\Source; use Stripe\Source;
use Library\BitpayX; use Library\BitpayX;
use Library\PayTaro; use Library\MGate;
use Library\Epay;
class OrderController extends Controller class OrderController extends Controller
{ {
@ -96,6 +97,10 @@ class OrderController extends Controller
abort(500, '必须存在订阅才可以购买流量重置包'); abort(500, '必须存在订阅才可以购买流量重置包');
} }
if ($request->input('cycle') === 'reset_price' && $user->expired_at <= time()) {
abort(500, '当前订阅已过期,无法购买重置包');
}
DB::beginTransaction(); DB::beginTransaction();
$order = new Order(); $order = new Order();
$orderService = new OrderService($order); $orderService = new OrderService($order);
@ -209,12 +214,12 @@ class OrderController extends Controller
'data' => $this->bitpayX($order) 'data' => $this->bitpayX($order)
]); ]);
case 5: case 5:
if (!(int)config('v2board.paytaro_enable')) { if (!(int)config('v2board.mgate_enable')) {
abort(500, '支付方式不可用'); abort(500, '支付方式不可用');
} }
return response([ return response([
'type' => 1, 'type' => 1,
'data' => $this->payTaro($order) 'data' => $this->mgate($order)
]); ]);
case 6: case 6:
if (!(int)config('v2board.stripe_card_enable')) { if (!(int)config('v2board.stripe_card_enable')) {
@ -224,6 +229,14 @@ class OrderController extends Controller
'type' => 2, 'type' => 2,
'data' => $this->stripeCard($order, $request->input('token')) 'data' => $this->stripeCard($order, $request->input('token'))
]); ]);
case 7:
if (!(int)config('v2board.epay_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 1,
'data' => $this->epay($order)
]);
default: default:
abort(500, '支付方式不存在'); abort(500, '支付方式不存在');
} }
@ -278,9 +291,9 @@ class OrderController extends Controller
array_push($data, $bitpayX); array_push($data, $bitpayX);
} }
if ((int)config('v2board.paytaro_enable')) { if ((int)config('v2board.mgate_enable')) {
$obj = new \StdClass(); $obj = new \StdClass();
$obj->name = config('v2board.paytaro_name', '在线支付'); $obj->name = config('v2board.mgate_name', '在线支付');
$obj->method = 5; $obj->method = 5;
$obj->icon = 'wallet'; $obj->icon = 'wallet';
array_push($data, $obj); array_push($data, $obj);
@ -294,6 +307,14 @@ class OrderController extends Controller
array_push($data, $obj); array_push($data, $obj);
} }
if ((int)config('v2board.epay_enable')) {
$obj = new \StdClass();
$obj->name = config('v2board.epay_name', '在线支付');
$obj->method = 7;
$obj->icon = 'wallet';
array_push($data, $obj);
}
return response([ return response([
'data' => $data 'data' => $data
]); ]);
@ -450,16 +471,28 @@ class OrderController extends Controller
return isset($result['payment_url']) ? $result['payment_url'] : false; return isset($result['payment_url']) ? $result['payment_url'] : false;
} }
private function payTaro($order) private function mgate($order)
{ {
$payTaro = new PayTaro(config('v2board.paytaro_app_id'), config('v2board.paytaro_app_secret')); $mgate = new MGate(config('v2board.mgate_url'), config('v2board.mgate_app_id'), config('v2board.mgate_app_secret'));
$result = $payTaro->pay([ $result = $mgate->pay([
'app_id' => config('v2board.paytaro_app_id'), 'app_id' => config('v2board.mgate_app_id'),
'out_trade_no' => $order->trade_no, 'out_trade_no' => $order->trade_no,
'total_amount' => $order->total_amount, 'total_amount' => $order->total_amount,
'notify_url' => url('/api/v1/guest/order/payTaroNotify'), 'notify_url' => url('/api/v1/guest/order/mgateNotify'),
'return_url' => config('v2board.app_url', env('APP_URL')) . '/#/order' 'return_url' => config('v2board.app_url', env('APP_URL')) . '/#/order'
]); ]);
return $result; return $result;
} }
private function epay($order)
{
$epay = new Epay(config('v2board.epay_url'), config('v2board.epay_pid'), config('v2board.epay_key'));
return $epay->pay([
'money' => $order->total_amount / 100,
'name' => $order->trade_no,
'notify_url' => url('/api/v1/guest/order/epayNotify'),
'return_url' => config('v2board.app_url', env('APP_URL')) . '/#/order',
'out_trade_no' => $order->trade_no
]);
}
} }

View File

@ -7,6 +7,7 @@ use App\Http\Requests\User\TicketSave;
use App\Http\Requests\User\TicketWithdraw; use App\Http\Requests\User\TicketWithdraw;
use App\Jobs\SendTelegramJob; use App\Jobs\SendTelegramJob;
use App\Models\User; use App\Models\User;
use App\Services\TelegramService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Models\Ticket; use App\Models\Ticket;
use App\Models\TicketMessage; use App\Models\TicketMessage;
@ -188,13 +189,7 @@ class TicketController extends Controller
private function sendNotify(Ticket $ticket, TicketMessage $ticketMessage) private function sendNotify(Ticket $ticket, TicketMessage $ticketMessage)
{ {
if (!config('v2board.telegram_bot_enable', 0)) return; $telegramService = new TelegramService();
$users = User::where('is_admin', 1) $telegramService->sendMessageWithAdmin("📮工单提醒 #{$ticket->id}\n———————————————\n主题:\n`{$ticket->subject}`\n内容:\n`{$ticketMessage->message}`");
->where('telegram_id', '!=', NULL)
->get();
foreach ($users as $user) {
$text = "📮工单提醒 #{$ticket->id}\n———————————————\n主题:\n`{$ticket->subject}`\n内容:\n`{$ticketMessage->message}`";
SendTelegramJob::dispatch($user->telegram_id, $text);
}
} }
} }

View File

@ -99,6 +99,7 @@ class UserController extends Controller
} }
} }
$user['subscribe_url'] = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token']; $user['subscribe_url'] = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token'];
$user['reset_day'] = $this->getResetDay($user);
return response([ return response([
'data' => $user 'data' => $user
]); ]);
@ -160,4 +161,24 @@ class UserController extends Controller
'data' => true 'data' => true
]); ]);
} }
private function getResetDay(User $user)
{
if ($user->expired_at <= time() || $user->expired_at === NULL) return null;
$day = date('d', $user->expired_at);
$today = date('d');
$lastDay = date('d', strtotime('last day of +0 months'));
if ((int)config('v2board.reset_traffic_method') === 0) {
return $lastDay - $today;
}
if ((int)config('v2board.reset_traffic_method') === 1) {
if ((int)$day >= (int)$today) {
return $day - $today;
} else {
return $lastDay - $today + $day;
}
}
return null;
}
} }

View File

@ -6,7 +6,14 @@ use Illuminate\Foundation\Http\FormRequest;
class ConfigSave extends FormRequest class ConfigSave extends FormRequest
{ {
CONST RULES = [ /**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
// invite & commission // invite & commission
'safe_mode_enable' => 'in:0,1', 'safe_mode_enable' => 'in:0,1',
'invite_force' => 'in:0,1', 'invite_force' => 'in:0,1',
@ -35,7 +42,9 @@ class ConfigSave extends FormRequest
// server // server
'server_token' => 'nullable|min:16', 'server_token' => 'nullable|min:16',
'server_license' => 'nullable', 'server_license' => 'nullable',
'server_log_level' => 'nullable|in:debug,info,warning,error,none', 'server_log_enable' => 'in:0,1',
'server_v2ray_domain' => '',
'server_v2ray_protocol' => '',
// alipay // alipay
'alipay_enable' => 'in:0,1', 'alipay_enable' => 'in:0,1',
'alipay_appid' => 'nullable|integer|min:16', 'alipay_appid' => 'nullable|integer|min:16',
@ -48,41 +57,46 @@ class ConfigSave extends FormRequest
'stripe_sk_live' => '', 'stripe_sk_live' => '',
'stripe_pk_live' => '', 'stripe_pk_live' => '',
'stripe_webhook_key' => '', 'stripe_webhook_key' => '',
'stripe_currency' => 'in:hkd,usd,sgd,eur,gbp', 'stripe_currency' => 'in:hkd,usd,sgd,eur,gbp,jpy,cad',
// bitpayx // bitpayx
'bitpayx_name' => '', 'bitpayx_name' => '',
'bitpayx_enable' => 'in:0,1', 'bitpayx_enable' => 'in:0,1',
'bitpayx_appsecret' => '', 'bitpayx_appsecret' => '',
// paytaro // mGate
'paytaro_name' => '', 'mgate_name' => '',
'paytaro_enable' => 'in:0,1', 'mgate_enable' => 'in:0,1',
'paytaro_app_id' => '', 'mgate_url' => 'nullable|url',
'paytaro_app_secret' => '', 'mgate_app_id' => '',
'mgate_app_secret' => '',
// Epay
'epay_name' => '',
'epay_enable' => 'in:0,1',
'epay_url' => 'nullable|url',
'epay_pid' => '',
'epay_key' => '',
// frontend // frontend
'frontend_theme_sidebar' => 'in:dark,light', 'frontend_theme_sidebar' => 'in:dark,light',
'frontend_theme_header' => 'in:dark,light', 'frontend_theme_header' => 'in:dark,light',
'frontend_theme_color' => 'in:default,darkblue,black', 'frontend_theme_color' => 'in:default,darkblue,black',
'frontend_background_url' => 'nullable|url', 'frontend_background_url' => 'nullable|url',
'frontend_admin_path' => '',
// tutorial // tutorial
'apple_id' => 'email', 'apple_id' => 'email',
'apple_id_password' => '', 'apple_id_password' => '',
// email // email
'email_template' => '', 'email_template' => '',
'email_host' => '',
'email_port' => '',
'email_username' => '',
'email_password' => '',
'email_encryption' => '',
'email_from_address' => '',
// telegram // telegram
'telegram_bot_enable' => 'in:0,1', 'telegram_bot_enable' => 'in:0,1',
'telegram_bot_token' => '', 'telegram_bot_token' => '',
'telegram_discuss_id' => '', 'telegram_discuss_id' => '',
'telegram_channel_id' => '' 'telegram_channel_id' => ''
]; ];
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return self::RULES;
} }
public function messages() public function messages()

View File

@ -6,7 +6,14 @@ use Illuminate\Foundation\Http\FormRequest;
class CouponSave extends FormRequest class CouponSave extends FormRequest
{ {
const RULES = [ /**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required', 'name' => 'required',
'type' => 'required|in:1,2', 'type' => 'required|in:1,2',
'value' => 'required|integer', 'value' => 'required|integer',
@ -16,14 +23,6 @@ class CouponSave extends FormRequest
'limit_plan_ids' => 'nullable|array', 'limit_plan_ids' => 'nullable|array',
'code' => '' 'code' => ''
]; ];
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return self::RULES;
} }
public function messages() public function messages()

View File

@ -6,7 +6,14 @@ use Illuminate\Foundation\Http\FormRequest;
class PlanSave extends FormRequest class PlanSave extends FormRequest
{ {
CONST RULES = [ /**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required', 'name' => 'required',
'content' => '', 'content' => '',
'group_id' => 'required', 'group_id' => 'required',
@ -18,14 +25,6 @@ class PlanSave extends FormRequest
'onetime_price' => 'nullable|integer', 'onetime_price' => 'nullable|integer',
'reset_price' => 'nullable|integer' 'reset_price' => 'nullable|integer'
]; ];
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return self::RULES;
} }
public function messages() public function messages()

View File

@ -6,7 +6,14 @@ use Illuminate\Foundation\Http\FormRequest;
class ServerTrojanSave extends FormRequest class ServerTrojanSave extends FormRequest
{ {
CONST RULES = [ /**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'show' => '', 'show' => '',
'name' => 'required', 'name' => 'required',
'group_id' => 'required|array', 'group_id' => 'required|array',
@ -19,14 +26,6 @@ class ServerTrojanSave extends FormRequest
'tags' => 'nullable|array', 'tags' => 'nullable|array',
'rate' => 'required|numeric' 'rate' => 'required|numeric'
]; ];
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return self::RULES;
} }
public function messages() public function messages()

View File

@ -6,7 +6,14 @@ use Illuminate\Foundation\Http\FormRequest;
class ServerV2raySave extends FormRequest class ServerV2raySave extends FormRequest
{ {
CONST RULES = [ /**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'show' => '', 'show' => '',
'name' => 'required', 'name' => 'required',
'group_id' => 'required|array', 'group_id' => 'required|array',
@ -23,14 +30,6 @@ class ServerV2raySave extends FormRequest
'tlsSettings' => '', 'tlsSettings' => '',
'dnsSettings' => '' 'dnsSettings' => ''
]; ];
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return self::RULES;
} }
public function messages() public function messages()

View File

@ -6,12 +6,6 @@ use Illuminate\Foundation\Http\FormRequest;
class TutorialSave extends FormRequest class TutorialSave extends FormRequest
{ {
CONST RULES = [
'title' => 'required',
// 1:windows 2:macos 3:ios 4:android 5:linux 6:router
'category_id' => 'required|in:1,2,3,4,5,6',
'steps' => 'required'
];
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *
@ -19,7 +13,12 @@ class TutorialSave extends FormRequest
*/ */
public function rules() public function rules()
{ {
return self::RULES; return [
'title' => 'required',
// 1:windows 2:macos 3:ios 4:android 5:linux 6:router
'category_id' => 'required|in:1,2,3,4,5,6',
'steps' => 'required'
];
} }
public function messages() public function messages()

View File

@ -6,7 +6,14 @@ use Illuminate\Foundation\Http\FormRequest;
class UserUpdate extends FormRequest class UserUpdate extends FormRequest
{ {
CONST RULES = [ /**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'email' => 'required|email', 'email' => 'required|email',
'password' => 'nullable', 'password' => 'nullable',
'transfer_enable' => 'numeric', 'transfer_enable' => 'numeric',
@ -21,14 +28,6 @@ class UserUpdate extends FormRequest
'balance' => 'integer', 'balance' => 'integer',
'commission_balance' => 'integer' 'commission_balance' => 'integer'
]; ];
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return self::RULES;
} }
public function messages() public function messages()

View File

@ -16,7 +16,8 @@ class GuestRoute
$router->post('/order/alipayNotify', 'Guest\\OrderController@alipayNotify'); $router->post('/order/alipayNotify', 'Guest\\OrderController@alipayNotify');
$router->post('/order/stripeNotify', 'Guest\\OrderController@stripeNotify'); $router->post('/order/stripeNotify', 'Guest\\OrderController@stripeNotify');
$router->post('/order/bitpayXNotify', 'Guest\\OrderController@bitpayXNotify'); $router->post('/order/bitpayXNotify', 'Guest\\OrderController@bitpayXNotify');
$router->post('/order/payTaroNotify', 'Guest\\OrderController@payTaroNotify'); $router->post('/order/mgateNotify', 'Guest\\OrderController@mgateNotify');
$router->post('/order/epayNotify', 'Guset\\OrderController@epayNotify');
// Telegram // Telegram
$router->post('/telegram/webhook', 'Guest\\TelegramController@webhook'); $router->post('/telegram/webhook', 'Guest\\TelegramController@webhook');
}); });

View File

@ -10,14 +10,13 @@ class PassportRoute
$router->group([ $router->group([
'prefix' => 'passport' 'prefix' => 'passport'
], function ($router) { ], function ($router) {
// TODO: 1.1.1 abolish
$router->post('/login', 'Passport\\AuthController@login');
// Auth // Auth
$router->post('/auth/register', 'Passport\\AuthController@register'); $router->post('/auth/register', 'Passport\\AuthController@register');
$router->post('/auth/login', 'Passport\\AuthController@login'); $router->post('/auth/login', 'Passport\\AuthController@login');
$router->get ('/auth/token2Login', 'Passport\\AuthController@token2Login'); $router->get ('/auth/token2Login', 'Passport\\AuthController@token2Login');
$router->get ('/auth/check', 'Passport\\AuthController@check'); $router->get ('/auth/check', 'Passport\\AuthController@check');
$router->post('/auth/forget', 'Passport\\AuthController@forget'); $router->post('/auth/forget', 'Passport\\AuthController@forget');
$router->post('/auth/getTempToken', 'Passport\\AuthController@getTempToken');
// Comm // Comm
$router->get ('/comm/config', 'Passport\\CommController@config'); $router->get ('/comm/config', 'Passport\\CommController@config');
$router->post('/comm/sendEmailVerify', 'Passport\\CommController@sendEmailVerify'); $router->post('/comm/sendEmailVerify', 'Passport\\CommController@sendEmailVerify');

View File

@ -7,6 +7,7 @@ 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 Illuminate\Support\Facades\Mail;
use App\Models\MailLog; use App\Models\MailLog;
@ -34,6 +35,15 @@ class SendEmailJob implements ShouldQueue
*/ */
public function handle() public function handle()
{ {
if (config('v2board.email_host')) {
Config::set('mail.host', config('v2board.email_host', env('mail.host')));
Config::set('mail.port', config('v2board.email_port', env('mail.port')));
Config::set('mail.encryption', config('v2board.email_encryption', env('mail.encryption')));
Config::set('mail.username', config('v2board.email_username', env('mail.username')));
Config::set('mail.password', config('v2board.email_password', env('mail.password')));
Config::set('mail.from.address', config('v2board.email_from_address', env('mail.from.address')));
Config::set('mail.from.name', config('v2board.app_name', 'V2Board'));
}
$params = $this->params; $params = $this->params;
$email = $params['email']; $email = $params['email'];
$subject = $params['subject']; $subject = $params['subject'];

View File

@ -46,7 +46,7 @@ class OrderService
$order = $this->order; $order = $this->order;
if ($order->cycle === 'reset_price') { if ($order->cycle === 'reset_price') {
$order->type = 4; $order->type = 4;
} else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id) { } else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id && $user->expired_at > time()) { // 用户订阅存在且用户订阅与购买订阅不同且用户订阅未过期 === 更换
if (!(int)config('v2board.plan_change_enable', 1)) abort(500, '目前不允许更改订阅,请联系客服或提交工单操作'); if (!(int)config('v2board.plan_change_enable', 1)) abort(500, '目前不允许更改订阅,请联系客服或提交工单操作');
$order->type = 3; $order->type = 3;
$this->getSurplusValue($user, $order); $this->getSurplusValue($user, $order);
@ -56,9 +56,9 @@ class OrderService
} else { } else {
$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 = 2;
} else { } else { // 新购
$order->type = 1; $order->type = 1;
} }
} }

View File

@ -171,26 +171,41 @@ class ServerService
private function setRule(Server $server, object $json) private function setRule(Server $server, object $json)
{ {
$domainRules = array_filter(explode(PHP_EOL, config('v2board.server_v2ray_domain')));
$protocolRules = array_filter(explode(PHP_EOL, config('v2board.server_v2ray_protocol')));
if ($server->ruleSettings) { if ($server->ruleSettings) {
$rules = json_decode($server->ruleSettings); $ruleSettings = json_decode($server->ruleSettings);
// domain // domain
if (isset($rules->domain) && !empty($rules->domain)) { if (isset($ruleSettings->domain)) {
$rules->domain = array_filter($rules->domain); $ruleSettings->domain = array_filter($ruleSettings->domain);
if (!empty($ruleSettings->domain)) {
$domainRules = array_merge($domainRules, $ruleSettings->domain);
}
}
// protocol
if (isset($ruleSettings->protocol)) {
$ruleSettings->protocol = array_filter($ruleSettings->protocol);
if (!empty($ruleSettings->protocol)) {
$protocolRules = array_merge($protocolRules, $ruleSettings->protocol);
}
}
}
if (!empty($domainRules)) {
$domainObj = new \StdClass(); $domainObj = new \StdClass();
$domainObj->type = 'field'; $domainObj->type = 'field';
$domainObj->domain = $rules->domain; $domainObj->domain = $domainRules;
$domainObj->outboundTag = 'block'; $domainObj->outboundTag = 'block';
array_push($json->routing->rules, $domainObj); array_push($json->routing->rules, $domainObj);
} }
// protocol if (!empty($protocolRules)) {
if (isset($rules->protocol) && !empty($rules->protocol)) {
$rules->protocol = array_filter($rules->protocol);
$protocolObj = new \StdClass(); $protocolObj = new \StdClass();
$protocolObj->type = 'field'; $protocolObj->type = 'field';
$protocolObj->protocol = $rules->protocol; $protocolObj->protocol = $protocolRules;
$protocolObj->outboundTag = 'block'; $protocolObj->outboundTag = 'block';
array_push($json->routing->rules, $protocolObj); array_push($json->routing->rules, $protocolObj);
} }
if (empty($domainRules) && empty($protocolRules)) {
$json->inbound->sniffing->enabled = false;
} }
} }

View File

@ -1,6 +1,8 @@
<?php <?php
namespace App\Services; namespace App\Services;
use App\Jobs\SendTelegramJob;
use App\Models\User;
use \Curl\Curl; use \Curl\Curl;
class TelegramService { class TelegramService {
@ -43,4 +45,15 @@ class TelegramService {
} }
return $response; return $response;
} }
public function sendMessageWithAdmin($message)
{
if (!config('v2board.telegram_bot_enable', 0)) return;
$users = User::where('is_admin', 1)
->where('telegram_id', '!=', NULL)
->get();
foreach ($users as $user) {
SendTelegramJob::dispatch($user->telegram_id, $message);
}
}
} }

View File

@ -0,0 +1,57 @@
<?php
namespace App\Services;
use App\Jobs\SendEmailJob;
use App\Models\Ticket;
use App\Models\TicketMessage;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class TicketService {
public function replyByAdmin($ticketId, $message, $userId):void
{
$ticket = Ticket::where('id', $ticketId)
->first();
if (!$ticket) {
abort(500, '工单不存在');
}
if ($ticket->status) {
abort(500, '工单已关闭,无法回复');
}
DB::beginTransaction();
$ticketMessage = TicketMessage::create([
'user_id' => $userId,
'ticket_id' => $ticket->id,
'message' => $message
]);
$ticket->last_reply_user_id = $userId;
if (!$ticketMessage || !$ticket->save()) {
DB::rollback();
abort(500, '工单回复失败');
}
DB::commit();
$this->sendEmailNotify($ticket, $ticketMessage);
}
// 半小时内不再重复通知
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' => '您在' . config('v2board.app_name', 'V2Board') . '的工单得到了回复',
'template_name' => 'notify',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'url' => config('v2board.app_url'),
'content' => "主题:{$ticket->subject}\r\n回复内容:{$ticketMessage->message}"
]
]);
}
}
}

View File

@ -10,7 +10,8 @@ class CacheKey
'SERVER_V2RAY_ONLINE_USER' => '节点在线用户', 'SERVER_V2RAY_ONLINE_USER' => '节点在线用户',
'SERVER_V2RAY_LAST_CHECK_AT' => '节点最后检查时间', 'SERVER_V2RAY_LAST_CHECK_AT' => '节点最后检查时间',
'SERVER_TROJAN_ONLINE_USER' => 'trojan节点在线用户', 'SERVER_TROJAN_ONLINE_USER' => 'trojan节点在线用户',
'SERVER_TROJAN_LAST_CHECK_AT' => 'trojan节点最后检查时间' 'SERVER_TROJAN_LAST_CHECK_AT' => 'trojan节点最后检查时间',
'TEMP_TOKEN' => '临时令牌'
]; ];
public static function get(string $key, $uniqueValue) public static function get(string $key, $uniqueValue)

View File

@ -15,10 +15,12 @@ class Clash
$array['uuid'] = $uuid; $array['uuid'] = $uuid;
$array['alterId'] = 2; $array['alterId'] = 2;
$array['cipher'] = 'auto'; $array['cipher'] = 'auto';
$array['udp'] = true;
if ($server->tls) { if ($server->tls) {
$tlsSettings = json_decode($server->tlsSettings); $tlsSettings = json_decode($server->tlsSettings);
$array['tls'] = true; $array['tls'] = true;
if (isset($tlsSettings->allowInsecure)) $array['skip-cert-verify'] = ($tlsSettings->allowInsecure ? true : false ); if (isset($tlsSettings->allowInsecure)) $array['skip-cert-verify'] = ($tlsSettings->allowInsecure ? true : false );
if (isset($tlsSettings->serverName)) $array['servername'] = $tlsSettings->serverName;
} }
if ($server->network == 'ws') { if ($server->network == 'ws') {
$array['network'] = $server->network; $array['network'] = $server->network;
@ -41,6 +43,7 @@ class Clash
$array['server'] = $server->host; $array['server'] = $server->host;
$array['port'] = $server->port; $array['port'] = $server->port;
$array['password'] = $password; $array['password'] = $password;
$array['udp'] = true;
$array['sni'] = $server->server_name; $array['sni'] = $server->server_name;
if ($server->allow_insecure) { if ($server->allow_insecure) {
$array['skip-cert-verify'] = true; $array['skip-cert-verify'] = true;

3
app/Utils/Helper.php Executable file → Normal file
View File

@ -62,7 +62,8 @@ class Helper
$server->name = rawurlencode($server->name); $server->name = rawurlencode($server->name);
$query = http_build_query([ $query = http_build_query([
'allowInsecure' => $server->allow_insecure, 'allowInsecure' => $server->allow_insecure,
'peer' => $server->server_name 'peer' => $server->server_name,
'sni' => $server->server_name
]); ]);
$uri = "trojan://{$user->uuid}@{$server->host}:{$server->port}?{$query}#{$server->name}"; $uri = "trojan://{$user->uuid}@{$server->host}:{$server->port}?{$query}#{$server->name}";
$uri .= "\r\n"; $uri .= "\r\n";

View File

@ -7,26 +7,44 @@ class QuantumultX
{ {
public static function buildVmess($uuid, $server) public static function buildVmess($uuid, $server)
{ {
$uri = "vmess=" . $server->host . ":" . $server->port . ", method=none, password=" . $uuid . ", fast-open=false, udp-relay=false, tag=" . $server->name; $config = [
"vmess={$server->host}:{$server->port}",
"method=chacha20-poly1305",
"password={$uuid}",
"tag={$server->name}"
];
if ($server->network === 'tcp') {
if ($server->tls) { if ($server->tls) {
$tlsSettings = json_decode($server->tlsSettings); $tlsSettings = json_decode($server->tlsSettings);
if ($server->network === 'tcp') $uri .= ', obfs=over-tls'; array_push($config, 'obfs=over-tls');
if (isset($tlsSettings->allowInsecure)) { if (isset($tlsSettings->allowInsecure)) {
// Default: tls-verification=true // Tips: allowInsecure=false = tls-verification=true
$uri .= ', tls-verification=' . ($tlsSettings->allowInsecure ? "false" : "true"); array_push($config, $tlsSettings->allowInsecure ? 'tls-verification=false' : 'tls-verification=true');
} }
if (isset($tlsSettings->serverName)) { if (isset($tlsSettings->serverName)) {
$uri .= ', obfs-host=' . $tlsSettings->serverName; array_push($config, "obfs-host={$tlsSettings->serverName}");
} }
} }
}
if ($server->network === 'ws') { if ($server->network === 'ws') {
$uri .= ', obfs=' . ($server->tls ? 'wss' : 'ws'); if ($server->tls) {
$tlsSettings = json_decode($server->tlsSettings);
array_push($config, 'obfs=wss');
if (isset($tlsSettings->allowInsecure)) {
array_push($config, $tlsSettings->allowInsecure ? 'tls-verification=false' : 'tls-verification=true');
}
} else {
array_push($config, 'obfs=ws');
}
if ($server->networkSettings) { if ($server->networkSettings) {
$wsSettings = json_decode($server->networkSettings); $wsSettings = json_decode($server->networkSettings);
if (isset($wsSettings->path)) $uri .= ', obfs-uri=' . $wsSettings->path; if (isset($wsSettings->path)) array_push($config, "obfs-uri={$wsSettings->path}");
if (isset($wsSettings->headers->Host)) $uri .= ', obfs-host=' . $wsSettings->headers->Host; if (isset($wsSettings->headers->Host)) array_push($config, "obfs-host={$wsSettings->headers->Host}");
} }
} }
$uri = implode(',', $config);
$uri .= "\r\n"; $uri .= "\r\n";
return $uri; return $uri;
} }
@ -38,13 +56,14 @@ class QuantumultX
"password={$password}", "password={$password}",
"over-tls=true", "over-tls=true",
$server->server_name ? "tls-host={$server->server_name}" : "", $server->server_name ? "tls-host={$server->server_name}" : "",
$server->allow_insecure ? 'tls-verification=true' : 'tls-verification=false', // Tips: allowInsecure=false = tls-verification=true
$server->allow_insecure ? 'tls-verification=false' : 'tls-verification=true',
"fast-open=false", "fast-open=false",
"udp-relay=false", "udp-relay=false",
"tag={$server->name}" "tag={$server->name}"
]; ];
$config = array_filter($config); $config = array_filter($config);
$uri = implode($config, ','); $uri = implode(',', $config);
$uri .= "\r\n"; $uri .= "\r\n";
return $uri; return $uri;
} }

View File

@ -0,0 +1,43 @@
<?php
namespace App\Utils;
class Shadowrocket
{
public static function buildVmess($uuid, $server)
{
$userinfo = base64_encode('auto:' . $uuid . '@' . $server->host . ':' . $server->port);
$config = [
'remark' => $server->name
];
if ($server->tls) {
$tlsSettings = json_decode($server->tlsSettings);
$config['tls'] = 1;
if (isset($tlsSettings->serverName)) $config['peer'] = $tlsSettings->serverName;
if (isset($tlsSettings->allowInsecure)) $config['allowInsecure'] = 1;
}
if ($server->network === 'ws') {
$wsSettings = json_decode($server->networkSettings);
$config['obfs'] = "websocket";
if (isset($wsSettings->path)) $config['path'] = $wsSettings->path;
if (isset($wsSettings->headers->Host)) $config['obfsParam'] = $wsSettings->headers->Host;
}
$query = http_build_query($config, null, '&', PHP_QUERY_RFC3986);
$uri = "vmess://{$userinfo}?{$query}&tfo=1";
$uri .= "\r\n";
return $uri;
}
public static function buildTrojan($password, $server)
{
$server->name = rawurlencode($server->name);
$query = http_build_query([
'allowInsecure' => $server->allow_insecure,
'peer' => $server->server_name
]);
$uri = "trojan://{$password}@{$server->host}:{$server->port}?{$query}&tfo=1#{$server->name}";
$uri .= "\r\n";
return $uri;
}
}

View File

@ -39,7 +39,7 @@ class Surge
"tfo=true" "tfo=true"
]; ];
$config = array_filter($config); $config = array_filter($config);
$uri = implode($config, ','); $uri = implode(',', $config);
$uri .= "\r\n"; $uri .= "\r\n";
return $uri; return $uri;
} }

View File

@ -236,5 +236,5 @@ return [
| The only modification by laravel config | The only modification by laravel config
| |
*/ */
'version' => '1.3.1-r.2' 'version' => '1.3.2-d.1'
]; ];

View File

@ -296,3 +296,7 @@ CHANGE `show` `show` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否显示' AFTE
ALTER TABLE `v2_server_trojan` ALTER TABLE `v2_server_trojan`
ADD `server_name` varchar(255) NULL AFTER `allow_insecure`; ADD `server_name` varchar(255) NULL AFTER `allow_insecure`;
UPDATE `v2_server` SET
`ruleSettings` = NULL
WHERE `ruleSettings` = '{}';

42
library/Epay.php Normal file
View File

@ -0,0 +1,42 @@
<?php
namespace Library;
class Epay
{
private $pid;
private $key;
private $url;
public function __construct($url, $pid, $key)
{
$this->pid = $pid;
$this->key = $key;
$this->url = $url;
}
public function pay($params)
{
$params['pid'] = $this->pid;
ksort($params);
reset($params);
$str = stripslashes(urldecode(http_build_query($params))) . $this->key;
$params['sign'] = md5($str);
$params['sign_type'] = 'MD5';
return $this->url . '/submit.php?' . http_build_query($params);
}
public function verify($params)
{
$sign = $params['sign'];
unset($params['sign']);
unset($params['sign_type']);
ksort($params);
reset($params);
$str = stripslashes(urldecode(http_build_query($params))) . $this->key;
if ($sign !== md5($str)) {
return false;
}
return true;
}
}

View File

@ -4,15 +4,17 @@ namespace Library;
use \Curl\Curl; use \Curl\Curl;
class PayTaro class MGate
{ {
private $appId; private $appId;
private $appSecret; private $appSecret;
private $url;
public function __construct($appId, $appSecret) public function __construct($url, $appId, $appSecret)
{ {
$this->appId = $appId; $this->appId = $appId;
$this->appSecret = $appSecret; $this->appSecret = $appSecret;
$this->url = $url;
} }
public function pay($params) public function pay($params)
@ -21,15 +23,21 @@ class PayTaro
$str = http_build_query($params) . $this->appSecret; $str = http_build_query($params) . $this->appSecret;
$params['sign'] = md5($str); $params['sign'] = md5($str);
$curl = new Curl(); $curl = new Curl();
$curl->post('https://api.paytaro.com/v1/gateway/fetch', http_build_query($params)); $curl->post($this->url . '/v1/gateway/fetch', http_build_query($params));
$result = $curl->response; $result = $curl->response;
if (!$result) { if (!$result) {
abort(500, '网络异常'); abort(500, '网络异常');
} }
if ($curl->error) { if ($curl->error) {
if (isset($result->errors)) {
$errors = (array)$result->errors; $errors = (array)$result->errors;
abort(500, $errors[array_keys($errors)[0]][0]); abort(500, $errors[array_keys($errors)[0]][0]);
} }
if (isset($result->message)) {
abort(500, $result->message);
}
abort(500, '未知错误');
}
$curl->close(); $curl->close();
if (!isset($result->data->trade_no)) { if (!isset($result->data->trade_no)) {
abort(500, '接口请求失败'); abort(500, '接口请求失败');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -10,7 +10,7 @@
## Demo ## Demo
[Demo](https://v2board.com) site provided by 👉[Moack](https://www.moack.co.kr/dedicated.php)👈 [Demo](https://v2board.com)
## Document ## Document
[Click](https://docs.v2board.com) [Click](https://docs.v2board.com)

View File

@ -30,7 +30,7 @@ Route::get('/', function (Request $request) {
]); ]);
}); });
Route::get('/admin', function () { Route::get('/' . config('v2board.frontend_admin_path', 'admin'), function () {
return view('admin', [ return view('admin', [
'title' => config('v2board.app_name', 'V2Board'), 'title' => config('v2board.app_name', 'V2Board'),
'theme_sidebar' => config('v2board.frontend_theme_sidebar', 'light'), 'theme_sidebar' => config('v2board.frontend_theme_sidebar', 'light'),