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:
xboard
2025-06-22 01:18:38 +08:00
parent 7bab761db6
commit 4fe2f35183
34 changed files with 2176 additions and 539 deletions
@@ -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,
]
]);
}
}