Compare commits

...

8 Commits

Author SHA1 Message Date
xboard
819feef80c fix: StatUserJob again
Some checks are pending
Docker Build and Publish / build (push) Waiting to run
2025-01-10 14:30:57 +08:00
Xboard
0b16e49756
Update aapanel+docker安装指南.md 2025-01-10 14:20:55 +08:00
xboard
12029632b2 update dockerfile 2025-01-10 14:20:41 +08:00
xboard
b01bea0fa8 fix: resolve plan pricing issues after migration 2025-01-10 14:08:23 +08:00
xboard
5648f2ba8a chore: change rate field type to decimal 2025-01-10 14:03:02 +08:00
xboard
84bef8d339 fix: StatUserJob 2025-01-10 13:53:03 +08:00
xboard
42f1bb2ded upgrade to php8.2 2025-01-10 13:14:53 +08:00
xboard
e7745cb4d9 upgrade to lararvel 11 and fixcorrect know file issues 2025-01-10 12:41:09 +08:00
18 changed files with 470 additions and 279 deletions

3
.gitignore vendored
View File

@ -29,4 +29,5 @@ cli-php.ini
frontend
docker-compose.yaml
bun.lockb
compose.yaml
compose.yaml
.scribe

View File

@ -1,8 +1,8 @@
FROM phpswoole/swoole:php8.1-alpine
FROM phpswoole/swoole:php8.2-alpine
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
RUN install-php-extensions pcntl bcmath \
&& apk --no-cache add shadow sqlite mysql-client git patch supervisor redis \
&& apk --no-cache add shadow sqlite mysql-client mysql-client mysql-dev mariadb-connector-c git patch supervisor redis \
&& addgroup -S -g 1000 www && adduser -S -G www -u 1000 www \
&& (getent group redis || addgroup -S redis) \
&& (getent passwd redis || adduser -S -G redis -H -h /data redis)

View File

@ -6,6 +6,7 @@ Xboard New是基于Xboard二次开发重写后台管理并优化系统架构
# Xboard New 特点
基于Xboard 二次开发,增加了以下特性
- 升级Laravel11
- 增加Octane支持
- 使用React + Shadcn UI + TailwindCSS重构后台管理
- 使用Vue3 + TypeScript + NaiveUI + Unocss + Pinia重构用户前端
@ -16,7 +17,7 @@ Xboard New是基于Xboard二次开发重写后台管理并优化系统架构
- 优化系统架构,提升可维护性
# **系统架构**
- PHP8.1+
- PHP8.2+
- Composer
- MySQL5.7+
- Redis

View File

@ -180,7 +180,7 @@ class AuthController extends Controller
$authService = new AuthService($user);
$data = $authService->generateAuthData($request);
$data = $authService->generateAuthData();
return $this->success($data);
}
@ -223,48 +223,70 @@ class AuthController extends Controller
}
$authService = new AuthService($user);
return $this->success($authService->generateAuthData($request));
return $this->success($authService->generateAuthData());
}
public function token2Login(Request $request)
{
if ($request->input('token')) {
$redirect = '/#/login?verify=' . $request->input('token') . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard');
if (admin_setting('app_url')) {
$location = admin_setting('app_url') . $redirect;
} else {
$location = url($redirect);
}
return redirect()->to($location)->send();
if ($token = $request->input('token')) {
$redirect = '/#/login?verify=' . $token . '&redirect=' . ($request->input('redirect', 'dashboard'));
return redirect()->to(
admin_setting('app_url')
? admin_setting('app_url') . $redirect
: url($redirect)
);
}
if ($request->input('verify')) {
$key = CacheKey::get('TEMP_TOKEN', $request->input('verify'));
if ($verify = $request->input('verify')) {
$key = CacheKey::get('TEMP_TOKEN', $verify);
$userId = Cache::get($key);
if (!$userId) {
return $this->fail([400,__('Token error')]);
}
$user = User::find($userId);
if (!$user) {
return $this->fail([400,__('The user does not ')]);
return response()->json([
'message' => __('Token error')
], 400);
}
$user = User::findOrFail($userId);
if ($user->banned) {
return $this->fail([400,__('Your account has been suspended')]);
return response()->json([
'message' => __('Your account has been suspended')
], 400);
}
Cache::forget($key);
$authService = new AuthService($user);
return $this->success($authService->generateAuthData($request));
return response()->json([
'data' => $authService->generateAuthData()
]);
}
return response()->json([
'message' => __('Invalid request')
], 400);
}
public function getQuickLoginUrl(Request $request)
{
$authorization = $request->input('auth_data') ?? $request->header('authorization');
if (!$authorization) return $this->fail(ResponseEnum::CLIENT_HTTP_UNAUTHORIZED);
$user = AuthService::decryptAuthData($authorization);
if (!$user) return $this->fail(ResponseEnum::CLIENT_HTTP_UNAUTHORIZED_EXPIRED);
if (!$authorization) {
return response()->json([
'message' => ResponseEnum::CLIENT_HTTP_UNAUTHORIZED
], 401);
}
$user = AuthService::findUserByBearerToken($authorization);
if (!$user) {
return response()->json([
'message' => ResponseEnum::CLIENT_HTTP_UNAUTHORIZED_EXPIRED
], 401);
}
$code = Helper::guid();
$key = CacheKey::get('TEMP_TOKEN', $code);
Cache::put($key, $user['id'], 60);

View File

@ -35,47 +35,47 @@ class OrderController extends Controller
$current = $request->input('current', 1);
$pageSize = $request->input('pageSize', 10);
$orderModel = Order::with('plan:id,name');
if ($request->input('is_commission')) {
if ($request->boolean('is_commission')) {
$orderModel->whereNotNull('invite_user_id')
->whereNotIn('status', [0, 2])
->where('commission_balance', '>', 0);
}
$this->applyFiltersAndSorts($request, $orderModel);
$orders = $orderModel
->orderBy('created_at', 'desc')
->paginate($pageSize, ['*'], 'page', $current);
return response([
'data' => $orders->transform(function ($order) {
$order['period'] = PlanService::getLegacyPeriod($order->period);
return $order;
}),
'total' => $orders->total()
]);
return response()->json(
$orderModel
->latest('created_at')
->paginate(
perPage: $pageSize,
page: $current
)->through(fn($order) => [
...$order->toArray(),
'period' => PlanService::getLegacyPeriod($order->period)
]),
);
}
private function applyFiltersAndSorts(Request $request, $builder)
{
if ($request->has('filter')) {
collect($request->input('filter'))->each(callback: function ($filter) use ($builder) {
$key = $filter['id'];
$value = $filter['value'];
$builder->where(function ($query) use ($key, $value) {
if (is_array($value)) {
$query->whereIn($key, $value);
} else {
$query->where($key, 'like', "%{$value}%");
}
});
});
}
$request->collect('filter')->each(function ($filter) use ($builder) {
$key = $filter['id'];
$value = $filter['value'];
if ($request->has('sort')) {
collect($request->input('sort'))->each(function ($sort) use ($builder) {
$key = $sort['id'];
$value = $sort['desc'] ? 'DESC' : 'ASC';
$builder->orderBy($key, $value);
$builder->where(function ($query) use ($key, $value) {
is_array($value)
? $query->whereIn($key, $value)
: $query->where($key, 'like', "%{$value}%");
});
}
});
$request->collect('sort')->each(function ($sort) use ($builder) {
$builder->orderBy(
$sort['id'],
$sort['desc'] ? 'DESC' : 'ASC'
);
});
}
public function paid(Request $request)

View File

@ -2,11 +2,8 @@
namespace App\Http\Controllers\V2\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Models\Ticket;
use App\Models\TicketMessage;
use App\Models\User;
use App\Services\TicketService;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
@ -54,15 +51,15 @@ class TicketController extends Controller
private function fetchTicketById(Request $request)
{
$ticket = Ticket::with('messages', 'user')->find($request->input('id'));
if (!$ticket) {
return $this->fail([400202, '工单不存在']);
}
$ticket->messages->each(function ($message) use ($ticket) {
$message->is_me = $message->user_id !== $ticket->user_id;
});
return $this->success($ticket);
}
@ -73,35 +70,27 @@ class TicketController extends Controller
*/
private function fetchTickets(Request $request)
{
$current = $request->input('current') ?? 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$ticketModel = Ticket::query()
->when($request->has('status'), function ($query) use ($request) {
$query->where('status', $request->input('status'));
})
->when($request->has('reply_status'), function ($query) use ($request) {
$query->whereIn('reply_status', $request->input('reply_status'));
})
->when($request->has('email'), function ($query) use ($request) {
$query->whereHas('user', function ($q) use ($request) {
$q->where('email', $request->input('email'));
});
});
$ticketModel = Ticket::query();
$this->applyFiltersAndSorts($request, $ticketModel);
$ticketModel->orderBy('updated_at', 'DESC');
if ($request->has('status')) {
$ticketModel->where('status', $request->input('status'));
}
if ($request->has('reply_status')) {
$ticketModel->whereIn('reply_status', $request->input('reply_status'));
}
if ($request->has('email')) {
$user = User::where('email', $request->input('email'))->first();
if ($user) {
$ticketModel->where('user_id', $user->id);
}
}
$total = $ticketModel->count();
$res = $ticketModel->forPage($current, $pageSize)->get();
return response([
'data' => $res,
'total' => $total
]);
return response()->json($ticketModel
->latest('updated_at')
->paginate(
perPage: $request->integer('pageSize', 10),
page: $request->integer('current', 1)
));
}
public function reply(Request $request)

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Admin;
use App\Models\Server;
@ -7,15 +9,56 @@ use Illuminate\Foundation\Http\FormRequest;
class ServerSave extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
private const PROTOCOL_RULES = [
'shadowsocks' => [
'cipher' => 'required|string',
'obfs' => 'nullable|string',
'obfs_settings.path' => 'nullable|string',
'obfs_settings.host' => 'nullable|string',
],
'vmess' => [
'tls' => 'required|integer',
'network' => 'required|string',
'network_settings' => 'nullable|array',
'tls_settings.server_name' => 'nullable|string',
'tls_settings.allow_insecure' => 'nullable|boolean',
],
'trojan' => [
'network' => 'required|string',
'network_settings' => 'nullable|array',
'server_name' => 'nullable|string',
'allow_insecure' => 'nullable|boolean',
],
'hysteria' => [
'version' => 'required|integer',
'alpn' => 'nullable|string',
'obfs.open' => 'nullable|boolean',
'obfs.type' => 'string|nullable',
'obfs.password' => 'string|nullable',
'tls.server_name' => 'nullable|string',
'tls.allow_insecure' => 'nullable|boolean',
'bandwidth.up' => 'nullable|integer',
'bandwidth.down' => 'nullable|integer',
],
'vless' => [
'tls' => 'required|integer',
'network' => 'required|string',
'network_settings' => 'nullable|array',
'flow' => 'nullable|string',
'tls_settings.server_name' => 'nullable|string',
'tls_settings.allow_insecure' => 'nullable|boolean',
'reality_settings.allow_insecure' => 'nullable|boolean',
'reality_settings.server_name' => 'nullable|string',
'reality_settings.server_port' => 'nullable|integer',
'reality_settings.public_key' => 'nullable|string',
'reality_settings.private_key' => 'nullable|string',
'reality_settings.short_id' => 'nullable|string',
]
];
private function getBaseRules(): array
{
$type = $this->input('type');
$protocolRules = [
return [
'type' => 'required|in:' . implode(',', Server::VALID_TYPES),
'spectific_key' => 'nullable|string',
'code' => 'nullable|string',
@ -33,56 +76,14 @@ class ServerSave extends FormRequest
'rate' => 'required|numeric',
'protocol_settings' => 'array',
];
}
$protocolSpecificRules = [
'shadowsocks' => [
'cipher' => 'required|string',
'obfs' => 'nullable|string',
'obfs_settings.path' => 'nullable|string',
'obfs_settings.host' => 'nullable|string',
],
'vmess' => [
'tls' => 'required|integer',
'network' => 'required|string',
'network_settings' => 'nullable|array',
'tls_settings.server_name' => 'nullable|string',
'tls_settings.allow_insecure' => 'nullable|boolean',
],
'trojan' => [
'network' => 'required|string',
'network_settings' => 'nullable|array',
'server_name' => 'nullable|string',
'allow_insecure' => 'nullable|boolean',
],
'hysteria' => [
'version' => 'required|integer',
'alpn' => 'nullable|string',
'obfs.open' => 'nullable|boolean',
'obfs.type' => 'string|nullable',
'obfs.password' => 'string|nullable',
'tls.server_name' => 'nullable|string',
'tls.allow_insecure' => 'nullable|boolean',
'bandwidth.up' => 'nullable|integer',
'bandwidth.down' => 'nullable|integer',
],
'vless' => [
'tls' => 'required|integer',
'network' => 'required|string',
'network_settings' => 'nullable|array',
'flow' => 'nullable|string',
'tls_settings.server_name' => 'nullable|string',
'tls_settings.allow_insecure' => 'nullable|boolean',
'reality_settings.allow_insecure' => 'nullable|boolean',
'reality_settings.server_name' => 'nullable|string',
'reality_settings.server_port' => 'nullable|integer',
'reality_settings.public_key' => 'nullable|string',
'reality_settings.private_key' => 'nullable|string',
'reality_settings.short_id' => 'nullable|string',
]
];
$rules = $protocolRules;
foreach ($protocolSpecificRules[$type] as $field => $rule) {
public function rules(): array
{
$type = $this->input('type');
$rules = $this->getBaseRules();
foreach (self::PROTOCOL_RULES[$type] ?? [] as $field => $rule) {
$rules['protocol_settings.' . $field] = $rule;
}

View File

@ -57,7 +57,6 @@ class StatUserJob implements ShouldQueue
if ($stat) {
$stat->u += ($v[0] * $this->server['rate']);
$stat->d += ($v[1] * $this->server['rate']);
$stat->t = time();
$stat->save();
} else {
StatUser::create([
@ -67,7 +66,6 @@ class StatUserJob implements ShouldQueue
'record_type' => $this->recordType,
'u' => ($v[0] * $this->server['rate']),
'd' => ($v[1] * $this->server['rate']),
't' => time(),
]);
}
});

View File

@ -3,11 +3,8 @@
namespace App\Services;
use App\Models\User;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Laravel\Sanctum\NewAccessToken;
use Illuminate\Support\Str;
use Laravel\Sanctum\PersonalAccessToken;
class AuthService
{
@ -47,4 +44,13 @@ class AuthService
$this->user->tokens()->delete();
return true;
}
public static function findUserByBearerToken(string $bearerToken): ?User
{
$token = str_replace('Bearer ', '', $bearerToken);
$accessToken = PersonalAccessToken::findToken($token);
return $accessToken?->tokenable;
}
}

View File

@ -11,17 +11,17 @@
],
"license": "MIT",
"require": {
"php": "^8.1",
"php": "^8.2",
"doctrine/dbal": "^3.7",
"google/cloud-storage": "^1.35",
"google/recaptcha": "^1.2",
"guzzlehttp/guzzle": "^7.4.3",
"laravel/framework": "10.*",
"laravel/horizon": "^5.9.6",
"laravel/octane": "*",
"laravel/prompts": "^0.1.22",
"laravel/sanctum": "^3.3",
"laravel/tinker": "^2.5",
"guzzlehttp/guzzle": "^7.8",
"laravel/framework": "^11.0",
"laravel/horizon": "^5.21",
"laravel/octane": "^2.3",
"laravel/prompts": "^0.1.13",
"laravel/sanctum": "^4.0",
"laravel/tinker": "^2.9",
"linfo/linfo": "^4.0",
"paragonie/sodium_compat": "^1.20",
"php-curl-class/php-curl-class": "^8.6",
@ -35,11 +35,11 @@
"require-dev": {
"barryvdh/laravel-debugbar": "^3.9",
"fakerphp/faker": "^1.9.1",
"mockery/mockery": "^1.3.1",
"nunomaduro/collision": "^7.10",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.0",
"orangehill/iseed": "^3.0",
"phpunit/phpunit": "^10.0",
"spatie/laravel-ignition": "^2.0"
"phpunit/phpunit": "^10.5",
"spatie/laravel-ignition": "^2.4"
},
"config": {
"optimize-autoloader": true,

View File

@ -180,57 +180,6 @@ return [
],
/*
|--------------------------------------------------------------------------
| Class Aliases
|--------------------------------------------------------------------------
|
| This array of class aliases will be registered when this application
| is started. However, feel free to register as many as you wish as
| the aliases are "lazy" loaded so they don't hinder performance.
|
*/
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Arr' => Illuminate\Support\Arr::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
'Bus' => Illuminate\Support\Facades\Bus::class,
'Cache' => Illuminate\Support\Facades\Cache::class,
'Config' => Illuminate\Support\Facades\Config::class,
'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,
'Gate' => Illuminate\Support\Facades\Gate::class,
'Hash' => Illuminate\Support\Facades\Hash::class,
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
'Redis' => Illuminate\Support\Facades\Redis::class,
'Request' => Illuminate\Support\Facades\Request::class,
'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class,
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
'Str' => Illuminate\Support\Str::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
],
/*
|--------------------------------------------------------------------------
| V2board version

270
config/scribe.php Normal file
View File

@ -0,0 +1,270 @@
<?php
use Knuckles\Scribe\Extracting\Strategies;
return [
// The HTML <title> for the generated documentation. If this is empty, Scribe will infer it from config('app.name').
'title' => null,
// A short description of your API. Will be included in the docs webpage, Postman collection and OpenAPI spec.
'description' => '',
// The base URL displayed in the docs. If this is empty, Scribe will use the value of config('app.url') at generation time.
// If you're using `laravel` type, you can set this to a dynamic string, like '{{ config("app.tenant_url") }}' to get a dynamic base URL.
'base_url' => null,
'routes' => [
[
// Routes that match these conditions will be included in the docs
'match' => [
// Match only routes whose paths match this pattern (use * as a wildcard to match any characters). Example: 'users/*'.
'prefixes' => ['api/*'],
// Match only routes whose domains match this pattern (use * as a wildcard to match any characters). Example: 'api.*'.
'domains' => ['*'],
// [Dingo router only] Match only routes registered under this version. Wildcards are NOT supported.
'versions' => ['v1'],
],
// Include these routes even if they did not match the rules above.
'include' => [
// 'users.index', 'POST /new', '/auth/*'
],
// Exclude these routes even if they matched the rules above.
'exclude' => [
// 'GET /health', 'admin.*'
],
],
],
// The type of documentation output to generate.
// - "static" will generate a static HTMl page in the /public/docs folder,
// - "laravel" will generate the documentation as a Blade view, so you can add routing and authentication.
// - "external_static" and "external_laravel" do the same as above, but generate a basic template,
// passing the OpenAPI spec as a URL, allowing you to easily use the docs with an external generator
'type' => 'static',
// See https://scribe.knuckles.wtf/laravel/reference/config#theme for supported options
'theme' => 'default',
'static' => [
// HTML documentation, assets and Postman collection will be generated to this folder.
// Source Markdown will still be in resources/docs.
'output_path' => 'public/docs',
],
'laravel' => [
// Whether to automatically create a docs endpoint for you to view your generated docs.
// If this is false, you can still set up routing manually.
'add_routes' => true,
// URL path to use for the docs endpoint (if `add_routes` is true).
// By default, `/docs` opens the HTML page, `/docs.postman` opens the Postman collection, and `/docs.openapi` the OpenAPI spec.
'docs_url' => '/docs',
// Directory within `public` in which to store CSS and JS assets.
// By default, assets are stored in `public/vendor/scribe`.
// If set, assets will be stored in `public/{{assets_directory}}`
'assets_directory' => null,
// Middleware to attach to the docs endpoint (if `add_routes` is true).
'middleware' => [],
],
'external' => [
'html_attributes' => []
],
'try_it_out' => [
// Add a Try It Out button to your endpoints so consumers can test endpoints right from their browser.
// Don't forget to enable CORS headers for your endpoints.
'enabled' => true,
// The base URL for the API tester to use (for example, you can set this to your staging URL).
// Leave as null to use the current app URL when generating (config("app.url")).
'base_url' => null,
// [Laravel Sanctum] Fetch a CSRF token before each request, and add it as an X-XSRF-TOKEN header.
'use_csrf' => false,
// The URL to fetch the CSRF token from (if `use_csrf` is true).
'csrf_url' => '/sanctum/csrf-cookie',
],
// How is your API authenticated? This information will be used in the displayed docs, generated examples and response calls.
'auth' => [
// Set this to true if ANY endpoints in your API use authentication.
'enabled' => false,
// Set this to true if your API should be authenticated by default. If so, you must also set `enabled` (above) to true.
// You can then use @unauthenticated or @authenticated on individual endpoints to change their status from the default.
'default' => false,
// Where is the auth value meant to be sent in a request?
// Options: query, body, basic, bearer, header (for custom header)
'in' => 'bearer',
// The name of the auth parameter (e.g. token, key, apiKey) or header (e.g. Authorization, Api-Key).
'name' => 'key',
// The value of the parameter to be used by Scribe to authenticate response calls.
// This will NOT be included in the generated documentation. If empty, Scribe will use a random value.
'use_value' => env('SCRIBE_AUTH_KEY'),
// Placeholder your users will see for the auth parameter in the example requests.
// Set this to null if you want Scribe to use a random value as placeholder instead.
'placeholder' => '{YOUR_AUTH_KEY}',
// Any extra authentication-related info for your users. Markdown and HTML are supported.
'extra_info' => 'You can retrieve your token by visiting your dashboard and clicking <b>Generate API token</b>.',
],
// Text to place in the "Introduction" section, right after the `description`. Markdown and HTML are supported.
'intro_text' => <<<INTRO
This documentation aims to provide all the information you need to work with our API.
<aside>As you scroll, you'll see code examples for working with the API in different programming languages in the dark area to the right (or as part of the content on mobile).
You can switch the language used with the tabs at the top right (or from the nav menu at the top left on mobile).</aside>
INTRO
,
// Example requests for each endpoint will be shown in each of these languages.
// Supported options are: bash, javascript, php, python
// To add a language of your own, see https://scribe.knuckles.wtf/laravel/advanced/example-requests
'example_languages' => [
'bash',
'javascript',
],
// Generate a Postman collection (v2.1.0) in addition to HTML docs.
// For 'static' docs, the collection will be generated to public/docs/collection.json.
// For 'laravel' docs, it will be generated to storage/app/scribe/collection.json.
// Setting `laravel.add_routes` to true (above) will also add a route for the collection.
'postman' => [
'enabled' => true,
'overrides' => [
// 'info.version' => '2.0.0',
],
],
// Generate an OpenAPI spec (v3.0.1) in addition to docs webpage.
// For 'static' docs, the collection will be generated to public/docs/openapi.yaml.
// For 'laravel' docs, it will be generated to storage/app/scribe/openapi.yaml.
// Setting `laravel.add_routes` to true (above) will also add a route for the spec.
'openapi' => [
'enabled' => true,
'overrides' => [
// 'info.version' => '2.0.0',
],
],
'groups' => [
// Endpoints which don't have a @group will be placed in this default group.
'default' => 'Endpoints',
// By default, Scribe will sort groups alphabetically, and endpoints in the order their routes are defined.
// You can override this by listing the groups, subgroups and endpoints here in the order you want them.
// See https://scribe.knuckles.wtf/blog/laravel-v4#easier-sorting and https://scribe.knuckles.wtf/laravel/reference/config#order for details
'order' => [],
],
// Custom logo path. This will be used as the value of the src attribute for the <img> tag,
// so make sure it points to an accessible URL or path. Set to false to not use a logo.
// For example, if your logo is in public/img:
// - 'logo' => '../img/logo.png' // for `static` type (output folder is public/docs)
// - 'logo' => 'img/logo.png' // for `laravel` type
'logo' => false,
// Customize the "Last updated" value displayed in the docs by specifying tokens and formats.
// Examples:
// - {date:F j Y} => March 28, 2022
// - {git:short} => Short hash of the last Git commit
// Available tokens are `{date:<format>}` and `{git:<format>}`.
// The format you pass to `date` will be passed to PHP's `date()` function.
// The format you pass to `git` can be either "short" or "long".
'last_updated' => 'Last updated: {date:F j, Y}',
'examples' => [
// Set this to any number (e.g. 1234) to generate the same example values for parameters on each run,
'faker_seed' => null,
// With API resources and transformers, Scribe tries to generate example models to use in your API responses.
// By default, Scribe will try the model's factory, and if that fails, try fetching the first from the database.
// You can reorder or remove strategies here.
'models_source' => ['factoryCreate', 'factoryMake', 'databaseFirst'],
],
// The strategies Scribe will use to extract information about your routes at each stage.
// If you create or install a custom strategy, add it here.
'strategies' => [
'metadata' => [
Strategies\Metadata\GetFromDocBlocks::class,
Strategies\Metadata\GetFromMetadataAttributes::class,
],
'urlParameters' => [
Strategies\UrlParameters\GetFromLaravelAPI::class,
Strategies\UrlParameters\GetFromUrlParamAttribute::class,
Strategies\UrlParameters\GetFromUrlParamTag::class,
],
'queryParameters' => [
Strategies\QueryParameters\GetFromFormRequest::class,
Strategies\QueryParameters\GetFromInlineValidator::class,
Strategies\QueryParameters\GetFromQueryParamAttribute::class,
Strategies\QueryParameters\GetFromQueryParamTag::class,
],
'headers' => [
Strategies\Headers\GetFromHeaderAttribute::class,
Strategies\Headers\GetFromHeaderTag::class,
[
'override',
[
'Content-Type' => 'application/json',
'Accept' => 'application/json',
]
]
],
'bodyParameters' => [
Strategies\BodyParameters\GetFromFormRequest::class,
Strategies\BodyParameters\GetFromInlineValidator::class,
Strategies\BodyParameters\GetFromBodyParamAttribute::class,
Strategies\BodyParameters\GetFromBodyParamTag::class,
],
'responses' => [
Strategies\Responses\UseResponseAttributes::class,
Strategies\Responses\UseTransformerTags::class,
Strategies\Responses\UseApiResourceTags::class,
Strategies\Responses\UseResponseTag::class,
Strategies\Responses\UseResponseFileTag::class,
[
Strategies\Responses\ResponseCalls::class,
[
'only' => ['GET *'],
// Disable debug mode when generating response calls to avoid error stack traces in responses
'config' => [
'app.debug' => false,
],
]
]
],
'responseFields' => [
Strategies\ResponseFields\GetFromResponseFieldAttribute::class,
Strategies\ResponseFields\GetFromResponseFieldTag::class,
],
],
// For response calls, API resource responses and transformer responses,
// Scribe will try to start database transactions, so no changes are persisted to your database.
// Tell Scribe which connections should be transacted here. If you only use one db connection, you can leave this as is.
'database_connections_to_transact' => [config('database.default')],
'fractal' => [
// If you are using a custom serializer with league/fractal, you can specify it here.
'serializer' => null,
],
'routeMatcher' => \Knuckles\Scribe\Matching\RouteMatcher::class,
];

View File

@ -22,14 +22,14 @@ return new class extends Migration {
DB::table('v2_plan')->orderBy('id')->chunk(100, function ($plans) {
foreach ($plans as $plan) {
$prices = array_filter([
'monthly' => $plan->month_price / 100,
'quarterly' => $plan->quarter_price / 100,
'half_yearly' => $plan->half_year_price / 100,
'yearly' => $plan->year_price / 100,
'two_yearly' => $plan->two_year_price / 100,
'three_yearly' => $plan->three_year_price / 100,
'onetime' => $plan->onetime_price / 100,
'reset_traffic' => $plan->reset_price / 100
'monthly' => $plan->month_price !== null ? $plan->month_price / 100 : null,
'quarterly' => $plan->quarter_price !== null ? $plan->quarter_price / 100 : null,
'half_yearly' => $plan->half_year_price !== null ? $plan->half_year_price / 100 : null,
'yearly' => $plan->year_price !== null ? $plan->year_price / 100 : null,
'two_yearly' => $plan->two_year_price !== null ? $plan->two_year_price / 100 : null,
'three_yearly' => $plan->three_year_price !== null ? $plan->three_year_price / 100 : null,
'onetime' => $plan->onetime_price !== null ? $plan->onetime_price / 100 : null,
'reset_traffic' => $plan->reset_price !== null ? $plan->reset_price / 100 : null
], function ($price) {
return $price !== null;
});
@ -96,7 +96,7 @@ return new class extends Migration {
DB::table('v2_plan')
->where('id', $plan->id)
->update([
'month_price' => $prices['monthly'] * 100 ?? null,
'month_price' => $prices['monthly'] * 100 ?? null,
'quarter_price' => $prices['quarterly'] * 100 ?? null,
'half_year_price' => $prices['half_yearly'] * 100 ?? null,
'year_price' => $prices['yearly'] * 100 ?? null,

View File

@ -20,7 +20,7 @@ return new class extends Migration {
$table->json('group_ids')->nullable()->comment('Group ID');
$table->json('route_ids')->nullable()->comment('Route ID');
$table->string('name')->comment('Server Name');
$table->integer('rate')->comment('Traffic Rate');
$table->decimal('rate', 8, 2)->comment('Traffic Rate');
$table->json('tags')->nullable()->comment('Server Tags');
$table->string('host')->comment('Server Host');
$table->string('port')->comment('Client Port');
@ -42,7 +42,7 @@ return new class extends Migration {
'group_ids' => $server->group_id ?: "[]",
'route_ids' => $server->route_id ?: "[]",
'name' => $server->name,
'rate' => (int) $server->rate,
'rate' => $server->rate,
'tags' => $server->tags ?: "[]",
'host' => $server->host,
'port' => $server->port,
@ -70,7 +70,7 @@ return new class extends Migration {
'group_ids' => $server->group_id ?: "[]",
'route_ids' => $server->route_id ?: "[]",
'name' => $server->name,
'rate' => (int) $server->rate,
'rate' => $server->rate,
'tags' => $server->tags ?: "[]",
'host' => $server->host,
'port' => $server->port,
@ -100,7 +100,7 @@ return new class extends Migration {
'group_ids' => $server->group_id ?: "[]",
'route_ids' => $server->route_id ?: "[]",
'name' => $server->name,
'rate' => (int) $server->rate,
'rate' => $server->rate,
'tags' => $server->tags ?: "[]",
'host' => $server->host,
'port' => $server->port,
@ -136,7 +136,7 @@ return new class extends Migration {
'group_ids' => $server->group_id ?: "[]",
'route_ids' => $server->route_id ?: "[]",
'name' => $server->name,
'rate' => (int) $server->rate,
'rate' => $server->rate,
'tags' => $server->tags ?: "[]",
'host' => $server->host,
'port' => $server->port,
@ -163,7 +163,7 @@ return new class extends Migration {
'group_ids' => $server->group_id ?: "[]",
'route_ids' => $server->route_id ?: "[]",
'name' => $server->name,
'rate' => (int) $server->rate,
'rate' => $server->rate,
'tags' => $server->tags ?: "[]",
'host' => $server->host,
'port' => $server->port,
@ -449,7 +449,7 @@ return new class extends Migration {
'name' => $server->name,
'parent_id' => $server->parent_id,
'host' => $server->host,
'port' => (int) $server->port,
'port' => $server->port,
'server_port' => $server->server_port,
'tls' => $settings['tls'],
'tls_settings' => json_encode($tlsSettings),

View File

@ -45,7 +45,7 @@ chattr -i .user.ini
rm -rf .htaccess 404.html 502.html index.html .user.ini
# 克隆代码
git clone https://github.com/cedar2025/Xboard.git ./
git clone -b new https://github.com/cedar2025/Xboard.git ./
# 准备配置文件
cp compose.sample.yaml compose.yaml

View File

@ -18,7 +18,7 @@ bash install_6.0_en.sh aapanel
1. 在 aaPanel 中安装 LNMP
- Nginx任意版本
- MySQL 5.7
- PHP 8.1
- PHP 8.2
2. 安装 PHP 扩展:
- redis

View File

@ -1,41 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
</whitelist>
</filter>
<extensions>
<extension class="Tests\Bootstrap"/>
</extensions>
<php>
<server name="APP_ENV" value="testing"/>
<server name="BCRYPT_ROUNDS" value="4"/>
<server name="CACHE_DRIVER" value="array"/>
<server name="MAIL_DRIVER" value="array"/>
<server name="QUEUE_CONNECTION" value="sync"/>
<server name="SESSION_DRIVER" value="array"/>
<server name="APP_CONFIG_CACHE" value="bootstrap/cache/config.phpunit.php"/>
<server name="APP_SERVICES_CACHE" value="bootstrap/cache/services.phpunit.php"/>
<server name="APP_PACKAGES_CACHE" value="bootstrap/cache/packages.phpunit.php"/>
<server name="APP_ROUTES_CACHE" value="bootstrap/cache/routes.phpunit.php"/>
<server name="APP_EVENTS_CACHE" value="bootstrap/cache/events.phpunit.php"/>
</php>
</phpunit>

View File

@ -1,5 +0,0 @@
apps:
- name : 'V2Board'
script : 'php artisan horizon'
instances: 1
out_file : './storage/logs/queue/queue.log'