mirror of
https://github.com/lkddi/Xboard.git
synced 2026-04-24 03:57:27 +08:00
feat: enhance plan validation, traffic system and email verification
- feat: add plan price validation - feat: make traffic packages stackable - feat: add commission and invite info to admin order details - feat: apply email whitelist to verification code API - fix: subscription link copy compatibility for non-HTTPS - fix: resolve route editing 500 error in certain cases - refactor: restructure traffic reset logic
This commit is contained in:
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\V1\Guest;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Utils\Dict;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class CommController extends Controller
|
||||
@@ -12,12 +13,12 @@ class CommController extends Controller
|
||||
{
|
||||
$data = [
|
||||
'tos_url' => admin_setting('tos_url'),
|
||||
'is_email_verify' => (int)admin_setting('email_verify', 0) ? 1 : 0,
|
||||
'is_invite_force' => (int)admin_setting('invite_force', 0) ? 1 : 0,
|
||||
'email_whitelist_suffix' => (int)admin_setting('email_whitelist_enable', 0)
|
||||
? $this->getEmailSuffix()
|
||||
'is_email_verify' => (int) admin_setting('email_verify', 0) ? 1 : 0,
|
||||
'is_invite_force' => (int) admin_setting('invite_force', 0) ? 1 : 0,
|
||||
'email_whitelist_suffix' => (int) admin_setting('email_whitelist_enable', 0)
|
||||
? Helper::getEmailSuffix()
|
||||
: 0,
|
||||
'is_recaptcha' => (int)admin_setting('recaptcha_enable', 0) ? 1 : 0,
|
||||
'is_recaptcha' => (int) admin_setting('recaptcha_enable', 0) ? 1 : 0,
|
||||
'recaptcha_site_key' => admin_setting('recaptcha_site_key'),
|
||||
'app_description' => admin_setting('app_description'),
|
||||
'app_url' => admin_setting('app_url'),
|
||||
@@ -25,13 +26,4 @@ class CommController extends Controller
|
||||
];
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
private function getEmailSuffix()
|
||||
{
|
||||
$suffix = admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT);
|
||||
if (!is_array($suffix)) {
|
||||
return preg_split('/,/', $suffix);
|
||||
}
|
||||
return $suffix;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
namespace App\Http\Controllers\V1\Passport;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Passport\CommSendEmailVerify;
|
||||
use App\Jobs\SendEmailJob;
|
||||
use App\Models\InviteCode;
|
||||
use App\Models\User;
|
||||
use App\Utils\CacheKey;
|
||||
use App\Utils\Dict;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use ReCaptcha\ReCaptcha;
|
||||
@@ -25,7 +25,22 @@ class CommController extends Controller
|
||||
return $this->fail([400, __('Invalid code is incorrect')]);
|
||||
}
|
||||
}
|
||||
|
||||
$email = $request->input('email');
|
||||
|
||||
// 检查白名单后缀限制
|
||||
if ((int) admin_setting('email_whitelist_enable', 0)) {
|
||||
$isRegisteredEmail = User::where('email', $email)->exists();
|
||||
if (!$isRegisteredEmail) {
|
||||
$allowedSuffixes = Helper::getEmailSuffix();
|
||||
$emailSuffix = substr(strrchr($email, '@'), 1);
|
||||
|
||||
if (!in_array($emailSuffix, $allowedSuffixes)) {
|
||||
return $this->fail([400, __('Email suffix is not in whitelist')]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Cache::get(CacheKey::get('LAST_SEND_EMAIL_VERIFY_TIMESTAMP', $email))) {
|
||||
return $this->fail([400, __('Email verification code has been sent, please request again later')]);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Http\Controllers\V1\User;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\User\UserChangePassword;
|
||||
use App\Http\Requests\User\UserTransfer;
|
||||
@@ -15,7 +14,6 @@ use App\Services\AuthService;
|
||||
use App\Services\UserService;
|
||||
use App\Utils\CacheKey;
|
||||
use App\Utils\Helper;
|
||||
use Auth;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class OrderController extends Controller
|
||||
|
||||
public function detail(Request $request)
|
||||
{
|
||||
$order = Order::with(['user', 'plan', 'commission_log'])->find($request->input('id'));
|
||||
$order = Order::with(['user', 'plan', 'commission_log', 'invite_user'])->find($request->input('id'));
|
||||
if (!$order)
|
||||
return $this->fail([400202, '订单不存在']);
|
||||
if ($order->surplus_order_ids) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\V2\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\PlanSave;
|
||||
use App\Models\Order;
|
||||
use App\Models\Plan;
|
||||
use App\Models\User;
|
||||
@@ -32,27 +33,17 @@ class PlanController extends Controller
|
||||
return $this->success($plans);
|
||||
}
|
||||
|
||||
public function save(Request $request)
|
||||
public function save(PlanSave $request)
|
||||
{
|
||||
$params = $request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'required|string',
|
||||
'content' => 'nullable|string',
|
||||
'reset_traffic_method' => 'integer|nullable',
|
||||
'transfer_enable' => 'integer|required',
|
||||
'prices' => 'array|nullable',
|
||||
'group_id' => 'integer|nullable',
|
||||
'speed_limit' => 'integer|nullable',
|
||||
'device_limit' => 'integer|nullable',
|
||||
'capacity_limit' => 'integer|nullable',
|
||||
]);
|
||||
$params = $request->validated();
|
||||
|
||||
if ($request->input('id')) {
|
||||
$plan = Plan::find($request->input('id'));
|
||||
if (!$plan) {
|
||||
return $this->fail([400202, '该订阅不存在']);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
// update user group id and transfer
|
||||
try {
|
||||
if ($request->input('force_update')) {
|
||||
User::where('plan_id', $plan->id)->update([
|
||||
|
||||
@@ -6,18 +6,13 @@ use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ServerRoute;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class RouteController extends Controller
|
||||
{
|
||||
public function fetch(Request $request)
|
||||
{
|
||||
$routes = ServerRoute::get();
|
||||
// TODO: remove on 1.8.0
|
||||
foreach ($routes as $k => $route) {
|
||||
$array = json_decode($route->match, true);
|
||||
if (is_array($array)) $routes[$k]['match'] = $array;
|
||||
}
|
||||
// TODO: remove on 1.8.0
|
||||
return [
|
||||
'data' => $routes
|
||||
];
|
||||
@@ -38,15 +33,13 @@ class RouteController extends Controller
|
||||
]);
|
||||
$params['match'] = array_filter($params['match']);
|
||||
// TODO: remove on 1.8.0
|
||||
$params['match'] = json_encode($params['match']);
|
||||
// TODO: remove on 1.8.0
|
||||
if ($request->input('id')) {
|
||||
try {
|
||||
$route = ServerRoute::find($request->input('id'));
|
||||
$route->update($params);
|
||||
return $this->success(true);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error($e);
|
||||
Log::error($e);
|
||||
return $this->fail([500,'保存失败']);
|
||||
}
|
||||
}
|
||||
@@ -54,7 +47,7 @@ class RouteController extends Controller
|
||||
ServerRoute::create($params);
|
||||
return $this->success(true);
|
||||
}catch(\Exception $e){
|
||||
\Log::error($e);
|
||||
Log::error($e);
|
||||
return $this->fail([500,'创建失败']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V2\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Models\TrafficResetLog;
|
||||
use App\Services\TrafficResetService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 流量重置管理控制器
|
||||
*/
|
||||
class TrafficResetController extends Controller
|
||||
{
|
||||
private TrafficResetService $trafficResetService;
|
||||
|
||||
public function __construct(TrafficResetService $trafficResetService)
|
||||
{
|
||||
$this->trafficResetService = $trafficResetService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流量重置日志列表
|
||||
*/
|
||||
public function logs(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'user_id' => 'nullable|integer',
|
||||
'user_email' => 'nullable|string',
|
||||
'reset_type' => 'nullable|string|in:' . implode(',', array_keys(TrafficResetLog::getResetTypeNames())),
|
||||
'trigger_source' => 'nullable|string|in:' . implode(',', array_keys(TrafficResetLog::getSourceNames())),
|
||||
'start_date' => 'nullable|date',
|
||||
'end_date' => 'nullable|date|after_or_equal:start_date',
|
||||
'per_page' => 'nullable|integer|min:1|max:10000',
|
||||
'page' => 'nullable|integer|min:1',
|
||||
]);
|
||||
|
||||
$query = TrafficResetLog::with(['user:id,email'])
|
||||
->orderBy('reset_time', 'desc');
|
||||
|
||||
// 筛选条件
|
||||
if ($request->filled('user_id')) {
|
||||
$query->where('user_id', $request->user_id);
|
||||
}
|
||||
|
||||
if ($request->filled('user_email')) {
|
||||
$query->whereHas('user', function ($query) use ($request) {
|
||||
$query->where('email', 'like', '%' . $request->user_email . '%');
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('reset_type')) {
|
||||
$query->where('reset_type', $request->reset_type);
|
||||
}
|
||||
|
||||
if ($request->filled('trigger_source')) {
|
||||
$query->where('trigger_source', $request->trigger_source);
|
||||
}
|
||||
|
||||
if ($request->filled('start_date')) {
|
||||
$query->where('reset_time', '>=', $request->start_date);
|
||||
}
|
||||
|
||||
if ($request->filled('end_date')) {
|
||||
$query->where('reset_time', '<=', $request->end_date . ' 23:59:59');
|
||||
}
|
||||
|
||||
$perPage = $request->get('per_page', 20);
|
||||
$logs = $query->paginate($perPage);
|
||||
|
||||
// 格式化数据
|
||||
$logs->getCollection()->transform(function ($log) {
|
||||
return [
|
||||
'id' => $log->id,
|
||||
'user_id' => $log->user_id,
|
||||
'user_email' => $log->user->email ?? 'N/A',
|
||||
'reset_type' => $log->reset_type,
|
||||
'reset_type_name' => $log->getResetTypeName(),
|
||||
'reset_time' => $log->reset_time,
|
||||
'old_traffic' => [
|
||||
'upload' => $log->old_upload,
|
||||
'download' => $log->old_download,
|
||||
'total' => $log->old_total,
|
||||
'formatted' => $log->formatTraffic($log->old_total),
|
||||
],
|
||||
'new_traffic' => [
|
||||
'upload' => $log->new_upload,
|
||||
'download' => $log->new_download,
|
||||
'total' => $log->new_total,
|
||||
'formatted' => $log->formatTraffic($log->new_total),
|
||||
],
|
||||
'trigger_source' => $log->trigger_source,
|
||||
'trigger_source_name' => $log->getSourceName(),
|
||||
'metadata' => $log->metadata,
|
||||
'created_at' => $log->created_at,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'data' => $logs->items(),
|
||||
'pagination' => [
|
||||
'current_page' => $logs->currentPage(),
|
||||
'last_page' => $logs->lastPage(),
|
||||
'per_page' => $logs->perPage(),
|
||||
'total' => $logs->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流量重置统计信息
|
||||
*/
|
||||
public function stats(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'days' => 'nullable|integer|min:1|max:365',
|
||||
]);
|
||||
|
||||
$days = $request->get('days', 30);
|
||||
$startDate = now()->subDays($days)->startOfDay();
|
||||
|
||||
$stats = [
|
||||
'total_resets' => TrafficResetLog::where('reset_time', '>=', $startDate)->count(),
|
||||
'auto_resets' => TrafficResetLog::where('reset_time', '>=', $startDate)
|
||||
->where('trigger_source', TrafficResetLog::SOURCE_AUTO)
|
||||
->count(),
|
||||
'manual_resets' => TrafficResetLog::where('reset_time', '>=', $startDate)
|
||||
->where('trigger_source', TrafficResetLog::SOURCE_MANUAL)
|
||||
->count(),
|
||||
'cron_resets' => TrafficResetLog::where('reset_time', '>=', $startDate)
|
||||
->where('trigger_source', TrafficResetLog::SOURCE_CRON)
|
||||
->count(),
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
'data' => $stats
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动重置用户流量
|
||||
*/
|
||||
public function resetUser(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'user_id' => 'required|integer|exists:v2_user,id',
|
||||
'reason' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$user = User::find($request->user_id);
|
||||
|
||||
if (!$this->trafficResetService->canReset($user)) {
|
||||
return response()->json([
|
||||
'message' => __('traffic_reset.user_cannot_reset')
|
||||
], 400);
|
||||
}
|
||||
|
||||
$metadata = [];
|
||||
if ($request->filled('reason')) {
|
||||
$metadata['reason'] = $request->reason;
|
||||
$metadata['admin_id'] = auth()->user()?->id;
|
||||
}
|
||||
|
||||
$success = $this->trafficResetService->manualReset($user, $metadata);
|
||||
|
||||
if (!$success) {
|
||||
return response()->json([
|
||||
'message' => __('traffic_reset.reset_failed')
|
||||
], 500);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => __('traffic_reset.reset_success'),
|
||||
'data' => [
|
||||
'user_id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'reset_time' => now(),
|
||||
'next_reset_at' => $user->fresh()->next_reset_at,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取用户重置历史
|
||||
*/
|
||||
public function userHistory(Request $request, int $userId): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'limit' => 'nullable|integer|min:1|max:50',
|
||||
]);
|
||||
|
||||
$user = User::findOrFail($userId);
|
||||
$limit = $request->get('limit', 10);
|
||||
|
||||
$history = $this->trafficResetService->getUserResetHistory($user, $limit);
|
||||
|
||||
$data = $history->map(function ($log) {
|
||||
return [
|
||||
'id' => $log->id,
|
||||
'reset_type' => $log->reset_type,
|
||||
'reset_type_name' => $log->getResetTypeName(),
|
||||
'reset_time' => $log->reset_time,
|
||||
'old_traffic' => [
|
||||
'upload' => $log->old_upload,
|
||||
'download' => $log->old_download,
|
||||
'total' => $log->old_total,
|
||||
'formatted' => $log->formatTraffic($log->old_total),
|
||||
],
|
||||
'trigger_source' => $log->trigger_source,
|
||||
'trigger_source_name' => $log->getSourceName(),
|
||||
'metadata' => $log->metadata,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
"data" => [
|
||||
'user' => [
|
||||
'id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'reset_count' => $user->reset_count,
|
||||
'last_reset_at' => $user->last_reset_at,
|
||||
'next_reset_at' => $user->next_reset_at,
|
||||
],
|
||||
'history' => $data,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user