Merge branch 'dev'

This commit is contained in:
tokumeikoi 2022-06-12 19:47:56 +08:00
commit ecef0315a0
102 changed files with 1697 additions and 877 deletions

View File

@ -98,6 +98,7 @@ class CheckCommission extends Command
if (!$inviter) continue;
if (!isset($commissionShareLevels[$l])) continue;
$commissionBalance = $order->commission_balance * ($commissionShareLevels[$l] / 100);
if (!$commissionBalance) continue;
if ((int)config('v2board.withdraw_close_enable', 0)) {
$inviter->balance = $inviter->balance + $commissionBalance;
} else {

View File

@ -0,0 +1,51 @@
<?php
namespace App\Console\Commands;
use App\Models\Ticket;
use App\Models\User;
use Illuminate\Console\Command;
class ClearUser extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'clear:user';
/**
* The console command description.
*
* @var string
*/
protected $description = '清理用户';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$builder = User::where('plan_id', NULL)
->where('transfer_enable', 0)
->where('expired_at', 0)
->where('last_login_at', NULL);
$count = $builder->count();
if ($builder->delete()) {
$this->info("已删除${count}位没有任何数据的用户");
}
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Console\Commands;
use App\Models\Plan;
use App\Utils\Helper;
use Illuminate\Console\Command;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class ResetPassword extends Command
{
protected $builder;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reset:password {email}';
/**
* The console command description.
*
* @var string
*/
protected $description = '重置用户密码';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$user = User::where('email', $this->argument('email'))->first();
if (!$user) abort(500, '邮箱不存在');
$password = Helper::guid(false);
$user->password = password_hash($password, PASSWORD_DEFAULT);
$user->password_algo = null;
if (!$user->save()) abort(500, '重置失败');
$this->info("!!!重置成功!!!");
$this->info("新密码为:{$password},请尽快修改密码。");
}
}

View File

@ -69,6 +69,12 @@ class ResetTraffic extends Command
// no action
case 2:
break;
// year first day
case 3:
$this->resetByYearFirstDay($builder);
// year expire day
case 4:
$this->resetByExpireYear($builder);
}
break;
}
@ -85,7 +91,43 @@ class ResetTraffic extends Command
case ($resetMethod['method'] === 2): {
break;
}
case ($resetMethod['method'] === 3): {
$builder = with(clone($this->builder))->whereIn('plan_id', $planIds);
$this->resetByYearFirstDay($builder);
break;
}
case ($resetMethod['method'] === 4): {
$builder = with(clone($this->builder))->whereIn('plan_id', $planIds);
$this->resetByExpireYear($builder);
break;
}
}
}
}
private function resetByExpireYear($builder):void
{
$users = [];
foreach ($builder->get() as $item) {
$expireDay = date('m-d', $item->expired_at);
$today = date('m-d');
if ($expireDay === $today) {
array_push($users, $item->id);
}
}
User::whereIn('id', $users)->update([
'u' => 0,
'd' => 0
]);
}
private function resetByYearFirstDay($builder):void
{
if ((string)date('md') === '0101') {
$builder->update([
'u' => 0,
'd' => 0
]);
}
}

View File

@ -7,6 +7,8 @@ use App\Models\User;
use App\Utils\CacheKey;
use App\Utils\Helper;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Foundation\Console\ConfigCacheCommand;
use Illuminate\Support\Facades\Cache;
use Matriphe\Larinfo;

View File

@ -2,12 +2,10 @@
namespace App\Console\Commands;
use App\Jobs\StatServerJob;
use Illuminate\Console\Command;
use App\Models\Order;
use App\Models\StatOrder;
use App\Models\ServerLog;
use Illuminate\Support\Facades\DB;
use App\Models\CommissionLog;
class V2boardStatistics extends Command
{
@ -50,14 +48,16 @@ class V2boardStatistics extends Command
{
$endAt = strtotime(date('Y-m-d'));
$startAt = strtotime('-1 day', $endAt);
$builder = Order::where('paid_at', '>=', $startAt)
$orderBuilder = Order::where('paid_at', '>=', $startAt)
->where('paid_at', '<', $endAt)
->whereNotIn('status', [0, 2]);
$orderCount = $builder->count();
$orderAmount = $builder->sum('total_amount');
$builder = $builder->whereNotNull('actual_commission_balance');
$commissionCount = $builder->count();
$commissionAmount = $builder->sum('actual_commission_balance');
$orderCount = $orderBuilder->count();
$orderAmount = $orderBuilder->sum('total_amount');
$commissionBuilder = CommissionLog::where('created_at', '>=', $startAt)
->where('created_at', '<', $endAt)
->where('get_amount', '>', 0);
$commissionCount = $commissionBuilder->count();
$commissionAmount = $commissionBuilder->sum('get_amount');
$data = [
'order_count' => $orderCount,
'order_amount' => $orderAmount,

View File

@ -3,7 +3,10 @@
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Support\Arr;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
use Facade\Ignition\Exceptions\ViewException;
class Handler extends ExceptionHandler
{
@ -50,6 +53,27 @@ class Handler extends ExceptionHandler
*/
public function render($request, Throwable $exception)
{
if ($exception instanceof ViewException) {
return response([
'message' => "主题初始化发生错误,请在后台对主题检查或配置后重试。"
]);
}
return parent::render($request, $exception);
}
protected function convertExceptionToArray(Throwable $e)
{
return config('app.debug') ? [
'message' => $e->getMessage(),
'exception' => get_class($e),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => collect($e->getTrace())->map(function ($trace) {
return Arr::except($trace, ['args']);
})->all(),
] : [
'message' => $this->isHttpException($e) ? $e->getMessage() : __("Uh-oh, we've had some problems, we're working on it."),
];
}
}

View File

@ -8,6 +8,8 @@ use App\Services\TelegramService;
use Illuminate\Http\Request;
use App\Utils\Dict;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Mail;
class ConfigController extends Controller
@ -54,23 +56,19 @@ class ConfigController extends Controller
public function setTelegramWebhook(Request $request)
{
$hookUrl = url('/api/v1/guest/telegram/webhook?access_token=' . md5(config('v2board.telegram_bot_token', $request->input('telegram_bot_token'))));
$telegramService = new TelegramService($request->input('telegram_bot_token'));
$telegramService->getMe();
$telegramService->setWebhook(
url(
'/api/v1/guest/telegram/webhook?access_token=' . md5(config('v2board.telegram_bot_token', $request->input('telegram_bot_token')))
)
);
$telegramService->setWebhook($hookUrl);
return response([
'data' => true
]);
}
public function fetch()
public function fetch(Request $request)
{
// TODO: default should be in Dict
return response([
'data' => [
$key = $request->input('key');
$data = [
'invite' => [
'invite_force' => (int)config('v2board.invite_force', 0),
'invite_commission' => config('v2board.invite_commission', 10),
@ -87,6 +85,8 @@ class ConfigController extends Controller
'commission_distribution_l3' => config('v2board.commission_distribution_l3')
],
'site' => [
'logo' => config('v2board.logo'),
'force_https' => (int)config('v2board.force_https', 0),
'safe_mode_enable' => (int)config('v2board.safe_mode_enable', 0),
'stop_register' => (int)config('v2board.stop_register', 0),
'email_verify' => (int)config('v2board.email_verify', 0),
@ -104,7 +104,10 @@ class ConfigController extends Controller
'recaptcha_site_key' => config('v2board.recaptcha_site_key'),
'tos_url' => config('v2board.tos_url'),
'currency' => config('v2board.currency', 'CNY'),
'currency_symbol' => config('v2board.currency_symbol', '¥')
'currency_symbol' => config('v2board.currency_symbol', '¥'),
'register_limit_by_ip_enable' => (int)config('v2board.register_limit_by_ip_enable', 0),
'register_limit_count' => config('v2board.register_limit_count', 3),
'register_limit_expire' => config('v2board.register_limit_expire', 60)
],
'subscribe' => [
'plan_change_enable' => (int)config('v2board.plan_change_enable', 1),
@ -113,37 +116,7 @@ class ConfigController extends Controller
'new_order_event_id' => (int)config('v2board.new_order_event_id', 0),
'renew_order_event_id' => (int)config('v2board.renew_order_event_id', 0),
'change_order_event_id' => (int)config('v2board.change_order_event_id', 0),
],
'pay' => [
// alipay
'alipay_enable' => (int)config('v2board.alipay_enable'),
'alipay_appid' => config('v2board.alipay_appid'),
'alipay_pubkey' => config('v2board.alipay_pubkey'),
'alipay_privkey' => config('v2board.alipay_privkey'),
// stripe
'stripe_alipay_enable' => (int)config('v2board.stripe_alipay_enable', 0),
'stripe_wepay_enable' => (int)config('v2board.stripe_wepay_enable', 0),
'stripe_card_enable' => (int)config('v2board.stripe_card_enable', 0),
'stripe_sk_live' => config('v2board.stripe_sk_live'),
'stripe_pk_live' => config('v2board.stripe_pk_live'),
'stripe_webhook_key' => config('v2board.stripe_webhook_key'),
'stripe_currency' => config('v2board.stripe_currency', 'hkd'),
// bitpayx
'bitpayx_name' => config('v2board.bitpayx_name', '在线支付'),
'bitpayx_enable' => (int)config('v2board.bitpayx_enable', 0),
'bitpayx_appsecret' => config('v2board.bitpayx_appsecret'),
// mGate
'mgate_name' => config('v2board.mgate_name', '在线支付'),
'mgate_enable' => (int)config('v2board.mgate_enable', 0),
'mgate_url' => config('v2board.mgate_url'),
'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'),
'show_info_to_server_enable' => (int)config('v2board.show_info_to_server_enable', 0)
],
'frontend' => [
'frontend_theme' => config('v2board.frontend_theme', 'v2board'),
@ -151,9 +124,7 @@ class ConfigController extends Controller
'frontend_theme_header' => config('v2board.frontend_theme_header', 'dark'),
'frontend_theme_color' => config('v2board.frontend_theme_color', 'default'),
'frontend_background_url' => config('v2board.frontend_background_url'),
'frontend_admin_path' => config('v2board.frontend_admin_path', 'admin'),
'frontend_customer_service_method' => config('v2board.frontend_customer_service_method', 0),
'frontend_customer_service_id' => config('v2board.frontend_customer_service_id'),
'frontend_admin_path' => config('v2board.frontend_admin_path', 'admin')
],
'server' => [
'server_token' => config('v2board.server_token'),
@ -184,22 +155,35 @@ class ConfigController extends Controller
'android_version' => config('v2board.android_version'),
'android_download_url' => config('v2board.android_download_url')
]
];
if ($key && isset($data[$key])) {
return response([
'data' => [
$key => $data[$key]
]
]);
};
// TODO: default should be in Dict
return response([
'data' => $data
]);
}
public function save(ConfigSave $request)
{
$data = $request->validated();
$array = \Config::get('v2board');
foreach ($data as $k => $v) {
if (!in_array($k, array_keys($request->validated()))) {
abort(500, '参数' . $k . '不在规则内,禁止修改');
$config = config('v2board');
foreach (ConfigSave::RULES as $k => $v) {
if (!in_array($k, array_keys(ConfigSave::RULES))) {
unset($config[$k]);
continue;
}
$array[$k] = $v;
if (array_key_exists($k, $data)) {
$config[$k] = $data[$k];
}
$data = var_export($array, 1);
if (!\File::put(base_path() . '/config/v2board.php', "<?php\n return $data ;")) {
}
$data = var_export($config, 1);
if (!File::put(base_path() . '/config/v2board.php', "<?php\n return $data ;")) {
abort(500, '修改失败');
}
if (function_exists('opcache_reset')) {
@ -207,7 +191,7 @@ class ConfigController extends Controller
abort(500, '缓存清除失败请卸载或检查opcache配置状态');
}
}
\Artisan::call('config:cache');
Artisan::call('config:cache');
return response([
'data' => true
]);

View File

@ -88,7 +88,16 @@ class CouponController extends Controller
array_push($coupons, $coupon);
}
DB::beginTransaction();
if (!Coupon::insert($coupons)) {
if (!Coupon::insert(array_map(function ($item) use ($coupon) {
// format data
if (isset($item['limit_plan_ids']) && is_array($item['limit_plan_ids'])) {
$item['limit_plan_ids'] = json_encode($coupon['limit_plan_ids']);
}
if (isset($item['limit_period']) && is_array($item['limit_period'])) {
$item['limit_period'] = json_encode($coupon['limit_period']);
}
return $item;
}, $coupons))) {
DB::rollBack();
abort(500, '生成失败');
}

View File

@ -22,7 +22,8 @@ class NoticeController extends Controller
$data = $request->only([
'title',
'content',
'img_url'
'img_url',
'tags'
]);
if (!$request->input('id')) {
if (!Notice::create($data)) {

View File

@ -46,35 +46,50 @@ class PaymentController extends Controller
]);
}
public function show(Request $request)
{
$payment = Payment::find($request->input('id'));
if (!$payment) abort(500, '支付方式不存在');
$payment->enable = !$payment->enable;
if (!$payment->save()) abort(500, '保存失败');
return response([
'data' => true
]);
}
public function save(Request $request)
{
if (!config('v2board.app_url')) {
abort(500, '请在站点配置中配置站点地址');
}
if ($request->input('id')) {
$payment = Payment::find($request->input('id'));
if (!$payment) abort(500, '支付方式不存在');
try {
$payment->update($request->input());
} catch (\Exception $e) {
abort(500, '更新失败');
}
return response([
'data' => true
]);
}
$params = $request->validate([
'name' => 'required',
'icon' => 'nullable',
'payment' => 'required',
'config' => 'required',
'notify_domain' => 'nullable|url'
'notify_domain' => 'nullable|url',
'handling_fee_fixed' => 'nullable|integer',
'handling_fee_percent' => 'nullable|numeric|between:0.1,100'
], [
'name.required' => '显示名称不能为空',
'payment.required' => '网关参数不能为空',
'config.required' => '配置参数不能为空',
'notify_domain.url' => '自定义通知域名格式有误'
'notify_domain.url' => '自定义通知域名格式有误',
'handling_fee_fixed.integer' => '固定手续费格式有误',
'handling_fee_percent.between' => '百分比手续费范围须在0.1-100之间'
]);
if ($request->input('id')) {
$payment = Payment::find($request->input('id'));
if (!$payment) abort(500, '支付方式不存在');
try {
$payment->update($params);
} catch (\Exception $e) {
abort(500, $e->getMessage());
}
return response([
'data' => true
]);
}
$params['uuid'] = Helper::randomChar(8);
if (!Payment::create($params)) {
abort(500, '保存失败');

View File

@ -16,7 +16,6 @@ class PlanController extends Controller
{
public function fetch(Request $request)
{
$counts = User::select(
DB::raw("plan_id"),
DB::raw("count(*) as count")

View File

@ -71,12 +71,12 @@ class StatController extends Controller
'value' => $statistic['order_count']
]);
array_push($result, [
'type' => '佣金金额',
'type' => '佣金金额(已发放)',
'date' => $date,
'value' => $statistic['commission_amount'] / 100
]);
array_push($result, [
'type' => '佣金笔数',
'type' => '佣金笔数(已发放)',
'date' => $date,
'value' => $statistic['commission_count']
]);
@ -94,7 +94,8 @@ class StatController extends Controller
'vmess' => ServerV2ray::where('parent_id', null)->get()->toArray(),
'trojan' => ServerTrojan::where('parent_id', null)->get()->toArray()
];
$timestamp = strtotime('-1 day', strtotime(date('Y-m-d')));
$startAt = strtotime('-1 day', strtotime(date('Y-m-d')));
$endAt = strtotime(date('Y-m-d'));
$statistics = StatServer::select([
'server_id',
'server_type',
@ -102,7 +103,8 @@ class StatController extends Controller
'd',
DB::raw('(u+d) as total')
])
->where('record_at', '>=', $timestamp)
->where('record_at', '>=', $startAt)
->where('record_at', '<', $endAt)
->where('record_type', 'd')
->limit(10)
->orderBy('total', 'DESC')

View File

@ -0,0 +1,90 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Services\ThemeService;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Http\Request;
class ThemeController extends Controller
{
private $themes;
private $path;
public function __construct()
{
$this->path = $path = public_path('theme/');
$this->themes = array_map(function ($item) use ($path) {
return str_replace($path, '', $item);
}, glob($path . '*'));
}
public function getThemes()
{
$themeConfigs = [];
foreach ($this->themes as $theme) {
$themeConfigFile = $this->path . "{$theme}/config.php";
if (!File::exists($themeConfigFile)) continue;
$themeConfig = include($themeConfigFile);
if (!isset($themeConfig['configs']) || !is_array($themeConfig)) continue;
$themeConfigs[$theme] = $themeConfig;
if (config("theme.{$theme}")) continue;
$themeService = new ThemeService($theme);
$themeService->init();
}
return response([
'data' => [
'themes' => $themeConfigs,
'active' => config('v2board.frontend_theme', 'v2board')
]
]);
}
public function getThemeConfig(Request $request)
{
$payload = $request->validate([
'name' => 'required|in:' . join(',', $this->themes)
]);
return response([
'data' => config("theme.{$payload['name']}")
]);
}
public function saveThemeConfig(Request $request)
{
$payload = $request->validate([
'name' => 'required|in:' . join(',', $this->themes),
'config' => 'required'
]);
$payload['config'] = json_decode(base64_decode($payload['config']), true);
if (!$payload['config'] || !is_array($payload['config'])) abort(500, '参数有误');
$themeConfigFile = public_path("theme/{$payload['name']}/config.php");
if (!File::exists($themeConfigFile)) abort(500, '主题不存在');
$themeConfig = include($themeConfigFile);
$validateFields = array_column($themeConfig['configs'], 'field_name');
$config = [];
foreach ($validateFields as $validateField) {
$config[$validateField] = isset($payload['config'][$validateField]) ? $payload['config'][$validateField] : '';
}
File::ensureDirectoryExists(base_path() . '/config/theme/');
$data = var_export($config, 1);
if (!File::put(base_path() . "/config/theme/{$payload['name']}.php", "<?php\n return $data ;")) {
abort(500, '修改失败');
}
try {
Artisan::call('config:cache');
// sleep(2);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => $config
]);
}
}

View File

@ -36,20 +36,20 @@ class TicketController extends Controller
}
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$model = Ticket::orderBy('created_at', 'DESC');
$model = Ticket::orderBy('updated_at', 'DESC');
if ($request->input('status') !== NULL) {
$model->where('status', $request->input('status'));
}
if ($request->input('reply_status') !== NULL) {
$model->whereIn('reply_status', $request->input('reply_status'));
}
if ($request->input('email') !== NULL) {
$user = User::where('email', $request->input('email'))->first();
if ($user) $model->where('user_id', $user->id);
}
$total = $model->count();
$res = $model->forPage($current, $pageSize)
->get();
for ($i = 0; $i < count($res); $i++) {
if ($res[$i]['last_reply_user_id'] == $request->session()->get('id')) {
$res[$i]['reply_status'] = 0;
} else {
$res[$i]['reply_status'] = 1;
}
}
return response([
'data' => $res,
'total' => $total

View File

@ -74,7 +74,7 @@ class UserController extends Controller
$res[$i]['plan_name'] = $plan[$k]['name'];
}
}
$res[$i]['subscribe_url'] = Helper::getSubscribeHost() . '/api/v1/client/subscribe?token=' . $res[$i]['token'];
$res[$i]['subscribe_url'] = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $res[$i]['token']);
}
return response([
'data' => $res,
@ -153,7 +153,6 @@ class UserController extends Controller
}
$data = "邮箱,余额,推广佣金,总流量,剩余流量,套餐到期时间,订阅计划,订阅地址\r\n";
$baseUrl = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL')));
foreach($res as $user) {
$expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
$balance = $user['balance'] / 100;
@ -161,7 +160,7 @@ class UserController extends Controller
$transferEnable = $user['transfer_enable'] ? $user['transfer_enable'] / 1073741824 : 0;
$notUseFlow = (($user['transfer_enable'] - ($user['u'] + $user['d'])) / 1073741824) ?? 0;
$planName = $user['plan_name'] ?? '无订阅';
$subscribeUrl = $baseUrl . '/api/v1/client/subscribe?token=' . $user['token'];
$subscribeUrl = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user['token']);
$data .= "{$user['email']},{$balance},{$commissionBalance},{$transferEnable},{$notUseFlow},{$expireDate},{$planName},{$subscribeUrl}\r\n";
}
echo "\xEF\xBB\xBF" . $data;
@ -232,12 +231,11 @@ class UserController extends Controller
}
DB::commit();
$data = "账号,密码,过期时间,UUID,创建时间,订阅地址\r\n";
$baseUrl = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL')));
foreach($users as $user) {
$expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
$createDate = date('Y-m-d H:i:s', $user['created_at']);
$password = $request->input('password') ?? $user['email'];
$subscribeUrl = $baseUrl . '/api/v1/client/subscribe?token=' . $user['token'];
$subscribeUrl = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user['token']);
$data .= "{$user['email']},{$password},{$expireDate},{$user['uuid']},{$createDate},{$subscribeUrl}\r\n";
}
echo $data;

View File

@ -23,6 +23,7 @@ class ClientController extends Controller
if ($userService->isAvailable($user)) {
$serverService = new ServerService();
$servers = $serverService->getAvailableServers($user);
$this->setSubscribeInfoToServers($servers, $user);
if ($flag) {
foreach (glob(app_path('Http//Controllers//Client//Protocols') . '/*.php') as $file) {
$file = 'App\\Http\\Controllers\\Client\\Protocols\\' . basename($file, '.php');
@ -38,4 +39,26 @@ class ClientController extends Controller
die('该客户端暂不支持进行订阅');
}
}
private function setSubscribeInfoToServers(&$servers, $user)
{
if (!(int)config('v2board.show_info_to_server_enable', 0)) return;
$useTraffic = round($user['u'] / (1024*1024*1024), 2) + round($user['d'] / (1024*1024*1024), 2);
$totalTraffic = round($user['transfer_enable'] / (1024*1024*1024), 2);
$remainingTraffic = $totalTraffic - $useTraffic;
$expiredDate = $user['expired_at'] ? date('Y-m-d', $user['expired_at']) : '长期有效';
$userService = new UserService();
$resetDay = $userService->getResetDay($user);
array_unshift($servers, array_merge($servers[0], [
'name' => "套餐到期:{$expiredDate}",
]));
if ($resetDay) {
array_unshift($servers, array_merge($servers[0], [
'name' => "距离下次重置剩余:{$resetDay}",
]));
}
array_unshift($servers, array_merge($servers[0], [
'name' => "剩余流量:{$remainingTraffic} GB",
]));
}
}

View File

@ -23,7 +23,7 @@ class Clash
$appName = config('v2board.app_name', 'V2Board');
header("subscription-userinfo: upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}");
header('profile-update-interval: 24');
header("content-disposition:attachment;filename={$appName}");
header("content-disposition:attachment;filename*=UTF-8''".rawurlencode($appName));
$defaultConfig = base_path() . '/resources/rules/default.clash.yaml';
$customConfig = base_path() . '/resources/rules/custom.clash.yaml';
if (\File::exists($customConfig)) {
@ -133,7 +133,7 @@ class Clash
if ($server['networkSettings']) {
$grpcSettings = $server['networkSettings'];
$array['grpc-opts'] = [];
$array['grpc-opts']['grpc-service-name'] = $grpcSettings['serviceName'];
if (isset($grpcSettings['serviceName'])) $array['grpc-opts']['grpc-service-name'] = $grpcSettings['serviceName'];
}
}

View File

@ -2,9 +2,9 @@
namespace App\Http\Controllers\Client\Protocols;
class AnXray
class SagerNet
{
public $flag = 'axxray';
public $flag = 'sagernet';
private $servers;
private $user;
@ -74,7 +74,7 @@ class AnXray
}
if ((string)$server['network'] === 'ws') {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path'])) $config['path'] = urlencode($wsSettings['path']);
if (isset($wsSettings['path'])) $config['path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host'])) $config['host'] = urlencode($wsSettings['headers']['Host']);
}
if ((string)$server['network'] === 'grpc') {

View File

@ -23,7 +23,7 @@ class Stash
$appName = config('v2board.app_name', 'V2Board');
header("subscription-userinfo: upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}");
header('profile-update-interval: 24');
header("content-disposition: filename={$appName}");
header("content-disposition: filename*=UTF-8''".rawurlencode($appName));
// 暂时使用clash配置文件后续根据Stash更新情况更新
$defaultConfig = base_path() . '/resources/rules/default.clash.yaml';
$customConfig = base_path() . '/resources/rules/custom.clash.yaml';
@ -134,7 +134,7 @@ class Stash
if ($server['networkSettings']) {
$grpcSettings = $server['networkSettings'];
$array['grpc-opts'] = [];
$array['grpc-opts']['grpc-service-name'] = $grpcSettings['serviceName'];
if (isset($grpcSettings['serviceName'])) $array['grpc-opts']['grpc-service-name'] = $grpcSettings['serviceName'];
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Client\Protocols;
use App\Utils\Helper;
class Surfboard
{
@ -53,7 +54,7 @@ class Surfboard
}
// Subscription link
$subsURL = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token'];
$subsURL = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}");
$subsDomain = $_SERVER['SERVER_NAME'];
$config = str_replace('$subs_link', $subsURL, $config);

View File

@ -2,6 +2,8 @@
namespace App\Http\Controllers\Client\Protocols;
use App\Utils\Helper;
class Surge
{
public $flag = 'surge';
@ -52,6 +54,7 @@ class Surge
}
// Subscription link
$subsURL = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}");
$subsDomain = $_SERVER['SERVER_NAME'];
$subsURL = 'https://' . $subsDomain . '/api/v1/client/subscribe?token=' . $user['token'];

View File

@ -21,7 +21,8 @@ class CommController extends Controller
'is_recaptcha' => (int)config('v2board.recaptcha_enable', 0) ? 1 : 0,
'recaptcha_site_key' => config('v2board.recaptcha_site_key'),
'app_description' => config('v2board.app_description'),
'app_url' => config('v2board.app_url')
'app_url' => config('v2board.app_url'),
'logo' => config('v2board.logo'),
]
]);
}
@ -34,11 +35,4 @@ class CommController extends Controller
}
return $suffix;
}
public function getHitokoto()
{
return response([
'data' => Http::get('https://v1.hitokoto.cn/')->json()
]);
}
}

View File

@ -32,7 +32,7 @@ class PaymentController extends Controller
if (!$order) {
abort(500, 'order is not found');
}
if ($order->status === 1) return true;
if ($order->status !== 0) return true;
$orderService = new OrderService($order);
if (!$orderService->paid($callbackNo)) {
return false;

View File

@ -20,6 +20,12 @@ class AuthController extends Controller
{
public function register(AuthRegister $request)
{
if ((int)config('v2board.register_limit_by_ip_enable', 0)) {
$registerCountByIP = Cache::get(CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip())) ?? 0;
if ((int)$registerCountByIP >= (int)config('v2board.register_limit_count', 3)) {
abort(500, __('Register frequently, please try again after 1 hour'));
}
}
if ((int)config('v2board.recaptcha_enable', 0)) {
$recaptcha = new ReCaptcha(config('v2board.recaptcha_key'));
$recaptchaResp = $recaptcha->verify($request->input('recaptcha_data'));
@ -109,6 +115,16 @@ class AuthController extends Controller
];
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
$user->last_login_at = time();
$user->save();
if ((int)config('v2board.register_limit_by_ip_enable', 0)) {
Cache::put(
CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip()),
(int)$registerCountByIP + 1,
(int)config('v2board.register_limit_expire', 60) * 60
);
}
return response()->json([
'data' => $data
]);

View File

@ -20,6 +20,7 @@ use Illuminate\Support\Facades\Cache;
*/
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}}}}';
public function __construct(Request $request)
{
$token = $request->input('token');
@ -52,13 +53,16 @@ class DeepbworkController extends Controller
"level" => 0,
];
unset($user['uuid']);
unset($user['email']);
array_push($result, $user);
}
$eTag = sha1(json_encode($result));
if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
abort(304);
}
return response([
'msg' => 'ok',
'data' => $result,
]);
])->header('ETag', "\"{$eTag}\"");
}
// 后端提交数据
@ -97,13 +101,133 @@ class DeepbworkController extends Controller
if (empty($nodeId) || empty($localPort)) {
abort(500, '参数错误');
}
$serverService = new ServerService();
try {
$json = $serverService->getV2RayConfig($nodeId, $localPort);
$json = $this->getV2RayConfig($nodeId, $localPort);
} catch (\Exception $e) {
abort(500, $e->getMessage());
}
die(json_encode($json, JSON_UNESCAPED_UNICODE));
}
private function getV2RayConfig(int $nodeId, int $localPort)
{
$server = ServerV2ray::find($nodeId);
if (!$server) {
abort(500, '节点不存在');
}
$json = json_decode(self::V2RAY_CONFIG);
$json->log->loglevel = (int)config('v2board.server_log_enable') ? 'debug' : 'none';
$json->inbounds[1]->port = (int)$localPort;
$json->inbounds[0]->port = (int)$server->server_port;
$json->inbounds[0]->streamSettings->network = $server->network;
$this->setDns($server, $json);
$this->setNetwork($server, $json);
$this->setRule($server, $json);
$this->setTls($server, $json);
return $json;
}
private function setDns(ServerV2ray $server, object $json)
{
if ($server->dnsSettings) {
$dns = $server->dnsSettings;
if (isset($dns->servers)) {
array_push($dns->servers, '1.1.1.1');
array_push($dns->servers, 'localhost');
}
$json->dns = $dns;
$json->outbounds[0]->settings->domainStrategy = 'UseIP';
}
}
private function setNetwork(ServerV2ray $server, object $json)
{
if ($server->networkSettings) {
switch ($server->network) {
case 'tcp':
$json->inbounds[0]->streamSettings->tcpSettings = $server->networkSettings;
break;
case 'kcp':
$json->inbounds[0]->streamSettings->kcpSettings = $server->networkSettings;
break;
case 'ws':
$json->inbounds[0]->streamSettings->wsSettings = $server->networkSettings;
break;
case 'http':
$json->inbounds[0]->streamSettings->httpSettings = $server->networkSettings;
break;
case 'domainsocket':
$json->inbounds[0]->streamSettings->dsSettings = $server->networkSettings;
break;
case 'quic':
$json->inbounds[0]->streamSettings->quicSettings = $server->networkSettings;
break;
case 'grpc':
$json->inbounds[0]->streamSettings->grpcSettings = $server->networkSettings;
break;
}
}
}
private function setRule(ServerV2ray $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) {
$ruleSettings = $server->ruleSettings;
// domain
if (isset($ruleSettings->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->type = 'field';
$domainObj->domain = $domainRules;
$domainObj->outboundTag = 'block';
array_push($json->routing->rules, $domainObj);
}
if (!empty($protocolRules)) {
$protocolObj = new \StdClass();
$protocolObj->type = 'field';
$protocolObj->protocol = $protocolRules;
$protocolObj->outboundTag = 'block';
array_push($json->routing->rules, $protocolObj);
}
if (empty($domainRules) && empty($protocolRules)) {
$json->inbounds[0]->sniffing->enabled = false;
}
}
private function setTls(ServerV2ray $server, object $json)
{
if ((int)$server->tls) {
$tlsSettings = $server->tlsSettings;
$json->inbounds[0]->streamSettings->security = 'tls';
$tls = (object)[
'certificateFile' => '/root/.cert/server.crt',
'keyFile' => '/root/.cert/server.key'
];
$json->inbounds[0]->streamSettings->tlsSettings = new \StdClass();
if (isset($tlsSettings->serverName)) {
$json->inbounds[0]->streamSettings->tlsSettings->serverName = (string)$tlsSettings->serverName;
}
if (isset($tlsSettings->allowInsecure)) {
$json->inbounds[0]->streamSettings->tlsSettings->allowInsecure = (int)$tlsSettings->allowInsecure ? true : false;
}
$json->inbounds[0]->streamSettings->tlsSettings->certificates[0] = $tls;
}
}
}

View File

@ -48,9 +48,13 @@ class ShadowsocksTidalabController extends Controller
'secret' => $user->uuid
]);
}
$eTag = sha1(json_encode($result));
if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
abort(304);
}
return response([
'data' => $result
]);
])->header('ETag', "\"{$eTag}\"");
}
// 后端提交数据

View File

@ -20,6 +20,7 @@ use Illuminate\Support\Facades\Cache;
*/
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}}';
public function __construct(Request $request)
{
$token = $request->input('token');
@ -49,13 +50,16 @@ class TrojanTidalabController extends Controller
"password" => $user->uuid,
];
unset($user['uuid']);
unset($user['email']);
array_push($result, $user);
}
$eTag = sha1(json_encode($result));
if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
abort(304);
}
return response([
'msg' => 'ok',
'data' => $result,
]);
])->header('ETag', "\"{$eTag}\"");
}
// 后端提交数据
@ -94,13 +98,28 @@ class TrojanTidalabController extends Controller
if (empty($nodeId) || empty($localPort)) {
abort(500, '参数错误');
}
$serverService = new ServerService();
try {
$json = $serverService->getTrojanConfig($nodeId, $localPort);
$json = $this->getTrojanConfig($nodeId, $localPort);
} catch (\Exception $e) {
abort(500, $e->getMessage());
}
die(json_encode($json, JSON_UNESCAPED_UNICODE));
}
private function getTrojanConfig(int $nodeId, int $localPort)
{
$server = ServerTrojan::find($nodeId);
if (!$server) {
abort(500, '节点不存在');
}
$json = json_decode(self::TROJAN_CONFIG);
$json->local_port = $server->server_port;
$json->ssl->sni = $server->server_name ? $server->server_name : $server->host;
$json->ssl->cert = "/root/.cert/server.crt";
$json->ssl->key = "/root/.cert/server.key";
$json->api->api_port = $localPort;
return $json;
}
}

View File

@ -0,0 +1,128 @@
<?php
namespace App\Http\Controllers\Server;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\ServerShadowsocks;
use App\Models\ServerV2ray;
use App\Models\ServerTrojan;
use Illuminate\Support\Facades\Cache;
class VProxyController extends Controller
{
private $nodeType;
private $nodeInfo;
private $nodeId;
private $token;
public function __construct(Request $request)
{
$token = $request->input('token');
if (empty($token)) {
abort(500, 'token is null');
}
if ($token !== config('v2board.server_token')) {
abort(500, 'token is error');
}
$this->token = $token;
$this->nodeType = $request->input('node_type');
$this->nodeId = $request->input('node_id');
switch ($this->nodeType) {
case 'v2ray':
$this->nodeInfo = ServerV2ray::find($this->nodeId);
break;
case 'shadowsocks':
$this->nodeInfo = ServerShadowsocks::find($this->nodeId);
break;
case 'trojan':
$this->nodeInfo = ServerTrojan::find($this->nodeId);
break;
default:
break;
}
if (!$this->nodeInfo) {
abort(500, 'server not found');
}
}
// 后端获取用户
public function user(Request $request)
{
ini_set('memory_limit', -1);
Cache::put(CacheKey::get('SERVER_' . strtoupper($this->nodeType) . '_LAST_CHECK_AT', $this->nodeInfo->id), time(), 3600);
$serverService = new ServerService();
$users = $serverService->getAvailableUsers($this->nodeInfo->group_id);
$users = $users->toArray();
$response['users'] = $users;
switch ($this->nodeType) {
case 'shadowsocks':
$response['server'] = [
'cipher' => $this->nodeInfo->cipher,
'server_port' => $this->nodeInfo->server_port
];
break;
}
$eTag = sha1(json_encode($response));
if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
abort(304);
}
return response($response)->header('ETag', "\"{$eTag}\"");
}
// 后端提交数据
public function submit(Request $request)
{
$data = file_get_contents('php://input');
$data = json_decode($data, true);
Cache::put(CacheKey::get('SERVER_' . strtoupper($this->nodeType) . '_ONLINE_USER', $this->nodeInfo->id), count($data), 3600);
Cache::put(CacheKey::get('SERVER_' . strtoupper($this->nodeType) . '_LAST_PUSH_AT', $this->nodeInfo->id), time(), 3600);
$userService = new UserService();
foreach ($data as $item) {
$u = $item['u'] * $this->nodeInfo->rate;
$d = $item['d'] * $this->nodeInfo->rate;
$userService->trafficFetch($u, $d, $item['user_id'], $this->nodeInfo, $this->nodeType);
}
return response([
'data' => true
]);
}
// 后端获取配置
public function config(Request $request)
{
switch ($this->nodeType) {
case 'shadowsocks':
die(json_encode([
'server_port' => $this->nodeInfo->server_port,
'cipher' => $this->nodeInfo->cipher,
'obfs' => $this->nodeInfo->obfs,
'obfs_settings' => $this->nodeInfo->obfs_settings
], JSON_UNESCAPED_UNICODE));
break;
case 'v2ray':
die(json_encode([
'server_port' => $this->nodeInfo->server_port,
'network' => $this->nodeInfo->network,
'cipher' => $this->nodeInfo->cipher,
'networkSettings' => $this->nodeInfo->networkSettings,
'tls' => $this->nodeInfo->tls
], JSON_UNESCAPED_UNICODE));
break;
case 'trojan':
die(json_encode([
'host' => $this->nodeInfo->host,
'server_port' => $this->nodeInfo->server_port
], JSON_UNESCAPED_UNICODE));
break;
}
}
}

View File

@ -39,13 +39,6 @@ class TicketController extends Controller
$total = $model->count();
$res = $model->forPage($current, $pageSize)
->get();
for ($i = 0; $i < count($res); $i++) {
if ($res[$i]['last_reply_user_id'] == $request->session()->get('id')) {
$res[$i]['reply_status'] = 0;
} else {
$res[$i]['reply_status'] = 1;
}
}
return response([
'data' => $res,
'total' => $total

View File

@ -29,6 +29,7 @@ class InviteController extends Controller
{
return response([
'data' => CommissionLog::where('invite_user_id', $request->session()->get('id'))
->where('get_amount', '>', 0)
->select([
'id',
'trade_no',

View File

@ -5,6 +5,7 @@ namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use App\Models\Knowledge;
@ -28,12 +29,7 @@ class KnowledgeController extends Controller
$appleIdPassword = __('No active subscription. Unable to use our provided Apple ID');
$this->formatAccessData($knowledge['body']);
}
$subscribeUrl = config('v2board.app_url', env('APP_URL'));
$subscribeUrls = explode(',', config('v2board.subscribe_url'));
if ($subscribeUrls) {
$subscribeUrl = $subscribeUrls[rand(0, count($subscribeUrls) - 1)];
}
$subscribeUrl = "{$subscribeUrl}/api/v1/client/subscribe?token={$user['token']}";
$subscribeUrl = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}");
$knowledge['body'] = str_replace('{{siteName}}', config('v2board.app_name', 'V2Board'), $knowledge['body']);
$knowledge['body'] = str_replace('{{subscribeUrl}}', $subscribeUrl, $knowledge['body']);
$knowledge['body'] = str_replace('{{urlEncodeSubscribeUrl}}', urlencode($subscribeUrl), $knowledge['body']);
@ -63,10 +59,12 @@ class KnowledgeController extends Controller
private function formatAccessData(&$body)
{
function getBetween($input, $start, $end){$substr = substr($input, strlen($start)+strpos($input, $start),(strlen($input) - strpos($input, $end))*(-1));return $substr;}
function getBetween($input, $start, $end){$substr = substr($input, strlen($start)+strpos($input, $start),(strlen($input) - strpos($input, $end))*(-1));return $start . $substr . $end;}
while (strpos($body, '<!--access start-->') !== false) {
$accessData = getBetween($body, '<!--access start-->', '<!--access end-->');
if ($accessData) {
$body = str_replace($accessData, '<div class="v2board-no-access">'. __('You must have a valid subscription to view content in this area') .'</div>', $body);
}
}
}
}

View File

@ -87,8 +87,12 @@ class OrderController extends Controller
}
if ($request->input('period') === 'reset_price') {
if ($user->expired_at <= time() || !$user->plan_id) {
if (!$user->plan_id) {
abort(500, __('Subscription has expired or no active subscription, unable to purchase Data Reset Package'));
} else {
if ($user->plan_id !== $request->input('plan_id')) {
abort(500, __('This subscription reset package does not apply to your subscription'));
}
}
}
@ -184,13 +188,17 @@ class OrderController extends Controller
$payment = Payment::find($method);
if (!$payment || $payment->enable !== 1) abort(500, __('Payment method is not available'));
$paymentService = new PaymentService($payment->payment, $payment->id);
if ($payment->handling_fee_fixed || $payment->handling_fee_percent) {
$order->handling_amount = round(($order->total_amount * ($payment->handling_fee_percent / 100)) + $payment->handling_fee_fixed);
}
$order->payment_id = $method;
if (!$order->save()) abort(500, __('Request failed, please try again later'));
$result = $paymentService->pay([
'trade_no' => $tradeNo,
'total_amount' => $order->total_amount,
'total_amount' => isset($order->handling_amount) ? ($order->total_amount + $order->handling_amount) : $order->total_amount,
'user_id' => $order->user_id,
'stripe_token' => $request->input('token')
]);
$order->update(['payment_id' => $method]);
return response([
'type' => $result['type'],
'data' => $result['data']
@ -217,7 +225,9 @@ class OrderController extends Controller
'id',
'name',
'payment',
'icon'
'icon',
'handling_fee_fixed',
'handling_fee_percent'
])
->where('enable', 1)->get();

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use App\Models\Plan;
@ -10,12 +11,15 @@ class PlanController extends Controller
{
public function fetch(Request $request)
{
$user = User::find($request->session()->get('id'));
if ($request->input('id')) {
$plan = Plan::where('id', $request->input('id'))
->first();
$plan = Plan::where('id', $request->input('id'))->first();
if (!$plan) {
abort(500, __('Subscription plan does not exist'));
}
if ((!$plan->show && !$plan->renew) || (!$plan->show && $user->plan_id !== $plan->id)) {
abort(500, __('Subscription plan does not exist'));
}
return response([
'data' => $plan
]);

View File

@ -12,15 +12,14 @@ class StatController extends Controller
public function getTrafficLog(Request $request)
{
$builder = StatUser::select([
DB::raw('sum(u) as u'),
DB::raw('sum(d) as d'),
'u',
'd',
'record_at',
'user_id',
'server_rate'
])
->where('user_id', $request->session()->get('id'))
->where('record_at', '>=', strtotime(date('Y-m-1')))
->groupBy('record_at', 'user_id', 'server_rate')
->orderBy('record_at', 'DESC');
return response([
'data' => $builder->get()

View File

@ -8,6 +8,7 @@ use App\Http\Requests\User\TicketWithdraw;
use App\Jobs\SendTelegramJob;
use App\Models\User;
use App\Services\TelegramService;
use App\Services\TicketService;
use App\Utils\Dict;
use Illuminate\Http\Request;
use App\Models\Ticket;
@ -40,13 +41,6 @@ class TicketController extends Controller
$ticket = Ticket::where('user_id', $request->session()->get('id'))
->orderBy('created_at', 'DESC')
->get();
for ($i = 0; $i < count($ticket); $i++) {
if ($ticket[$i]['last_reply_user_id'] == $request->session()->get('id')) {
$ticket[$i]['reply_status'] = 0;
} else {
$ticket[$i]['reply_status'] = 1;
}
}
return response([
'data' => $ticket
]);
@ -55,15 +49,14 @@ class TicketController extends Controller
public function save(TicketSave $request)
{
DB::beginTransaction();
if ((int)Ticket::where('status', 0)->where('user_id', $request->session()->get('id'))->count()) {
if ((int)Ticket::where('status', 0)->where('user_id', $request->session()->get('id'))->lockForUpdate()->count()) {
abort(500, __('There are other unresolved tickets'));
}
$ticket = Ticket::create(array_merge($request->only([
'subject',
'level'
]), [
'user_id' => $request->session()->get('id'),
'last_reply_user_id' => $request->session()->get('id')
'user_id' => $request->session()->get('id')
]));
if (!$ticket) {
DB::rollback();
@ -79,7 +72,7 @@ class TicketController extends Controller
abort(500, __('Failed to open ticket'));
}
DB::commit();
$this->sendNotify($ticket, $ticketMessage);
$this->sendNotify($ticket, $request->input('message'));
return response([
'data' => true
]);
@ -105,19 +98,15 @@ class TicketController extends Controller
if ($request->session()->get('id') == $this->getLastMessage($ticket->id)->user_id) {
abort(500, __('Please wait for the technical enginneer to reply'));
}
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();
$ticketService = new TicketService();
if (!$ticketService->reply(
$ticket,
$request->input('message'),
$request->session()->get('id')
)) {
abort(500, __('Ticket reply failed'));
}
DB::commit();
$this->sendNotify($ticket, $ticketMessage);
$this->sendNotify($ticket, $request->input('message'));
return response([
'data' => true
]);
@ -175,8 +164,7 @@ class TicketController extends Controller
$ticket = Ticket::create([
'subject' => $subject,
'level' => 2,
'user_id' => $request->session()->get('id'),
'last_reply_user_id' => $request->session()->get('id')
'user_id' => $request->session()->get('id')
]);
if (!$ticket) {
DB::rollback();
@ -196,15 +184,15 @@ class TicketController extends Controller
abort(500, __('Failed to open ticket'));
}
DB::commit();
$this->sendNotify($ticket, $ticketMessage);
$this->sendNotify($ticket, $message);
return response([
'data' => true
]);
}
private function sendNotify(Ticket $ticket, TicketMessage $ticketMessage)
private function sendNotify(Ticket $ticket, string $message)
{
$telegramService = new TelegramService();
$telegramService->sendMessageWithAdmin("📮工单提醒 #{$ticket->id}\n———————————————\n主题:\n`{$ticket->subject}`\n内容:\n`{$ticketMessage->message}`", true);
$telegramService->sendMessageWithAdmin("📮工单提醒 #{$ticket->id}\n———————————————\n主题:\n`{$ticket->subject}`\n内容:\n`{$message}`", true);
}
}

View File

@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\User\UserTransfer;
use App\Http\Requests\User\UserUpdate;
use App\Http\Requests\User\UserChangePassword;
use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use App\Models\User;
@ -120,8 +121,9 @@ class UserController extends Controller
abort(500, __('Subscription plan does not exist'));
}
}
$user['subscribe_url'] = Helper::getSubscribeHost() . "/api/v1/client/subscribe?token={$user['token']}";
$user['reset_day'] = $this->getResetDay($user);
$user['subscribe_url'] = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}");
$userService = new UserService();
$user['reset_day'] = $userService->getResetDay($user);
return response([
'data' => $user
]);
@ -139,7 +141,7 @@ class UserController extends Controller
abort(500, __('Reset failed'));
}
return response([
'data' => config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user->token
'data' => Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user->token)
]);
}
@ -184,36 +186,6 @@ class UserController extends Controller
]);
}
private function getResetDay(User $user)
{
if ($user->expired_at <= time() || $user->expired_at === NULL) return null;
// if reset method is not reset
if (isset($user->plan->reset_traffic_method) && $user->plan->reset_traffic_method === 2) 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 ||
(isset($user->plan->reset_traffic_method) && $user->plan->reset_traffic_method === 0))
{
return $lastDay - $today;
}
if ((int)config('v2board.reset_traffic_method') === 1 ||
(isset($user->plan->reset_traffic_method) && $user->plan->reset_traffic_method === 1))
{
if ((int)$day >= (int)$today && (int)$day >= (int)$lastDay) {
return $lastDay - $today;
}
if ((int)$day >= (int)$today) {
return $day - $today;
} else {
return $lastDay - $today + $day;
}
}
return null;
}
public function getQuickLoginUrl(Request $request)
{
$user = User::find($request->session()->get('id'));

View File

@ -2,8 +2,10 @@
namespace App\Http\Middleware;
use App\Utils\CacheKey;
use Closure;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
class Client
{

View File

@ -6,16 +6,8 @@ use Illuminate\Foundation\Http\FormRequest;
class ConfigSave extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
const RULES = [
// invite & commission
'safe_mode_enable' => 'in:0,1',
'invite_force' => 'in:0,1',
'invite_commission' => 'integer',
'invite_gen_limit' => 'integer',
@ -30,6 +22,9 @@ class ConfigSave extends FormRequest
'commission_distribution_l2' => 'nullable|numeric',
'commission_distribution_l3' => 'nullable|numeric',
// site
'logo' => 'nullable|url',
'force_https' => 'in:0,1',
'safe_mode_enable' => 'in:0,1',
'stop_register' => 'in:0,1',
'email_verify' => 'in:0,1',
'app_name' => '',
@ -48,48 +43,23 @@ class ConfigSave extends FormRequest
'tos_url' => 'nullable|url',
'currency' => '',
'currency_symbol' => '',
'register_limit_by_ip_enable' => 'in:0,1',
'register_limit_count' => 'integer',
'register_limit_expire' => 'integer',
// subscribe
'plan_change_enable' => 'in:0,1',
'reset_traffic_method' => 'in:0,1,2',
'reset_traffic_method' => 'in:0,1,2,3,4',
'surplus_enable' => 'in:0,1',
'new_order_event_id' => 'in:0,1',
'renew_order_event_id' => 'in:0,1',
'change_order_event_id' => 'in:0,1',
'show_info_to_server_enable' => 'in:0,1',
// server
'server_token' => 'nullable|min:16',
'server_license' => 'nullable',
'server_log_enable' => 'in:0,1',
'server_v2ray_domain' => '',
'server_v2ray_protocol' => '',
// alipay
'alipay_enable' => 'in:0,1',
'alipay_appid' => 'nullable|integer|min:16',
'alipay_pubkey' => 'max:2048',
'alipay_privkey' => 'max:2048',
// stripe
'stripe_alipay_enable' => 'in:0,1',
'stripe_wepay_enable' => 'in:0,1',
'stripe_card_enable' => 'in:0,1',
'stripe_sk_live' => '',
'stripe_pk_live' => '',
'stripe_webhook_key' => '',
'stripe_currency' => 'in:hkd,usd,sgd,eur,gbp,jpy,cad',
// bitpayx
'bitpayx_name' => '',
'bitpayx_enable' => 'in:0,1',
'bitpayx_appsecret' => '',
// mGate
'mgate_name' => '',
'mgate_enable' => 'in:0,1',
'mgate_url' => 'nullable|url',
'mgate_app_id' => '',
'mgate_app_secret' => '',
// Epay
'epay_name' => '',
'epay_enable' => 'in:0,1',
'epay_url' => 'nullable|url',
'epay_pid' => '',
'epay_key' => '',
// frontend
'frontend_theme' => '',
'frontend_theme_sidebar' => 'in:dark,light',
@ -97,8 +67,6 @@ class ConfigSave extends FormRequest
'frontend_theme_color' => 'in:default,darkblue,black,green',
'frontend_background_url' => 'nullable|url',
'frontend_admin_path' => '',
'frontend_customer_service_method' => '',
'frontend_customer_service_id' => '',
// email
'email_template' => '',
'email_host' => '',
@ -121,6 +89,14 @@ class ConfigSave extends FormRequest
'android_version' => '',
'android_download_url' => ''
];
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return self::RULES;
}
public function messages()
@ -131,7 +107,8 @@ class ConfigSave extends FormRequest
'subscribe_url.url' => '订阅URL格式不正确必须携带http(s)://',
'server_token.min' => '通讯密钥长度必须大于16位',
'tos_url.url' => '服务条款URL格式不正确必须携带http(s)://',
'telegram_discuss_link.url' => 'Telegram群组地址必须为URL格式必须携带http(s)://'
'telegram_discuss_link.url' => 'Telegram群组地址必须为URL格式必须携带http(s)://',
'logo.url' => 'LOGO URL格式不正确必须携带https(s)://'
];
}
}

View File

@ -16,7 +16,8 @@ class NoticeSave extends FormRequest
return [
'title' => 'required',
'content' => 'required',
'img_url' => 'nullable|url'
'img_url' => 'nullable|url',
'tags' => 'nullable|array'
];
}
@ -25,7 +26,8 @@ class NoticeSave extends FormRequest
return [
'title.required' => '标题不能为空',
'content.required' => '内容不能为空',
'img_url.url' => '图片URL格式不正确'
'img_url.url' => '图片URL格式不正确',
'tags.array' => '标签格式不正确'
];
}
}

View File

@ -26,7 +26,7 @@ class PlanSave extends FormRequest
'three_year_price' => 'nullable|integer',
'onetime_price' => 'nullable|integer',
'reset_price' => 'nullable|integer',
'reset_traffic_method' => 'nullable|integer|in:0,1,2'
'reset_traffic_method' => 'nullable|integer|in:0,1,2,3,4'
];
}

View File

@ -22,6 +22,8 @@ class ServerShadowsocksSave extends FormRequest
'port' => 'required',
'server_port' => 'required',
'cipher' => 'required|in:aes-128-gcm,aes-256-gcm,chacha20-ietf-poly1305',
'obfs' => 'nullable|in:http',
'obfs_settings' => 'nullable|array',
'tags' => 'nullable|array',
'rate' => 'required|numeric'
];
@ -40,7 +42,9 @@ class ServerShadowsocksSave extends FormRequest
'cipher.required' => '加密方式不能为空',
'tags.array' => '标签格式不正确',
'rate.required' => '倍率不能为空',
'rate.numeric' => '倍率格式不正确'
'rate.numeric' => '倍率格式不正确',
'obfs.in' => '混淆格式不正确',
'obfs_settings.array' => '混淆设置格式不正确'
];
}
}

View File

@ -111,8 +111,13 @@ class AdminRoute
$router->post('/payment/getPaymentForm', 'Admin\\PaymentController@getPaymentForm');
$router->post('/payment/save', 'Admin\\PaymentController@save');
$router->post('/payment/drop', 'Admin\\PaymentController@drop');
$router->post('/payment/show', 'Admin\\PaymentController@show');
// System
$router->get ('/system/getStatus', 'Admin\\SystemController@getStatus');
// Theme
$router->get ('/theme/getThemes', 'Admin\\ThemeController@getThemes');
$router->post('/theme/saveThemeConfig', 'Admin\\ThemeController@saveThemeConfig');
$router->post('/theme/getThemeConfig', 'Admin\\ThemeController@getThemeConfig');
});
}
}

View File

@ -18,7 +18,6 @@ class GuestRoute
$router->match(['get', 'post'], '/payment/notify/{method}/{uuid}', 'Guest\\PaymentController@notify');
// Comm
$router->get ('/comm/config', 'Guest\\CommController@config');
$router->get ('/comm/getHitokoto', 'Guest\\CommController@getHitokoto');
});
}
}

View File

@ -50,6 +50,7 @@ class StatServerJob implements ShouldQueue
$data = StatServer::where('record_at', $recordAt)
->where('server_id', $this->server->id)
->where('server_type', $this->protocol)
->lockForUpdate()
->first();
if ($data) {

View File

@ -52,7 +52,7 @@ class StatUserJob implements ShouldQueue
}
$data = StatUser::where('record_at', $recordAt)
->where('server_id', $this->server->id)
->where('server_rate', $this->server->rate)
->where('user_id', $this->userId)
->first();
if ($data) {
@ -67,8 +67,6 @@ class StatUserJob implements ShouldQueue
} else {
if (!StatUser::create([
'user_id' => $this->userId,
'server_id' => $this->server->id,
'server_type' => $this->protocol,
'server_rate' => $this->server->rate,
'u' => $this->u,
'd' => $this->d,

View File

@ -11,6 +11,7 @@ class Notice extends Model
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
'updated_at' => 'timestamp',
'tags' => 'array'
];
}

View File

@ -13,6 +13,7 @@ class ServerShadowsocks extends Model
'created_at' => 'timestamp',
'updated_at' => 'timestamp',
'group_id' => 'array',
'tags' => 'array'
'tags' => 'array',
'obfs_settings' => 'array'
];
}

View File

@ -28,7 +28,8 @@ class CoinPayments {
];
}
public function pay($order) {
public function pay($order)
{
// IPN notifications are slow, when the transaction is successful, we should return to the user center to avoid user confusion
$parseUrl = parse_url($order['return_url']);
@ -53,12 +54,12 @@ class CoinPayments {
return [
'type' => 1, // Redirect to url
'data' => 'https://www.coinpayments.net/index.php?' . $params_string,
'custom_result' => 'IPN OK'
'data' => 'https://www.coinpayments.net/index.php?' . $params_string
];
}
public function notify($params) {
public function notify($params)
{
if (!isset($params['merchant']) || $params['merchant'] != trim($this->config['coinpayments_merchant_id'])) {
abort(500, 'No or incorrect Merchant ID passed');
@ -75,24 +76,22 @@ class CoinPayments {
$hmac = hash_hmac("sha512", $request, trim($this->config['coinpayments_ipn_secret']));
// if (!hash_equals($hmac, $signHeader)) {
// if ($hmac != $_SERVER['HTTP_HMAC']) { <-- Use this if you are running a version of PHP below 5.6.0 without the hash_equals function
// $this->dieSendMessage(400, 'HMAC signature does not match');
// if ($hmac != $signHeader) { <-- Use this if you are running a version of PHP below 5.6.0 without the hash_equals function
// abort(400, 'HMAC signature does not match');
// }
if ($hmac != $signHeader) {
if (!hash_equals($hmac, $signHeader)) {
abort(400, 'HMAC signature does not match');
}
// HMAC Signature verified at this point, load some variables.
$status = $params['status'];
if ($status >= 100 || $status == 2) {
// payment is complete or queued for nightly payout, success
return [
'trade_no' => $params['item_number'],
'callback_no' => $params['txn_id']
'callback_no' => $params['txn_id'],
'custom_result' => 'IPN OK'
];
} else if ($status < 0) {
//payment error, this is usually final but payments will sometimes be reopened if there was no exchange rate conversion or with seller consent
@ -101,7 +100,5 @@ class CoinPayments {
//payment is pending, you can optionally add a note to the order page
die('IPN OK: pending');
}
}
}

View File

@ -24,6 +24,9 @@ class RouteServiceProvider extends ServiceProvider
public function boot()
{
//
if (config('v2board.force_https')) {
resolve(\Illuminate\Routing\UrlGenerator::class)->forceScheme('https');
}
parent::boot();
}

View File

@ -6,6 +6,8 @@ use App\Jobs\OrderHandleJob;
use App\Models\Order;
use App\Models\Plan;
use App\Models\User;
use App\Utils\CacheKey;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class OrderService
@ -163,18 +165,17 @@ class OrderService
private function getSurplusValueByOneTime(User $user, Order $order)
{
$lastOneTimeOrder = Order::where('user_id', $user->id)
->where('period', 'onetime')
->where('period', 'onetime_price')
->where('status', 3)
->orderBy('id', 'DESC')
->first();
if (!$lastOneTimeOrder) return;
$plan = Plan::find($lastOneTimeOrder->plan_id);
if (!$plan) return;
$trafficUnitPrice = $plan->onetime_price / $plan->transfer_enable;
if ($user->discount && $trafficUnitPrice) {
$trafficUnitPrice = $trafficUnitPrice - ($trafficUnitPrice * $user->discount / 100);
}
$notUsedTraffic = $plan->transfer_enable - (($user->u + $user->d) / 1073741824);
$nowUserTraffic = $user->transfer_enable / 1073741824;
if (!$nowUserTraffic) return;
$paidTotalAmount = ($lastOneTimeOrder->total_amount + $lastOneTimeOrder->balance_amount);
if (!$paidTotalAmount) return;
$trafficUnitPrice = $paidTotalAmount / $nowUserTraffic;
$notUsedTraffic = $nowUserTraffic - (($user->u + $user->d) / 1073741824);
$result = $trafficUnitPrice * $notUsedTraffic;
$orderModel = Order::where('user_id', $user->id)->where('period', '!=', 'reset_price')->where('status', 3);
$order->surplus_amount = $result > 0 ? $result : 0;

View File

@ -47,7 +47,7 @@ class PaymentService
return $this->payment->pay([
'notify_url' => $notifyUrl,
'return_url' => config('v2board.app_url', env('APP_URL')) . '/#/order/' . $order['trade_no'],
'return_url' => config('v2board.app_url') . '/#/order/' . $order['trade_no'],
'trade_no' => $order['trade_no'],
'total_amount' => $order['total_amount'],
'user_id' => $order['user_id'],

View File

@ -14,8 +14,6 @@ use Illuminate\Support\Facades\Cache;
class ServerService
{
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 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 getV2ray(User $user, $all = false):array
{
$servers = [];
@ -117,153 +115,11 @@ class ServerService
->where('banned', 0)
->select([
'id',
'email',
't',
'u',
'd',
'transfer_enable',
'uuid'
])
->get();
}
public function getV2RayConfig(int $nodeId, int $localPort)
{
$server = ServerV2ray::find($nodeId);
if (!$server) {
abort(500, '节点不存在');
}
$json = json_decode(self::V2RAY_CONFIG);
$json->log->loglevel = (int)config('v2board.server_log_enable') ? 'debug' : 'none';
$json->inbounds[1]->port = (int)$localPort;
$json->inbounds[0]->port = (int)$server->server_port;
$json->inbounds[0]->streamSettings->network = $server->network;
$this->setDns($server, $json);
$this->setNetwork($server, $json);
$this->setRule($server, $json);
$this->setTls($server, $json);
return $json;
}
public function getTrojanConfig(int $nodeId, int $localPort)
{
$server = ServerTrojan::find($nodeId);
if (!$server) {
abort(500, '节点不存在');
}
$json = json_decode(self::TROJAN_CONFIG);
$json->local_port = $server->server_port;
$json->ssl->sni = $server->server_name ? $server->server_name : $server->host;
$json->ssl->cert = "/root/.cert/server.crt";
$json->ssl->key = "/root/.cert/server.key";
$json->api->api_port = $localPort;
return $json;
}
private function setDns(ServerV2ray $server, object $json)
{
if ($server->dnsSettings) {
$dns = $server->dnsSettings;
if (isset($dns->servers)) {
array_push($dns->servers, '1.1.1.1');
array_push($dns->servers, 'localhost');
}
$json->dns = $dns;
$json->outbounds[0]->settings->domainStrategy = 'UseIP';
}
}
private function setNetwork(ServerV2ray $server, object $json)
{
if ($server->networkSettings) {
switch ($server->network) {
case 'tcp':
$json->inbounds[0]->streamSettings->tcpSettings = $server->networkSettings;
break;
case 'kcp':
$json->inbounds[0]->streamSettings->kcpSettings = $server->networkSettings;
break;
case 'ws':
$json->inbounds[0]->streamSettings->wsSettings = $server->networkSettings;
break;
case 'http':
$json->inbounds[0]->streamSettings->httpSettings = $server->networkSettings;
break;
case 'domainsocket':
$json->inbounds[0]->streamSettings->dsSettings = $server->networkSettings;
break;
case 'quic':
$json->inbounds[0]->streamSettings->quicSettings = $server->networkSettings;
break;
case 'grpc':
$json->inbounds[0]->streamSettings->grpcSettings = $server->networkSettings;
break;
}
}
}
private function setRule(ServerV2ray $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) {
$ruleSettings = $server->ruleSettings;
// domain
if (isset($ruleSettings->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->type = 'field';
$domainObj->domain = $domainRules;
$domainObj->outboundTag = 'block';
array_push($json->routing->rules, $domainObj);
}
if (!empty($protocolRules)) {
$protocolObj = new \StdClass();
$protocolObj->type = 'field';
$protocolObj->protocol = $protocolRules;
$protocolObj->outboundTag = 'block';
array_push($json->routing->rules, $protocolObj);
}
if (empty($domainRules) && empty($protocolRules)) {
$json->inbounds[0]->sniffing->enabled = false;
}
}
private function setTls(ServerV2ray $server, object $json)
{
if ((int)$server->tls) {
$tlsSettings = $server->tlsSettings;
$json->inbounds[0]->streamSettings->security = 'tls';
$tls = (object)[
'certificateFile' => '/root/.cert/server.crt',
'keyFile' => '/root/.cert/server.key'
];
$json->inbounds[0]->streamSettings->tlsSettings = new \StdClass();
if (isset($tlsSettings->serverName)) {
$json->inbounds[0]->streamSettings->tlsSettings->serverName = (string)$tlsSettings->serverName;
}
if (isset($tlsSettings->allowInsecure)) {
$json->inbounds[0]->streamSettings->tlsSettings->allowInsecure = (int)$tlsSettings->allowInsecure ? true : false;
}
$json->inbounds[0]->streamSettings->tlsSettings->certificates[0] = $tls;
}
}
public function log(int $userId, int $serverId, int $u, int $d, float $rate, string $method)
{
if (($u + $d) < 10240) return true;

View File

@ -0,0 +1,48 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
class ThemeService
{
private $path;
private $theme;
public function __construct($theme)
{
$this->theme = $theme;
$this->path = $path = public_path('theme/');
}
public function init()
{
$themeConfigFile = $this->path . "{$this->theme}/config.php";
if (!File::exists($themeConfigFile)) return;
$themeConfig = include($themeConfigFile);
$configs = $themeConfig['configs'];
$data = [];
foreach ($configs as $config) {
$data[$config['field_name']] = isset($config['default_value']) ? $config['default_value'] : '';
}
$data = var_export($data, 1);
try {
if (!File::put(base_path() . "/config/theme/{$this->theme}.php", "<?php\n return $data ;")) {
abort(500, "{$this->theme}初始化失败");
}
} catch (\Exception $e) {
abort(500, '请检查V2Board目录权限');
}
try {
Artisan::call('config:cache');
while (true) {
if (config("theme.{$this->theme}")) break;
}
} catch (\Exception $e) {
abort(500, "{$this->theme}初始化失败");
}
}
}

View File

@ -10,6 +10,27 @@ use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class TicketService {
public function reply($ticket, $message, $userId)
{
DB::beginTransaction();
$ticketMessage = TicketMessage::create([
'user_id' => $userId,
'ticket_id' => $ticket->id,
'message' => $message
]);
if ($userId !== $ticket->user_id) {
$ticket->reply_status = 0;
} else {
$ticket->reply_status = 1;
}
if (!$ticketMessage || !$ticket->save()) {
DB::rollback();
return false;
}
DB::commit();
return $ticketMessage;
}
public function replyByAdmin($ticketId, $message, $userId):void
{
$ticket = Ticket::where('id', $ticketId)
@ -24,7 +45,11 @@ class TicketService {
'ticket_id' => $ticket->id,
'message' => $message
]);
$ticket->last_reply_user_id = $userId;
if ($userId !== $ticket->user_id) {
$ticket->reply_status = 0;
} else {
$ticket->reply_status = 1;
}
if (!$ticketMessage || !$ticket->save()) {
DB::rollback();
abort(500, '工单回复失败');

View File

@ -15,6 +15,52 @@ use Illuminate\Support\Facades\DB;
class UserService
{
public function getResetDay(User $user)
{
if ($user->expired_at <= time() || $user->expired_at === NULL) return null;
// if reset method is not reset
if (isset($user->plan->reset_traffic_method) && $user->plan->reset_traffic_method === 2) return null;
if ((int)config('v2board.reset_traffic_method') === 0 ||
(isset($user->plan->reset_traffic_method) && $user->plan->reset_traffic_method === 0))
{
$day = date('d', $user->expired_at);
$today = date('d');
$lastDay = date('d', strtotime('last day of +0 months'));
return $lastDay - $today;
}
if ((int)config('v2board.reset_traffic_method') === 1 ||
(isset($user->plan->reset_traffic_method) && $user->plan->reset_traffic_method === 1))
{
$day = date('d', $user->expired_at);
$today = date('d');
$lastDay = date('d', strtotime('last day of +0 months'));
if ((int)$day >= (int)$today && (int)$day >= (int)$lastDay) {
return $lastDay - $today;
}
if ((int)$day >= (int)$today) {
return $day - $today;
} else {
return $lastDay - $today + $day;
}
}
if ((int)config('v2board.reset_traffic_method') === 3 ||
(isset($user->plan->reset_traffic_method) && $user->plan->reset_traffic_method === 3))
{
$nextYear = strtotime(date("Y-01-01", strtotime('+1 year')));
return (int)(($nextYear - time()) / 86400);
}
if ((int)config('v2board.reset_traffic_method') === 4 ||
(isset($user->plan->reset_traffic_method) && $user->plan->reset_traffic_method === 4))
{
$md = date('m-d', $user->expired_at);
$nowYear = strtotime(date("Y-{$md}"));
$nextYear = strtotime('+1 year', $nowYear);
return (int)(($nextYear - time()) / 86400);
}
return null;
}
public function isAvailable(User $user)
{
if (!$user->banned && $user->transfer_enable && ($user->expired_at > time() || $user->expired_at === NULL)) {

View File

@ -18,7 +18,8 @@ class CacheKey
'SERVER_SHADOWSOCKS_LAST_PUSH_AT' => 'ss节点最后推送时间',
'TEMP_TOKEN' => '临时令牌',
'LAST_SEND_EMAIL_REMIND_TRAFFIC' => '最后发送流量邮件提醒',
'SCHEDULE_LAST_CHECK_AT' => '计划任务最后检查时间'
'SCHEDULE_LAST_CHECK_AT' => '计划任务最后检查时间',
'REGISTER_IP_RATE_LIMIT' => '注册频率限制'
];
public static function get(string $key, $uniqueValue)

View File

@ -103,14 +103,12 @@ class Helper
}
}
public static function getSubscribeHost()
public static function getSubscribeUrl($path)
{
$subscribeUrl = config('v2board.app_url');
$subscribeUrls = explode(',', config('v2board.subscribe_url'));
if ($subscribeUrls && $subscribeUrls[0]) {
$subscribeUrl = $subscribeUrls[rand(0, count($subscribeUrls) - 1)];
}
return $subscribeUrl;
if ($subscribeUrl) return $subscribeUrl . $path;
return url($path);
}
public static function randomPort($range) {

View File

@ -11,26 +11,25 @@
],
"license": "MIT",
"require": {
"php": "^7.2.5|^8.0",
"php": "^7.3.0|^8.0",
"fideloper/proxy": "^4.4",
"fruitcake/laravel-cors": "^2.0",
"google/recaptcha": "^1.2",
"guzzlehttp/guzzle": "^6.3.1|^7.0.1",
"laravel/framework": "^7.29",
"laravel/horizon": "^4.3.5",
"guzzlehttp/guzzle": "^7.4.3",
"laravel/framework": "^8.0",
"laravel/horizon": "^5.9.6",
"laravel/tinker": "^2.5",
"linfo/linfo": "^4.0",
"lokielse/omnipay-wechatpay": "^3.0",
"php-curl-class/php-curl-class": "^8.6",
"stripe/stripe-php": "^7.36.1",
"symfony/yaml": "^4.3"
},
"require-dev": {
"facade/ignition": "^2.0",
"facade/ignition": "^2.3.6",
"fakerphp/faker": "^1.9.1",
"mockery/mockery": "^1.3.1",
"nunomaduro/collision": "^4.3",
"phpunit/phpunit": "^8.5.8|^9.3.3"
"phpunit/phpunit": "^9.0"
},
"config": {
"optimize-autoloader": true,

View File

@ -237,5 +237,5 @@ return [
| The only modification by laravel config
|
*/
'version' => '1.5.5.1646764814759'
'version' => '1.5.6.1652111181640'
];

2
config/theme/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.php
!.gitignore

View File

@ -84,14 +84,14 @@ CREATE TABLE `v2_knowledge` (
DROP TABLE IF EXISTS `v2_mail_log`;
CREATE TABLE `v2_mail_log` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`email` varchar(64) NOT NULL,
`subject` varchar(255) NOT NULL,
`template_name` varchar(255) NOT NULL,
`error` text,
`email` varchar(64) CHARACTER SET utf8 NOT NULL,
`subject` varchar(255) CHARACTER SET utf8 NOT NULL,
`template_name` varchar(255) CHARACTER SET utf8 NOT NULL,
`error` text CHARACTER SET utf8,
`created_at` int(11) NOT NULL,
`updated_at` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `v2_notice`;
@ -101,6 +101,7 @@ CREATE TABLE `v2_notice` (
`content` text NOT NULL,
`show` tinyint(1) NOT NULL DEFAULT '0',
`img_url` varchar(255) DEFAULT NULL,
`tags` varchar(255) DEFAULT NULL,
`created_at` int(11) NOT NULL,
`updated_at` int(11) NOT NULL,
PRIMARY KEY (`id`)
@ -120,6 +121,7 @@ CREATE TABLE `v2_order` (
`trade_no` varchar(36) NOT NULL,
`callback_no` varchar(255) DEFAULT NULL,
`total_amount` int(11) NOT NULL,
`handling_amount` int(11) DEFAULT NULL,
`discount_amount` int(11) DEFAULT NULL,
`surplus_amount` int(11) DEFAULT NULL COMMENT '剩余价值',
`refund_amount` int(11) DEFAULT NULL COMMENT '退款金额',
@ -145,6 +147,8 @@ CREATE TABLE `v2_payment` (
`icon` varchar(255) DEFAULT NULL,
`config` text NOT NULL,
`notify_domain` varchar(128) DEFAULT NULL,
`handling_fee_fixed` int(11) DEFAULT NULL,
`handling_fee_percent` decimal(5,2) DEFAULT NULL,
`enable` tinyint(1) NOT NULL DEFAULT '0',
`sort` int(11) DEFAULT NULL,
`created_at` int(11) NOT NULL,
@ -158,11 +162,11 @@ CREATE TABLE `v2_plan` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`group_id` int(11) NOT NULL,
`transfer_enable` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 NOT NULL,
`show` tinyint(1) NOT NULL DEFAULT '0',
`sort` int(11) DEFAULT NULL,
`renew` tinyint(1) NOT NULL DEFAULT '1',
`content` text,
`content` text CHARACTER SET utf8mb4,
`month_price` int(11) DEFAULT NULL,
`quarter_price` int(11) DEFAULT NULL,
`half_year_price` int(11) DEFAULT NULL,
@ -200,6 +204,8 @@ CREATE TABLE `v2_server_shadowsocks` (
`port` int(11) NOT NULL,
`server_port` int(11) NOT NULL,
`cipher` varchar(255) NOT NULL,
`obfs` char(11) DEFAULT NULL,
`obfs_settings` varchar(255) DEFAULT NULL,
`show` tinyint(4) NOT NULL DEFAULT '0',
`sort` int(11) DEFAULT NULL,
`created_at` int(11) NOT NULL,
@ -242,7 +248,6 @@ CREATE TABLE `v2_server_v2ray` (
`tags` varchar(255) DEFAULT NULL,
`rate` varchar(11) NOT NULL,
`network` text NOT NULL,
`settings` text,
`rules` text,
`networkSettings` text,
`tlsSettings` text,
@ -277,8 +282,8 @@ CREATE TABLE `v2_stat_server` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`server_id` int(11) NOT NULL COMMENT '节点id',
`server_type` char(11) NOT NULL COMMENT '节点类型',
`u` varchar(255) NOT NULL,
`d` varchar(255) NOT NULL,
`u` bigint(20) NOT NULL,
`d` bigint(20) NOT NULL,
`record_type` char(1) NOT NULL COMMENT 'd day m month',
`record_at` int(11) NOT NULL COMMENT '记录时间',
`created_at` int(11) NOT NULL,
@ -294,8 +299,6 @@ DROP TABLE IF EXISTS `v2_stat_user`;
CREATE TABLE `v2_stat_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`server_id` int(11) NOT NULL,
`server_type` char(11) NOT NULL,
`server_rate` decimal(10,2) NOT NULL,
`u` bigint(20) NOT NULL,
`d` bigint(20) NOT NULL,
@ -304,9 +307,10 @@ CREATE TABLE `v2_stat_user` (
`created_at` int(11) NOT NULL,
`updated_at` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `server_id` (`server_id`),
UNIQUE KEY `server_rate_user_id_record_at` (`server_rate`,`user_id`,`record_at`),
KEY `user_id` (`user_id`),
KEY `record_at` (`record_at`)
KEY `record_at` (`record_at`),
KEY `server_rate` (`server_rate`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@ -314,10 +318,10 @@ DROP TABLE IF EXISTS `v2_ticket`;
CREATE TABLE `v2_ticket` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`last_reply_user_id` int(11) NOT NULL,
`subject` varchar(255) NOT NULL,
`level` tinyint(1) NOT NULL,
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0:已开启 1:已关闭',
`reply_status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '0:待回复 1:已回复',
`created_at` int(11) NOT NULL,
`updated_at` int(11) NOT NULL,
PRIMARY KEY (`id`)
@ -362,8 +366,8 @@ CREATE TABLE `v2_user` (
`uuid` varchar(36) NOT NULL,
`group_id` int(11) DEFAULT NULL,
`plan_id` int(11) DEFAULT NULL,
`remind_expire` tinyint(4) DEFAULT '0',
`remind_traffic` tinyint(4) DEFAULT '0',
`remind_expire` tinyint(4) DEFAULT '1',
`remind_traffic` tinyint(4) DEFAULT '1',
`token` char(32) NOT NULL,
`remarks` text,
`expired_at` bigint(20) DEFAULT '0',
@ -374,4 +378,4 @@ CREATE TABLE `v2_user` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 2022-03-04 16:25:43
-- 2022-06-10 17:12:02

View File

@ -459,10 +459,6 @@ ALTER TABLE `v2_plan`
ALTER TABLE `v2_server`
RENAME TO `v2_server_v2ray`;
ALTER TABLE `v2_user`
CHANGE `remind_expire` `remind_expire` tinyint(4) NULL DEFAULT '0' AFTER `plan_id`,
CHANGE `remind_traffic` `remind_traffic` tinyint(4) NULL DEFAULT '0' AFTER `remind_expire`;
ALTER TABLE `v2_payment`
ADD `icon` varchar(255) COLLATE 'utf8mb4_general_ci' NULL AFTER `name`;
@ -513,3 +509,80 @@ ALTER TABLE `v2_stat_user`
ADD INDEX `server_id` (`server_id`),
ADD INDEX `user_id` (`user_id`),
ADD INDEX `record_at` (`record_at`);
ALTER TABLE `v2_stat_server`
CHANGE `u` `u` bigint NOT NULL AFTER `server_type`,
CHANGE `d` `d` bigint NOT NULL AFTER `u`;
ALTER TABLE `v2_payment`
ADD `handling_fee_fixed` int(11) NULL AFTER `notify_domain`,
ADD `handling_fee_percent` decimal(5,2) NULL AFTER `handling_fee_fixed`;
ALTER TABLE `v2_order`
ADD `handling_amount` int(11) NULL AFTER `total_amount`;
DELIMITER $$
DROP PROCEDURE IF EXISTS `path-2022-03-29` $$
CREATE PROCEDURE `path-2022-03-29`()
BEGIN
DECLARE IndexIsThere INTEGER;
SELECT COUNT(1) INTO IndexIsThere
FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_name = 'v2_stat_user'
AND index_name = 'server_id';
IF IndexIsThere != 0 THEN
TRUNCATE TABLE `v2_stat_user`;
END IF;
END $$
DELIMITER ;
CALL `path-2022-03-29`();
DROP PROCEDURE IF EXISTS `path-2022-03-29`;
ALTER TABLE `v2_stat_user`
ADD UNIQUE `server_rate_user_id_record_at` (`server_rate`, `user_id`, `record_at`);
ALTER TABLE `v2_stat_user`
ADD INDEX `server_rate` (`server_rate`);
ALTER TABLE `v2_stat_user`
DROP INDEX `server_id_user_id_record_at`;
ALTER TABLE `v2_stat_user`
DROP INDEX `server_id`;
ALTER TABLE `v2_stat_user`
DROP `server_id`;
ALTER TABLE `v2_stat_user`
DROP `server_type`;
ALTER TABLE `v2_notice`
ADD `tags` varchar(255) COLLATE 'utf8_general_ci' NULL AFTER `img_url`;
ALTER TABLE `v2_ticket`
ADD `reply_status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '0:待回复 1:已回复' AFTER `status`;
ALTER TABLE `v2_server_v2ray`
DROP `settings`;
ALTER TABLE `v2_ticket`
DROP `last_reply_user_id`;
ALTER TABLE `v2_server_shadowsocks`
ADD `obfs` char(11) NULL AFTER `cipher`,
ADD `obfs_settings` varchar(255) NULL AFTER `obfs`;
ALTER TABLE `v2_plan`
CHANGE `name` `name` varchar(255) COLLATE 'utf8mb4_general_ci' NOT NULL AFTER `transfer_enable`,
CHANGE `content` `content` text COLLATE 'utf8mb4_general_ci' NULL AFTER `renew`;
ALTER TABLE `v2_mail_log`
COLLATE 'utf8mb4_general_ci';
ALTER TABLE `v2_mail_log`
CHANGE `email` `email` varchar(64) NOT NULL AFTER `id`,
CHANGE `subject` `subject` varchar(255) NOT NULL AFTER `email`,
CHANGE `template_name` `template_name` varchar(255) NOT NULL AFTER `subject`,
CHANGE `error` `error` text NULL AFTER `template_name`;

View File

@ -1,4 +1,10 @@
#!/bin/bash
rm -rf composer.phar
wget https://github.com/composer/composer/releases/latest/download/composer.phar -O composer.phar
php composer.phar install -vvv
php artisan v2board:install
if [ -f "/etc/init.d/bt" ]; then
chown -R www $(pwd);
fi

View File

@ -10,5 +10,6 @@ window.settings = {
color: 'default'
},
// 背景
background_url: ''
background_url: '',
logo: ''
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,3 @@
*
!*v2board
/*
!v2board
!.gitignore

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 @@ window.settings.i18n['en-US'] = {
'重置流量包': 'Data Reset Package',
'待支付': 'Pending Payment',
'开通中': 'Pending Active',
'已取消': 'Cancelled',
'已取消': 'Canceled',
'已完成': 'Completed',
'已折抵': 'Converted',
'待确认': 'Pending',
@ -54,7 +54,7 @@ window.settings.i18n['en-US'] = {
'佣金将会在确认后会到达你的佣金账户。': 'The commission will reach your commission account after review.',
'邀请码管理': 'Invitation Code Management',
'生成邀请码': 'Generate invitation code',
'邀请明细': 'Invitation Details',
'佣金发放记录': 'Commission Income Record',
'复制成功': 'Copied successfully',
'密码': 'Password',
'登入': 'Login',
@ -82,8 +82,8 @@ window.settings.i18n['en-US'] = {
'等待支付中': 'Waiting for payment',
'开通中': 'Pending',
'订单系统正在进行处理请稍等1-3分钟。': 'Order system is being processed, please wait 1 to 3 minutes.',
'已取消': 'Cancelled',
'订单由于超时支付已被取消。': 'The order has been cancelled due to overtime payment.',
'已取消': 'Canceled',
'订单由于超时支付已被取消。': 'The order has been canceled due to overtime payment.',
'已完成': 'Success',
'订单已支付并开通。': 'The order has been paid and the service is activated.',
'选择订阅': 'Select a Subscription',
@ -96,7 +96,6 @@ window.settings.i18n['en-US'] = {
'订单总额': 'Order Total',
'下单': 'Order',
'总计': 'Total',
'订阅变更须知': 'Attention subscription changes',
'变更订阅会导致当前订阅被新订阅覆盖,请注意。': 'Attention please, change subscription will overwrite your current subscription.',
'该订阅无法续费': 'This subscription cannot be renewed',
'选择其他订阅': 'Choose another subscription',
@ -239,10 +238,32 @@ window.settings.i18n['en-US'] = {
'于 {date} 到期,距离到期还有 {day} 天。': 'Will expire on {date}, {day} days before expiration, ',
'Telegram 讨论组': 'Telegram Discussion Group',
'立即加入': 'Join Now',
'续费': 'Renewal',
'购买': 'Purchase',
'该订阅无法续费,仅允许新用户购买': 'This subscription cannot be renewed and is only available to new users.',
'重置当月流量': 'Reset current month usage',
'流量明细仅保留近月数据以供查询。': 'Only keep the most recent month\'s usage for checking the transfer data details.',
'扣费倍率': 'Fee deduction rate'
'扣费倍率': 'Fee deduction rate',
'支付手续费': 'Payment fee',
'续费订阅': 'Renewal Subscription',
'学习如何使用': 'Learn how to use',
'快速将节点导入对应客户端进行使用': 'Quickly export subscription into the client app',
'对您当前的订阅进行续费': 'Renew your current subscription',
'对您当前的订阅进行购买': 'Purchase your current subscription',
'捷径': 'Shortcut',
'不会使用,查看使用教程': 'I am a newbie, view the tutorial',
'使用支持扫码的客户端进行订阅': 'Use a client app that supports scanning QR code to subscribe',
'扫描二维码订阅': 'Scan QR code to subscribe',
'续费': 'Renewal',
'购买': 'Purchase',
'查看教程': 'View Tutorial',
'注意': 'Attention',
'你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?': 'You still have an unpaid order. You need to cancel it before purchasing. Are you sure you want to cancel the previous order?',
'确定取消': 'Confirm Cancel',
'返回我的订单': 'Back to My Order',
'如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?': 'If you have already paid, canceling the order may cause the payment to fail. Are you sure you want to cancel the order?',
'选择最适合你的计划': 'Choose the right plan for you',
'全部': 'All',
'按周期': 'By Cycle',
'一次性': 'One Time',
'遇到问题': 'I have a problem',
'遇到问题可以通过工单与我们沟通': 'If you have any problems, you can contact us via ticket'
};

View File

@ -54,12 +54,12 @@ window.settings.i18n['ja-JP'] = {
'佣金将会在确认后会到达你的佣金账户。': 'コミッションは承認処理完了後にカウントされます',
'邀请码管理': '招待コードの管理',
'生成邀请码': '招待コードを生成',
'邀请明细': '招待済みリスト',
'佣金发放记录': 'コミッション履歴',
'复制成功': 'クリップボードにコピーされました',
'密码': 'パスワード',
'登入': 'ログイン',
'注册': '新規登録',
'忘记密码': 'パスワードをお忘れの方は[こちら]',
'忘记密码': 'パスワードをお忘れの方',
'# 订单号': '受注番号',
'周期': 'サイクル',
'订单金额': 'ご注文金額',
@ -96,7 +96,6 @@ window.settings.i18n['ja-JP'] = {
'订单总额': 'ご注文の合計金額',
'下单': 'チェックアウト',
'总计': '合計',
'订阅变更须知': 'プラン変更のご注意',
'变更订阅会导致当前订阅被新订阅覆盖,请注意。': 'プランを変更なされます場合は、既存のプランが新規プランによって上書きされます、ご注意下さい',
'该订阅无法续费': '該当プランは継続利用できません',
'选择其他订阅': 'その他のプランを選択',
@ -239,10 +238,32 @@ window.settings.i18n['ja-JP'] = {
'于 {date} 到期,距离到期还有 {day} 天。': 'ご利用期限は {date} まで,期限まであと {day} 日',
'Telegram 讨论组': 'Telegramグループ',
'立即加入': '今すぐ参加',
'续费': '継続料金のお支払い',
'购买': '購入',
'该订阅无法续费,仅允许新用户购买': '該当プランは継続利用できません、新規ユーザーのみが購入可能です',
'重置当月流量': '使用済みデータ量のカウントリセット',
'流量明细仅保留近月数据以供查询。': 'データ通信明細は当月分のみ表示されます',
'扣费倍率': '適応レート'
'扣费倍率': '適応レート',
'支付手续费': 'お支払い手数料',
'续费订阅': '续费订阅',
'学习如何使用': '学习如何使用',
'快速将节点导入对应客户端进行使用': '快速将节点导入对应客户端进行使用',
'对您当前的订阅进行续费': '对您当前的订阅进行续费',
'对您当前的订阅进行购买': '对您当前的订阅进行购买',
'捷径': '捷径',
'不会使用,查看使用教程': '不会使用,查看使用教程',
'使用支持扫码的客户端进行订阅': '使用支持扫码的客户端进行订阅',
'扫描二维码订阅': '扫描二维码订阅',
'续费': '续费',
'购买': '购买',
'查看教程': '查看教程',
'注意': '注意',
'你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?': '你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?',
'确定取消': '确定取消',
'返回我的订单': '返回我的订单',
'如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?': '如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?',
'选择最适合你的计划': '选择最适合你的计划',
'全部': '全部',
'按周期': '按周期',
'一次性': '一次性',
'遇到问题': '遇到问题',
'遇到问题可以通过工单与我们沟通': '遇到问题可以通过工单与我们沟通'
};

View File

@ -54,7 +54,7 @@ window.settings.i18n['ko-KR'] = {
'佣金将会在确认后会到达你的佣金账户。': '수수료는 검토 후 수수료 계정에서 확인할 수 있습니다',
'邀请码管理': '초청 코드 관리',
'生成邀请码': '초청 코드 생성하기',
'邀请明细': '초청 세부사항',
'佣金发放记录': '佣金发放记录',
'复制成功': '성공적으로 복사 됨',
'密码': '비밀번호',
'登入': '로그인',
@ -96,7 +96,6 @@ window.settings.i18n['ko-KR'] = {
'订单总额': '전체 주문',
'下单': '주문',
'总计': '전체',
'订阅变更须知': '구독 변경 사항 주의',
'变更订阅会导致当前订阅被新订阅覆盖,请注意。': '주의하십시오. 구독을 변경하면 현재 구독을 덮어씁니다',
'该订阅无法续费': '이 구독은 갱신할 수 없습니다.',
'选择其他订阅': '다른 구독 선택',
@ -239,10 +238,32 @@ window.settings.i18n['ko-KR'] = {
'于 {date} 到期,距离到期还有 {day} 天。': '{day}까지, 만료 {day}일 전.',
'Telegram 讨论组': '텔레그램으로 문의하세요',
'立即加入': '지금 가입하세요',
'续费': '고쳐쓰기',
'购买': '구매',
'该订阅无法续费,仅允许新用户购买': '이 구독은 갱신할 수 없습니다. 신규 사용자만 구매할 수 있습니다.',
'重置当月流量': '이번 달 트래픽 초기화',
'流量明细仅保留近月数据以供查询。': '귀하의 트래픽 세부 정보는 최근 몇 달 동안만 유지됩니다',
'扣费倍率': '수수료 공제율'
'扣费倍率': '수수료 공제율',
'支付手续费': '支付手续费',
'续费订阅': '续费订阅',
'学习如何使用': '学习如何使用',
'快速将节点导入对应客户端进行使用': '快速将节点导入对应客户端进行使用',
'对您当前的订阅进行续费': '对您当前的订阅进行续费',
'对您当前的订阅进行购买': '对您当前的订阅进行购买',
'捷径': '捷径',
'不会使用,查看使用教程': '不会使用,查看使用教程',
'使用支持扫码的客户端进行订阅': '使用支持扫码的客户端进行订阅',
'扫描二维码订阅': '扫描二维码订阅',
'续费': '续费',
'购买': '购买',
'查看教程': '查看教程',
'注意': '注意',
'你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?': '你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?',
'确定取消': '确定取消',
'返回我的订单': '返回我的订单',
'如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?': '如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?',
'选择最适合你的计划': '选择最适合你的计划',
'全部': '全部',
'按周期': '按周期',
'一次性': '一次性',
'遇到问题': '遇到问题',
'遇到问题可以通过工单与我们沟通': '遇到问题可以通过工单与我们沟通'
};

View File

@ -54,7 +54,7 @@ window.settings.i18n['vi-VN'] = {
'佣金将会在确认后会到达你的佣金账户。': 'Sau khi xác nhận tiền hoa hồng sẽ gửi đến tài khoản hoa hồng của bạn.',
'邀请码管理': 'Quản lý mã mời',
'生成邀请码': 'Tạo mã mời',
'邀请明细': 'Chi tiết mời',
'佣金发放记录': 'Hồ sơ hoa hồng',
'复制成功': 'Sao chép thành công',
'密码': 'Mật khẩu',
'登入': 'Đăng nhập',
@ -96,7 +96,6 @@ window.settings.i18n['vi-VN'] = {
'订单总额': 'Tổng tiền đơn hàng',
'下单': 'Đặt hàng',
'总计': 'Tổng',
'订阅变更须知': 'Thông báo thay đổi gói dịch vụ',
'变更订阅会导致当前订阅被新订阅覆盖,请注意。': 'Việc thay đổi gói dịch vụ sẽ thay thế gói hiện tại bằng gói mới, xin lưu ý.',
'该订阅无法续费': 'Gói này không thể gia hạn',
'选择其他订阅': 'Chọn gói dịch vụ khác',
@ -239,10 +238,32 @@ window.settings.i18n['vi-VN'] = {
'于 {date} 到期,距离到期还有 {day} 天。': 'Hết hạn vào {date}, còn {day} ngày.',
'Telegram 讨论组': 'Nhóm Telegram',
'立即加入': 'Vào ngay',
'续费': 'Gia hạn',
'购买': 'Mua',
'该订阅无法续费,仅允许新用户购买': 'Đăng ký này không thể gia hạn, chỉ người dùng mới được phép mua',
'重置当月流量': 'Đặt lại dung lượng tháng hiện tại',
'流量明细仅保留近月数据以供查询。': 'Chi tiết dung lượng chỉ lưu dữ liệu của những tháng gần đây để truy vấn.',
'扣费倍率': 'Tỷ lệ khấu trừ'
'扣费倍率': 'Tỷ lệ khấu trừ',
'支付手续费': 'Phí thủ tục',
'续费订阅': '续费订阅',
'学习如何使用': '学习如何使用',
'快速将节点导入对应客户端进行使用': '快速将节点导入对应客户端进行使用',
'对您当前的订阅进行续费': '对您当前的订阅进行续费',
'对您当前的订阅进行购买': '对您当前的订阅进行购买',
'捷径': '捷径',
'不会使用,查看使用教程': '不会使用,查看使用教程',
'使用支持扫码的客户端进行订阅': '使用支持扫码的客户端进行订阅',
'扫描二维码订阅': '扫描二维码订阅',
'续费': '续费',
'购买': '购买',
'查看教程': '查看教程',
'注意': '注意',
'你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?': '你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?',
'确定取消': '确定取消',
'返回我的订单': '返回我的订单',
'如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?': '如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?',
'选择最适合你的计划': '选择最适合你的计划',
'全部': '全部',
'按周期': '按周期',
'一次性': '一次性',
'遇到问题': '遇到问题',
'遇到问题可以通过工单与我们沟通': '遇到问题可以通过工单与我们沟通'
};

View File

@ -51,10 +51,10 @@ window.settings.i18n['zh-CN'] = {
'已注册用户数': '已注册用户数',
'佣金比例': '佣金比例',
'确认中的佣金': '确认中的佣金',
'佣金将会在确认后会到达你的佣金账户。': '佣金将会在确认后到达您的佣金账户。',
'佣金将会在确认后会到达你的佣金账户。': '佣金将会在确认后到达您的佣金账户。',
'邀请码管理': '邀请码管理',
'生成邀请码': '生成邀请码',
'邀请明细': '邀请明细',
'佣金发放记录': '佣金发放记录',
'复制成功': '复制成功',
'密码': '密码',
'登入': '登入',
@ -96,8 +96,7 @@ window.settings.i18n['zh-CN'] = {
'订单总额': '订单总额',
'下单': '下单',
'总计': '总计',
'订阅变更须知': '订阅变更须知',
'变更订阅会导致当前订阅被新订阅覆盖,请注意。': '变更订阅会导致当前订阅被新订阅覆盖,请注意。',
'变更订阅会导致当前订阅被新订阅覆盖,请注意。': '请注意,变更订阅会导致当前订阅被新订阅覆盖。',
'该订阅无法续费': '该订阅无法续费',
'选择其他订阅': '选择其它订阅',
'我的钱包': '我的钱包',
@ -239,10 +238,32 @@ window.settings.i18n['zh-CN'] = {
'于 {date} 到期,距离到期还有 {day} 天。': '于 {date} 到期,距离到期还有 {day} 天。',
'Telegram 讨论组': 'Telegram 讨论组',
'立即加入': '立即加入',
'续费': '续费',
'购买': '购买',
'该订阅无法续费,仅允许新用户购买': '该订阅无法续费,仅允许新用户购买',
'重置当月流量': '重置当月流量',
'流量明细仅保留近月数据以供查询。': '流量明细仅保留近一个月数据以供查询。',
'扣费倍率': '扣费倍率'
'扣费倍率': '扣费倍率',
'支付手续费': '支付手续费',
'续费订阅': '续费订阅',
'学习如何使用': '学习如何使用',
'快速将节点导入对应客户端进行使用': '快速将节点导入对应客户端进行使用',
'对您当前的订阅进行续费': '对您当前的订阅进行续费',
'对您当前的订阅进行购买': '对您当前的订阅进行购买',
'捷径': '捷径',
'不会使用,查看使用教程': '不会使用,查看使用教程',
'使用支持扫码的客户端进行订阅': '使用支持扫码的客户端进行订阅',
'扫描二维码订阅': '扫描二维码订阅',
'续费': '续费',
'购买': '购买',
'查看教程': '查看教程',
'注意': '注意',
'你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?': '您还有未完成的订单,购买前需要先取消,确定要取消之前的订单吗?',
'确定取消': '确定取消',
'返回我的订单': '返回我的订单',
'如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?': '如果您已经付款,取消订单可能会导致支付失败,确定要取消订单吗?',
'选择最适合你的计划': '选择最适合您的计划',
'全部': '全部',
'按周期': '按周期',
'一次性': '一次性',
'遇到问题': '遇到问题',
'遇到问题可以通过工单与我们沟通': '遇到问题可以通过工单与我们沟通'
};

View File

@ -1,15 +1,15 @@
window.settings.i18n['zh-TW'] = {
'请求失败': '請求失敗',
'月付': '月繳制',
'季付': '季',
'半年付': '半年',
'季付': '季',
'半年付': '半年',
'年付': '年繳',
'两年付': '两年付',
'三年付': '三年',
'两年付': '兩年繳',
'三年付': '三年',
'一次性': '一次性',
'重置流量包': '重置流量包',
'待支付': '待支付',
'开通中': '通中',
'开通中': '通中',
'已取消': '已取消',
'已完成': '已完成',
'已折抵': '已折抵',
@ -28,221 +28,242 @@ window.settings.i18n['zh-TW'] = {
'我的订单': '我的訂單',
'我的邀请': '我的邀請',
'用户': '使用者',
'我的工单': '我的工',
'流量明细': '流量明',
'我的工单': '我的工',
'流量明细': '流量明',
'使用文档': '說明文件',
'绑定Telegram获取更多服务': '绑定Telegram获取更多服务',
'点击这里进行绑定': '点击这里进行绑定',
'绑定Telegram获取更多服务': '綁定 Telegram 獲取更多服務',
'点击这里进行绑定': '點擊這裡進行綁定',
'公告': '公告',
'总览': '總覽',
'该订阅长期有效': '该订阅长期有效',
'该订阅长期有效': '該訂閱長期有效',
'已过期': '已過期',
'已用 {used} / 总计 {total}': '已用 {used} / 总计 {total}',
'已用 {used} / 总计 {total}': '已用 {used} / 總計 {total}',
'查看订阅': '查看訂閱',
'邮箱': '郵箱',
'邮箱验证码': '邮箱验证码',
'邮箱验证码': '郵箱驗證碼',
'发送': '傳送',
'重置密码': '重設密碼',
'返回登入': '返回登',
'返回登入': '返回登',
'邀请码': '邀請碼',
'复制链接': '複製鏈接',
'完成时间': '完成時間',
'佣金': '佣金',
'已注册用户数': '已注册用户数',
'已注册用户数': '已註冊用戶數',
'佣金比例': '佣金比例',
'确认中的佣金': '确认中的佣金',
'佣金将会在确认后会到达你的佣金账户。': '佣金将会在确认后会到达你的佣金账户。',
'邀请码管理': '邀请码管理',
'生成邀请码': '生成邀请码',
'邀请明细': '邀请明细',
'确认中的佣金': '確認中的佣金',
'佣金将会在确认后会到达你的佣金账户。': '佣金將會在確認後到達您的佣金帳戶。',
'邀请码管理': '邀請碼管理',
'生成邀请码': '生成邀請碼',
'佣金发放记录': '佣金發放記錄',
'复制成功': '複製成功',
'密码': '密碼',
'登入': '登入',
'注册': '註冊',
'忘记密码': '忘記密碼',
'# 订单号': '# 订单号',
'# 订单号': '# 訂單號',
'周期': '週期',
'订单金额': '訂單金額',
'订单状态': '訂單狀態',
'创建时间': '创建时间',
'创建时间': '創建時間',
'操作': '操作',
'查看详情': '查看情',
'请选择支付方式': '请选择支付方式',
'请检查信用卡支付信息': '请检查信用卡支付信息',
'订单详情': '订单详情',
'查看详情': '查看情',
'请选择支付方式': '請選擇支付方式',
'请检查信用卡支付信息': '請檢查信用卡支付資訊',
'订单详情': '訂單詳情',
'折扣': '折扣',
'折抵': '折抵',
'退款': '退款',
'支付方式': '支付方式',
'填写信用卡支付信息': '填写信用卡支付信息',
'您的信用卡信息只会被用作当次扣款,系统并不会保存,这是我们认为最安全的。': '您的信用卡信息只会被用作当次扣款,系统并不会保存,这是我们认为最安全的。',
'订单总额': '订单总额',
'总计': '总计',
'结账': '结账',
'填写信用卡支付信息': '填寫信用卡支付資訊',
'您的信用卡信息只会被用作当次扣款,系统并不会保存,这是我们认为最安全的。': '您的信用卡資訊只會被用作當次扣款,系統並不會保存,我們認為這是最安全的。',
'订单总额': '訂單總額',
'总计': '總計',
'结账': '結賬',
'等待支付中': '等待支付中',
'开通中': '通中',
'订单系统正在进行处理请稍等1-3分钟。': '订单系统正在进行处理请稍等1-3分钟。',
'开通中': '通中',
'订单系统正在进行处理请稍等1-3分钟。': '訂單系統正在進行處理,請稍等 1-3 分鐘。',
'已取消': '已取消',
'订单由于超时支付已被取消。': '订单由于超时支付已被取消。',
'订单由于超时支付已被取消。': '訂單由於支付超時已被取消',
'已完成': '已完成',
'订单已支付并开通。': '订单已支付并开通。',
'选择订阅': '选择订阅',
'立即订阅': '立即订阅',
'配置订阅': '配置订阅',
'订单已支付并开通。': '訂單已支付並開通',
'选择订阅': '選擇訂閱',
'立即订阅': '立即訂閱',
'配置订阅': '配置訂閱',
'折扣': '折扣',
'付款周期': '付款周期',
'有优惠券?': '有优惠券?',
'验证': '验证',
'订单总额': '订单总额',
'下单': '下单',
'总计': '总计',
'订阅变更须知': '订阅变更须知',
'变更订阅会导致当前订阅被新订阅覆盖,请注意。': '变更订阅会导致当前订阅被新订阅覆盖,请注意。',
'该订阅无法续费': '该订阅无法续费',
'选择其他订阅': '选择其他订阅',
'我的钱包': '我的钱包',
'账户余额(仅消费)': '账户余额(仅消费)',
'推广佣金(可提现)': '推广佣金(可提现)',
'钱包组成部分': '钱包组成部分',
'划转': '划转',
'推广佣金提现': '推广佣金提现',
'修改密码': '修改密码',
'保存': '保存',
'旧密码': '旧密码',
'新密码': '新密码',
'请输入旧密码': '请输入旧密码',
'请输入新密码': '请输入新密码',
'付款周期': '付款週期',
'有优惠券?': '有優惠券?',
'验证': '驗證',
'订单总额': '訂單總額',
'下单': '下單',
'总计': '總計',
'变更订阅会导致当前订阅被新订阅覆盖,请注意。': '請注意,變更訂閱會導致當前訂閱被新訂閱覆蓋。',
'该订阅无法续费': '該訂閱無法續費',
'选择其他订阅': '選擇其它訂閱',
'我的钱包': '我的錢包',
'账户余额(仅消费)': '賬戶餘額(僅消費)',
'推广佣金(可提现)': '推廣佣金(可提現)',
'钱包组成部分': '錢包組成部分',
'划转': '劃轉',
'推广佣金提现': '推廣佣金提現',
'修改密码': '修改密碼',
'保存': '儲存',
'旧密码': '舊密碼',
'新密码': '新密碼',
'请输入旧密码': '請輸入舊密碼',
'请输入新密码': '請輸入新密碼',
'通知': '通知',
'到期邮件提醒': '到期件提醒',
'流量邮件提醒': '流量件提醒',
'绑定Telegram': '绑定Telegram',
'立即开始': '立即始',
'重置订阅信息': '重置订阅信息',
'到期邮件提醒': '到期件提醒',
'流量邮件提醒': '流量件提醒',
'绑定Telegram': '綁定 Telegram',
'立即开始': '立即始',
'重置订阅信息': '重置訂閲資訊',
'重置': '重置',
'确定要重置订阅信息?': '确定要重置订阅信息',
'如果你的订阅地址或信息泄露可以进行此操作。重置后你的UUID及订阅将会变更需要重新进行订阅。': '如果你的订阅地址或信息泄露可以进行此操作。重置后你的UUID及订阅将会变更需要重新进行订阅。',
'确定要重置订阅信息?': '確定要重置訂閱資訊',
'如果你的订阅地址或信息泄露可以进行此操作。重置后你的UUID及订阅将会变更需要重新进行订阅。': '如果您的訂閱位址或資訊發生洩露可以執行此操作。重置後您的 UUID 及訂閱將會變更,需要重新導入訂閱。',
'重置成功': '重置成功',
'两次新密码输入不同': '两次新密码输入不同',
'两次密码输入不同': '两次密码输入不同',
'两次新密码输入不同': '兩次新密碼輸入不同',
'两次密码输入不同': '兩次密碼輸入不同',
'邮箱': '郵箱',
'邮箱验证码': '邮箱验证码',
'发送': '送',
'邀请码': '邀请码',
'邀请码(选填)': '邀请码(选填)',
'注册': '注册',
'返回登入': '返回登',
'我已阅读并同意 <a target="_blank" href="{url}">服务条款</a>': '我已阅读并同意 <a target="_blank" href="{url}">服务条款</a>',
'请同意服务条款': '请同意服务条款',
'名称': '名',
'标签': '标签',
'状态': '状态',
'节点五分钟内节点在线情况': '节点五分钟内节点在线情况',
'邮箱验证码': '郵箱驗證碼',
'发送': '送',
'邀请码': '邀請碼',
'邀请码(选填)': '邀請碼(選填)',
'注册': '註冊',
'返回登入': '返回登',
'我已阅读并同意 <a target="_blank" href="{url}">服务条款</a>': '我已閱讀並同意 <a target="_blank" href="{url}">服務條款</a>',
'请同意服务条款': '請同意服務條款',
'名称': '名',
'标签': '標籤',
'状态': '狀態',
'节点五分钟内节点在线情况': '五分鐘內節點線上情況',
'倍率': '倍率',
'使用的流量将乘以倍率进行扣除': '使用的流量将乘以倍率进行扣除',
'使用的流量将乘以倍率进行扣除': '使用的流量將乘以倍率進行扣除',
'更多操作': '更多操作',
'复制成功': '复制成功',
'复制链接': '复制链接',
'该订阅长期有效': '该订阅长期有效',
'已过期': '已期',
'已用 {used} / 总计 {total}': '已用 {used} / 总计 {total}',
'重置订阅信息': '重置订阅信息',
'没有可用节点,如果您未订阅或已过期请': '没有可用节点,如果您未订阅或已过期请',
'订阅': '订阅',
'确定要重置当月流量?': '确定要重置当月流量?',
'点击「确定」将会跳转到收银台,支付订单后系统将会清空您当月已使用流量。': '点击「确定」将会跳转到收银台,支付订单后系统将会清空您当月已使用流量。',
'确定': '定',
'确定要重置订阅信息?': '确定要重置订阅信息',
'如果你的订阅地址或信息泄露可以进行此操作。重置后你的UUID及订阅将会变更需要重新进行订阅。': '如果你的订阅地址或信息泄露可以进行此操作。重置后你的UUID及订阅将会变更需要重新进行订阅。',
'复制成功': '複製成功',
'复制链接': '複製鏈接',
'该订阅长期有效': '該訂閱長期有效',
'已过期': '已期',
'已用 {used} / 总计 {total}': '已用 {used} / 總計 {total}',
'重置订阅信息': '重置訂閲資訊',
'没有可用节点,如果您未订阅或已过期请': '沒有可用節點,如果您未訂閱或已過期請',
'订阅': '訂閱',
'确定要重置当月流量?': '確定要重置當月流量?',
'点击「确定」将会跳转到收银台,支付订单后系统将会清空您当月已使用流量。': '點擊「確定」將會跳轉到收銀台,支付訂單後系統將會清空您當月已使用流量。',
'确定': '定',
'确定要重置订阅信息?': '確定要重置訂閱資訊',
'如果你的订阅地址或信息泄露可以进行此操作。重置后你的UUID及订阅将会变更需要重新进行订阅。': '如果您的訂閱位址或資訊發生洩露可以執行此操作。重置後您的 UUID 及訂閱將會變更,需要重新導入訂閱。',
'重置成功': '重置成功',
'低': '低',
'中': '中',
'高': '高',
'主题': '主',
'工单级别': '工单级别',
'工单状态': '工单状态',
'最后回复': '最后回复',
'已关闭': '已关闭',
'待回复': '待回',
'已回复': '已回',
'查看': '查看',
'关闭': '关闭',
'新的工单': '新的工',
'新的工单': '新的工',
'确认': '确认',
'主题': '主',
'请输入工单主题': '请输入工单主题',
'工单等级': '工单等级',
'请选择工单等级': '请选择工单等级',
'消息': '息',
'请描述你遇到的问题': '请描述你遇到的问题',
'记录时间': '记录时间',
'实际上行': '实际上行',
'实际下行': '实际下行',
'合计': '合',
'公式:(实际上行 + 实际下行) x 扣费倍率 = 扣除流量': '公式:(实际上行 + 实际下行) x 扣费倍率 = 扣除流量',
'复制成功': '复制成功',
'复制订阅地址': '复制订阅地址',
'主题': '主',
'工单级别': '工單級別',
'工单状态': '工單狀態',
'最后回复': '最新回復',
'已关闭': '已關閉',
'待回复': '待回',
'已回复': '已回',
'查看': '檢視',
'关闭': '關閉',
'新的工单': '新的工',
'新的工单': '新的工',
'确认': '確認',
'主题': '主',
'请输入工单主题': '請輸入工單主題',
'工单等级': '工單等級',
'请选择工单等级': '請選擇工單等級',
'消息': '息',
'请描述你遇到的问题': '請描述您遇到的問題',
'记录时间': '記錄時間',
'实际上行': '實際上行',
'实际下行': '實際下行',
'合计': '合',
'公式:(实际上行 + 实际下行) x 扣费倍率 = 扣除流量': '公式:(實際上行 + 實際下行) x 扣費倍率 = 扣除流量',
'复制成功': '複製成功',
'复制订阅地址': '複製訂閲位址',
'导入到': '导入到',
'一键订阅': '一键订阅',
'复制订阅': '复制订阅',
'推广佣金划转至余额': '推广佣金划转至余额',
'确认': '确认',
'划转后的余额仅用于{title}消费使用': '划转后的余额仅用于{title}消费使用',
'当前推广佣金余额': '当前推广佣金余额',
'划转金额': '划转金额',
'请输入需要划转到余额的金额': '请输入需要划转到余额的金额',
'输入内容回复工单...': '输入内容回复工单...',
'申请提现': '申请提现',
'确认': '确认',
'一键订阅': '一鍵訂閲',
'复制订阅': '複製訂閲',
'推广佣金划转至余额': '推廣佣金劃轉至餘額',
'确认': '確認',
'划转后的余额仅用于{title}消费使用': '劃轉后的餘額僅用於 {title} 消費使用',
'当前推广佣金余额': '當前推廣佣金餘額',
'划转金额': '劃轉金額',
'请输入需要划转到余额的金额': '請輸入需要劃轉到餘額的金額',
'输入内容回复工单...': '輸入内容回復工單…',
'申请提现': '申請提現',
'确认': '確認',
'取消': '取消',
'提现方式': '提方式',
'请选择提现方式': '请选择提现方式',
'提现账号': '提现账号',
'请输入提现账号': '请输入提现账号',
'提现方式': '提方式',
'请选择提现方式': '請選擇提現方式',
'提现账号': '提現賬號',
'请输入提现账号': '請輸入提現賬號',
'我知道了': '我知道了',
'绑定Telegram': '绑定Telegram',
'第一步': '第一步',
'第二步': '第二步',
'打开Telegram搜索': '打开Telegram搜索',
'向机器人发送你的': '向机器人发送你的',
'使用文档': '使用文档',
'最后更新: {date}': '最更新: {date}',
'复制成功': '复制成功',
'我的订阅': '我的订阅',
'还有没支付的订单': '还有没支付的订单',
'绑定Telegram': '綁定 Telegram',
'第一步': '驟一',
'第二步': '驟二',
'打开Telegram搜索': '打開 Telegram 並搜索',
'向机器人发送你的': '向機器人發送您的',
'使用文档': '使用檔案',
'最后更新: {date}': '最更新: {date}',
'复制成功': '複製成功',
'我的订阅': '我的訂閱',
'还有没支付的订单': '還有未支付的訂單',
'立即支付': '立即支付',
'条工单正在处理中': '条工单正在处理中',
'立即查看': '立即查看',
'购买订阅': '购买订阅',
'使用文档': '使用文档',
'我的订单': '我的订单',
'流量明细': '流量明',
'配置订阅': '配置订阅',
'我的邀请': '我的邀',
'节点状态': '节点状态',
'复制成功': '复制成功',
'商品信息': '商品信息',
'产品名称': '产品名称',
'类型/周期': '类型/周期',
'产品流量': '品流量',
'订单信息': '订单信息',
'关闭订单': '关闭订单',
'订单号': '订单号',
'优惠金额': '优惠金额',
'旧订阅折抵金额': '旧订阅折抵金额',
'退款金额': '退款金',
'余额支付': '余额支付',
'我的工单': '我的工',
'工单历史': '工单历史',
'{reset_day} 日后重置流量': '{reset_day} 日重置流量',
'节点名称': '节点名称',
'于 {date} 到期,距离到期还有 {day} 天。': '于 {date} 到期,距离到期还有 {day} 天。',
'Telegram 讨论组': 'Telegram 讨论组',
'条工单正在处理中': '條工單正在處理中',
'立即查看': '立即檢視',
'购买订阅': '購買訂閲',
'使用文档': '使用檔案',
'我的订单': '我的訂單',
'流量明细': '流量明',
'配置订阅': '配置訂閱',
'我的邀请': '我的邀',
'节点状态': '節點狀態',
'复制成功': '複製成功',
'商品信息': '商品資訊',
'产品名称': '產品名稱',
'类型/周期': '類型/週期',
'产品流量': '品流量',
'订单信息': '訂單信息',
'关闭订单': '關閉訂單',
'订单号': '訂單號',
'优惠金额': '優惠金額',
'旧订阅折抵金额': '舊訂閲折抵金額',
'退款金额': '退款金',
'余额支付': '餘額支付',
'我的工单': '我的工',
'工单历史': '工單歷史',
'{reset_day} 日后重置流量': '{reset_day} 日重置流量',
'节点名称': '節點名稱',
'于 {date} 到期,距离到期还有 {day} 天。': '於 {date} 到期,距離到期還有 {day} 天。',
'Telegram 讨论组': 'Telegram 討論組',
'立即加入': '立即加入',
'续费': '续费',
'购买': '购买',
'该订阅无法续费,仅允许新用户购买': '该订阅无法续费,仅允许新用户购买',
'重置当月流量': '重置当月流量',
'流量明细仅保留近月数据以供查询。': '流量明细仅保留近月数据以供查询。',
'扣费倍率': '扣费倍率'
'该订阅无法续费,仅允许新用户购买': '該訂閲無法續費,僅允許新用戶購買',
'重置当月流量': '重置當月流量',
'流量明细仅保留近月数据以供查询。': '流量明細僅保留近一個月資料以供查詢。',
'扣费倍率': '扣费倍率',
'支付手续费': '支付手續費',
'续费订阅': '續費訂閲',
'学习如何使用': '學習如何使用',
'快速将节点导入对应客户端进行使用': '快速將訂閲導入對應的客戶端進行使用',
'对您当前的订阅进行续费': '對您的當前訂閲進行續費',
'对您当前的订阅进行购买': '重新購買您的當前訂閲',
'捷径': '捷徑',
'不会使用,查看使用教程': '不會使用,檢視使用檔案',
'使用支持扫码的客户端进行订阅': '使用支持掃碼的客戶端進行訂閲',
'扫描二维码订阅': '掃描二維碼訂閲',
'续费': '續費',
'购买': '購買',
'查看教程': '查看教程',
'注意': '注意',
'你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?': '您还有未完成的订单,购买前需要先取消,确定要取消之前的订单吗?',
'确定取消': '確定取消',
'返回我的订单': '返回我的訂單',
'如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?': '如果您已經付款,取消訂單可能會導致支付失敗,確定要取消訂單嗎?',
'选择最适合你的计划': '選擇最適合您的計劃',
'全部': '全部',
'按周期': '按週期',
'一次性': '一次性',
'遇到问题': '遇到問題',
'遇到问题可以通过工单与我们沟通': '遇到問題您可以通過工單與我們溝通'
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,53 @@
<?php
return [
'name' => 'V2board',
'description' => 'V2board默认主题',
'version' => '1.5.6',
'images' => 'https://images.unsplash.com/photo-1515405295579-ba7b45403062?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2160&q=80',
'configs' => [
[
'label' => '主题色', // 标签
'placeholder' => '请选择主题颜色', // 描述
'field_name' => 'theme_color', // 字段名 作为数据key使用
'field_type' => 'select', // 字段类型: select,input,switch
'select_options' => [ // 当字段类型为select时有效
'default' => '默认(蓝色)',
'green' => '奶绿色',
'black' => '黑色',
'darkblue' => '暗蓝色',
],
'default_value' => 'default' // 字段默认值,将会在首次进行初始化
], [
'label' => '背景',
'placeholder' => '请输入背景图片URL',
'field_name' => 'background_url',
'field_type' => 'input'
], [
'label' => '边栏风格',
'placeholder' => '请选择边栏风格',
'field_name' => 'theme_sidebar',
'field_type' => 'select',
'select_options' => [
'light' => '亮',
'dark' => '暗'
],
'default_value' => 'light'
], [
'label' => '顶部风格',
'placeholder' => '请选择顶部风格',
'field_name' => 'theme_header',
'field_type' => 'select',
'select_options' => [
'light' => '亮',
'dark' => '暗'
],
'default_value' => 'dark'
], [
'label' => '自定义页脚HTML',
'placeholder' => '可以实现客服JS代码的加入等',
'field_name' => 'custom_html',
'field_type' => 'textarea'
]
]
];

View File

@ -15,7 +15,7 @@
'default' => '#0665d0',
'green' => '#319795'
])
<meta name="theme-color" content="{{$colors[$theme_color]}}">
<meta name="theme-color" content="{{$colors[$theme_config['theme_color']]}}">
<title>{{$title}}</title>
<!-- <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Nunito+Sans:300,400,400i,600,700"> -->
@ -23,15 +23,15 @@
<script>
window.settings = {
title: '{{$title}}',
theme_path: '{{$theme_path}}',
theme: {
sidebar: '{{$theme_sidebar}}',
header: '{{$theme_header}}',
color: '{{$theme_color}}',
sidebar: '{{$theme_config['theme_sidebar']}}',
header: '{{$theme_config['theme_header']}}',
color: '{{$theme_config['theme_color']}}',
},
version: '{{$version}}',
background_url: '{{$background_url}}',
background_url: '{{$theme_config['background_url']}}',
description: '{{$description}}',
crisp_id: '{{$crisp_id}}',
i18n: [
'zh-CN',
'en-US',
@ -39,7 +39,8 @@
'vi-VN',
'ko-KR',
'zh-TW'
]
],
logo: '{{$logo}}'
}
</script>
<script src="/theme/{{$theme}}/assets/i18n/zh-CN.js?v={{$version}}"></script>
@ -52,6 +53,7 @@
<body>
<div id="root"></div>
{!! $theme_config['custom_html'] !!}
<script src="/theme/{{$theme}}/assets/vendors.async.js?v={{$version}}"></script>
<script src="/theme/{{$theme}}/assets/components.async.js?v={{$version}}"></script>
<script src="/theme/{{$theme}}/assets/umi.js?v={{$version}}"></script>

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

@ -1,5 +1,6 @@
{
"/app.js": "/app.js?id=a2e36b7a4f248973b22b",
"/app.css": "/app.css?id=9ce01eaaba790566b895",
"/app-dark.css": "/app-dark.css?id=821c845f9bf3b7853c33"
"/app.js": "/app.js?id=9db6ba6424a3d1048c194c9c1e4429fe",
"/app-dark.css": "/app-dark.css?id=ff172044c4efc9f08f12c0eb824b0226",
"/app.css": "/app.css?id=a38514598173eedd6b8575a77bc1ead4",
"/img/favicon.png": "/img/favicon.png?id=1542bfe8a0010dcbee710da13cce367f"
}

View File

@ -86,5 +86,9 @@
"The traffic usage in :app_name has reached 80%": "The traffic usage in :app_name has reached 80%",
"The service in :app_name is about to expire": "The service in :app_name is about to expire",
"The coupon can only be used :limit_use_with_user per person": "The coupon can only be used :limit_use_with_user per person",
"The coupon code cannot be used for this period": "The coupon code cannot be used for this period"
"The coupon code cannot be used for this period": "The coupon code cannot be used for this period",
"Request failed, please try again later": "Request failed, please try again later",
"Register frequently, please try again after 1 hour": "Register frequently, please try again after 1 hour",
"Uh-oh, we've had some problems, we're working on it.": "Uh-oh, we've had some problems, we're working on it",
"This subscription reset package does not apply to your subscription": "This subscription reset package does not apply to your subscription"
}

View File

@ -86,5 +86,9 @@
"The traffic usage in :app_name has reached 80%": "在 :app_name 的已用流量已达到 80%",
"The service in :app_name is about to expire": "在 :app_name 的服务即将到期",
"The coupon can only be used :limit_use_with_user per person": "该优惠券每人只能用 :limit_use_with_user 次",
"The coupon code cannot be used for this period": "此优惠券无法用于该付款周期"
"The coupon code cannot be used for this period": "此优惠券无法用于该付款周期",
"Request failed, please try again later": "请求失败,请稍后再试",
"Register frequently, please try again after 1 hour": "注册频繁请等待1小时后再次尝试",
"Uh-oh, we've had some problems, we're working on it.": "遇到了些问题,我们正在进行处理",
"This subscription reset package does not apply to your subscription": "该订阅重置包不适用于你的订阅"
}

View File

@ -60,9 +60,6 @@ rules:
# - DOMAIN,e.crashlytics.com,REJECT //注释此选项有助于大多数App开发者分析崩溃信息如果您拒绝一切崩溃数据统计、搜集请取消 # 注释。
# 国内网站
- DOMAIN-SUFFIX,cn,DIRECT
- DOMAIN-KEYWORD,-cn,DIRECT
- DOMAIN-SUFFIX,126.com,DIRECT
- DOMAIN-SUFFIX,126.net,DIRECT
- DOMAIN-SUFFIX,127.net,DIRECT
@ -366,6 +363,7 @@ rules:
- DOMAIN-SUFFIX,klip.me,SELECT
- DOMAIN-SUFFIX,libsyn.com,SELECT
- DOMAIN-SUFFIX,linkedin.com,SELECT
- DOMAIN-SUFFIX,line-apps.com,SELECT
- DOMAIN-SUFFIX,linode.com,SELECT
- DOMAIN-SUFFIX,lithium.com,SELECT
- DOMAIN-SUFFIX,littlehj.com,SELECT
@ -550,6 +548,10 @@ rules:
- IP-CIDR,224.0.0.0/4,DIRECT
- IP-CIDR6,fe80::/10,DIRECT
# 剩余未匹配的国内网站
- DOMAIN-SUFFIX,cn,DIRECT
- DOMAIN-KEYWORD,-cn,DIRECT
# 最终规则
- GEOIP,CN,DIRECT
- MATCH,SELECT

View File

@ -24,9 +24,10 @@ dns:
- https://doh.pub/dns-query
- https://dns.alidns.com/dns-query
fallback:
- tls://1.0.0.1:853
- https://cloudflare-dns.com/dns-query
- https://dns.google/dns-query
- https://doh.dns.sb/dns-query
- https://dns.cloudflare.com/dns-query
- https://dns.twnic.tw/dns-query
- tls://8.8.4.4:853
fallback-filter:
geoip: true
ipcidr:
@ -84,9 +85,6 @@ rules:
# - DOMAIN,e.crashlytics.com,REJECT //注释此选项有助于大多数App开发者分析崩溃信息如果您拒绝一切崩溃数据统计、搜集请取消 # 注释。
# 国内网站
- DOMAIN-SUFFIX,cn,DIRECT
- DOMAIN-KEYWORD,-cn,DIRECT
- DOMAIN-SUFFIX,126.com,DIRECT
- DOMAIN-SUFFIX,126.net,DIRECT
- DOMAIN-SUFFIX,127.net,DIRECT
@ -390,6 +388,7 @@ rules:
- DOMAIN-SUFFIX,klip.me,$app_name
- DOMAIN-SUFFIX,libsyn.com,$app_name
- DOMAIN-SUFFIX,linkedin.com,$app_name
- DOMAIN-SUFFIX,line-apps.com,$app_name
- DOMAIN-SUFFIX,linode.com,$app_name
- DOMAIN-SUFFIX,lithium.com,$app_name
- DOMAIN-SUFFIX,littlehj.com,$app_name
@ -574,6 +573,10 @@ rules:
- IP-CIDR,224.0.0.0/4,DIRECT
- IP-CIDR6,fe80::/10,DIRECT
# 剩余未匹配的国内网站
- DOMAIN-SUFFIX,cn,DIRECT
- DOMAIN-KEYWORD,-cn,DIRECT
# 最终规则
- GEOIP,CN,DIRECT
- MATCH,$app_name

View File

@ -78,8 +78,6 @@ DOMAIN-SUFFIX,apple-mapkit.com,DIRECT
USER-AGENT,MicroMessenger Client*,DIRECT
USER-AGENT,WeChat*,DIRECT
DOMAIN-SUFFIX,cn,DIRECT
DOMAIN-KEYWORD,-cn,DIRECT
DOMAIN-SUFFIX,126.com,DIRECT
DOMAIN-SUFFIX,126.net,DIRECT
DOMAIN-SUFFIX,127.net,DIRECT
@ -382,6 +380,7 @@ DOMAIN-SUFFIX,kat.cr,Proxy
DOMAIN-SUFFIX,klip.me,Proxy
DOMAIN-SUFFIX,libsyn.com,Proxy
DOMAIN-SUFFIX,linkedin.com,Proxy
DOMAIN-SUFFIX,line-apps.com,Proxy
DOMAIN-SUFFIX,linode.com,Proxy
DOMAIN-SUFFIX,lithium.com,Proxy
DOMAIN-SUFFIX,littlehj.com,Proxy
@ -565,6 +564,10 @@ IP-CIDR,100.64.0.0/10,DIRECT
IP-CIDR,224.0.0.0/4,DIRECT
IP-CIDR6,fe80::/10,DIRECT
# 剩余未匹配的国内网站
DOMAIN-SUFFIX,cn,DIRECT
DOMAIN-KEYWORD,-cn,DIRECT
# 最终规则
GEOIP,CN,DIRECT
FINAL,Proxy

View File

@ -103,8 +103,6 @@ DOMAIN-SUFFIX,apple-mapkit.com,DIRECT
USER-AGENT,MicroMessenger Client*,DIRECT
USER-AGENT,WeChat*,DIRECT
DOMAIN-SUFFIX,cn,DIRECT
DOMAIN-KEYWORD,-cn,DIRECT
DOMAIN-SUFFIX,126.com,DIRECT
DOMAIN-SUFFIX,126.net,DIRECT
DOMAIN-SUFFIX,127.net,DIRECT
@ -407,6 +405,7 @@ DOMAIN-SUFFIX,kat.cr,Proxy
DOMAIN-SUFFIX,klip.me,Proxy
DOMAIN-SUFFIX,libsyn.com,Proxy
DOMAIN-SUFFIX,linkedin.com,Proxy
DOMAIN-SUFFIX,line-apps.com,Proxy
DOMAIN-SUFFIX,linode.com,Proxy
DOMAIN-SUFFIX,lithium.com,Proxy
DOMAIN-SUFFIX,littlehj.com,Proxy
@ -581,6 +580,10 @@ IP-CIDR,220.181.174.34/32,Proxy,no-resolve
RULE-SET,LAN,DIRECT
# 剩余未匹配的国内网站
DOMAIN-SUFFIX,cn,DIRECT
DOMAIN-KEYWORD,-cn,DIRECT
# 最终规则
GEOIP,CN,DIRECT
FINAL,Proxy,dns-failed

View File

@ -2,9 +2,9 @@
<html>
<head>
<link rel="stylesheet" href="/assets/admin/components.chunk.css?v={{$verison}}">
<link rel="stylesheet" href="/assets/admin/umi.css?v={{$verison}}">
<link rel="stylesheet" href="/assets/admin/custom.css?v={{$verison}}">
<link rel="stylesheet" href="/assets/admin/components.chunk.css?v={{$version}}">
<link rel="stylesheet" href="/assets/admin/umi.css?v={{$version}}">
<link rel="stylesheet" href="/assets/admin/custom.css?v={{$version}}">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no">
<title>{{$title}}</title>
@ -18,17 +18,18 @@
header: '{{$theme_header}}',
color: '{{$theme_color}}',
},
verison: '{{$verison}}',
background_url: '{{$backgroun_url}}'
version: '{{$version}}',
background_url: '{{$background_url}}',
logo: '{{$logo}}'
}
</script>
</head>
<body>
<div id="root"></div>
<script src="/assets/admin/vendors.async.js?v={{$verison}}"></script>
<script src="/assets/admin/components.async.js?v={{$verison}}"></script>
<script src="/assets/admin/umi.js?v={{$verison}}"></script>
<script src="/assets/admin/vendors.async.js?v={{$version}}"></script>
<script src="/assets/admin/components.async.js?v={{$version}}"></script>
<script src="/assets/admin/umi.js?v={{$version}}"></script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-P1E9Z5LRRK"></script>
<script>

View File

@ -1,5 +1,6 @@
<?php
use App\Services\ThemeService;
use Illuminate\Http\Request;
/*
@ -22,14 +23,18 @@ Route::get('/', function (Request $request) {
$renderParams = [
'title' => config('v2board.app_name', 'V2Board'),
'theme' => config('v2board.frontend_theme', 'v2board'),
'theme_sidebar' => config('v2board.frontend_theme_sidebar', 'light'),
'theme_header' => config('v2board.frontend_theme_header', 'dark'),
'theme_color' => config('v2board.frontend_theme_color', 'default'),
'background_url' => config('v2board.frontend_background_url'),
'theme_path' => '/theme/' . config('v2board.frontend_theme', 'v2board') . '/assets/',
'version' => config('app.version'),
'description' => config('v2board.app_description', 'V2Board is best'),
'crisp_id' => config('v2board.frontend_customer_service_method') === 'crisp' ? config('v2board.frontend_customer_service_id') : ''
'logo' => config('v2board.logo')
];
if (!config("theme.{$renderParams['theme']}")) {
$themeService = new ThemeService($renderParams['theme']);
$themeService->init();
}
$renderParams['theme_config'] = config('theme.' . config('v2board.frontend_theme', 'v2board'));
return view('theme::' . config('v2board.frontend_theme', 'v2board') . '.dashboard', $renderParams);
});
@ -39,7 +44,8 @@ Route::get('/' . config('v2board.frontend_admin_path', 'admin'), function () {
'theme_sidebar' => config('v2board.frontend_theme_sidebar', 'light'),
'theme_header' => config('v2board.frontend_theme_header', 'dark'),
'theme_color' => config('v2board.frontend_theme_color', 'default'),
'backgroun_url' => config('v2board.frontend_background_url'),
'verison' => config('app.version')
'background_url' => config('v2board.frontend_background_url'),
'version' => config('app.version'),
'logo' => config('v2board.logo')
]);
});

Some files were not shown because too many files have changed in this diff Show More