feat: new xboard

This commit is contained in:
xboard
2025-01-21 14:57:54 +08:00
parent de18cfe596
commit 0f43fff242
373 changed files with 17923 additions and 20264 deletions
@@ -1,141 +0,0 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\CouponGenerate;
use App\Http\Requests\Admin\CouponSave;
use App\Models\Coupon;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class CouponController extends Controller
{
public function fetch(Request $request)
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'id';
$builder = Coupon::orderBy($sort, $sortType);
$total = $builder->count();
$coupons = $builder->forPage($current, $pageSize)
->get();
return response([
'data' => $coupons,
'total' => $total
]);
}
public function show(Request $request)
{
$request->validate([
'id' => 'required|numeric'
],[
'id.required' => '优惠券ID不能为空',
'id.numeric' => '优惠券ID必须为数字'
]);
$coupon = Coupon::find($request->input('id'));
if (!$coupon) {
return $this->fail([400202,'优惠券不存在']);
}
$coupon->show = !$coupon->show;
if (!$coupon->save()) {
return $this->fail([500,'保存失败']);
}
return $this->success(true);
}
public function generate(CouponGenerate $request)
{
if ($request->input('generate_count')) {
$this->multiGenerate($request);
return;
}
$params = $request->validated();
if (!$request->input('id')) {
if (!isset($params['code'])) {
$params['code'] = Helper::randomChar(8);
}
if (!Coupon::create($params)) {
return $this->fail([500,'创建失败']);
}
} else {
try {
Coupon::find($request->input('id'))->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'保存失败']);
}
}
return $this->success(true);
}
private function multiGenerate(CouponGenerate $request)
{
$coupons = [];
$coupon = $request->validated();
$coupon['created_at'] = $coupon['updated_at'] = time();
$coupon['show'] = 1;
unset($coupon['generate_count']);
for ($i = 0;$i < $request->input('generate_count');$i++) {
$coupon['code'] = Helper::randomChar(8);
array_push($coupons, $coupon);
}
try{
DB::beginTransaction();
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))) {
throw new \Exception();
}
DB::commit();
}catch(\Exception $e){
DB::rollBack();
return $this->fail([500, '生成失败']);
}
$data = "名称,类型,金额或比例,开始时间,结束时间,可用次数,可用于订阅,券码,生成时间\r\n";
foreach($coupons as $coupon) {
$type = ['', '金额', '比例'][$coupon['type']];
$value = ['', ($coupon['value'] / 100),$coupon['value']][$coupon['type']];
$startTime = date('Y-m-d H:i:s', $coupon['started_at']);
$endTime = date('Y-m-d H:i:s', $coupon['ended_at']);
$limitUse = $coupon['limit_use'] ?? '不限制';
$createTime = date('Y-m-d H:i:s', $coupon['created_at']);
$limitPlanIds = isset($coupon['limit_plan_ids']) ? implode("/", $coupon['limit_plan_ids']) : '不限制';
$data .= "{$coupon['name']},{$type},{$value},{$startTime},{$endTime},{$limitUse},{$limitPlanIds},{$coupon['code']},{$createTime}\r\n";
}
echo $data;
}
public function drop(Request $request)
{
$request->validate([
'id' => 'required|numeric'
],[
'id.required' => '优惠券ID不能为空',
'id.numeric' => '优惠券ID必须为数字'
]);
$coupon = Coupon::find($request->input('id'));
if (!$coupon) {
return $this->fail([400202,'优惠券不存在']);
}
if (!$coupon->delete()) {
return $this->fail([500,'删除失败']);
}
return $this->success(true);
}
}
@@ -1,73 +0,0 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\NoticeSave;
use App\Models\Notice;
use Illuminate\Http\Request;
class NoticeController extends Controller
{
public function fetch(Request $request)
{
return $this->success(Notice::orderBy('id', 'DESC')->get());
}
public function save(NoticeSave $request)
{
$data = $request->only([
'title',
'content',
'img_url',
'tags'
]);
if (!$request->input('id')) {
if (!Notice::create($data)) {
return $this->fail([500 ,'保存失败']);
}
} else {
try {
Notice::find($request->input('id'))->update($data);
} catch (\Exception $e) {
return $this->fail([500 ,'保存失败']);
}
}
return $this->success(true);
}
public function show(Request $request)
{
if (empty($request->input('id'))) {
return $this->fail([500 ,'公告ID不能为空']);
}
$notice = Notice::find($request->input('id'));
if (!$notice) {
return $this->fail([400202 ,'公告不存在']);
}
$notice->show = $notice->show ? 0 : 1;
if (!$notice->save()) {
return $this->fail([500 ,'保存失败']);
}
return $this->success(true);
}
public function drop(Request $request)
{
if (empty($request->input('id'))) {
return $this->fail([422 ,'公告ID不能为空']);
}
$notice = Notice::find($request->input('id'));
if (!$notice) {
return $this->fail([400202 ,'公告不存在']);
}
if (!$notice->delete()) {
return $this->fail([500 ,'删除失败']);
}
return $this->success(true);
}
}
@@ -1,186 +0,0 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\OrderAssign;
use App\Http\Requests\Admin\OrderFetch;
use App\Http\Requests\Admin\OrderUpdate;
use App\Models\CommissionLog;
use App\Models\Order;
use App\Models\Plan;
use App\Models\User;
use App\Services\OrderService;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class OrderController extends Controller
{
private function filter(Request $request, &$builder)
{
if ($request->input('filter')) {
foreach ($request->input('filter') as $filter) {
if ($filter['key'] === 'email') {
$user = User::where('email', "%{$filter['value']}%")->first();
if (!$user) continue;
$builder->where('user_id', $user->id);
continue;
}
if ($filter['condition'] === '模糊') {
$filter['condition'] = 'like';
$filter['value'] = "%{$filter['value']}%";
}
$builder->where($filter['key'], $filter['condition'], $filter['value']);
}
}
}
public function detail(Request $request)
{
$order = Order::find($request->input('id'));
if (!$order) return $this->fail([400202 ,'订单不存在']);
$order['commission_log'] = CommissionLog::where('trade_no', $order->trade_no)->get();
if ($order->surplus_order_ids) {
$order['surplus_orders'] = Order::whereIn('id', $order->surplus_order_ids)->get();
}
return $this->success($order);
}
public function fetch(OrderFetch $request)
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$orderModel = Order::orderBy('created_at', 'DESC');
if ($request->input('is_commission')) {
$orderModel->where('invite_user_id', '!=', NULL);
$orderModel->whereNotIn('status', [0, 2]);
$orderModel->where('commission_balance', '>', 0);
}
$this->filter($request, $orderModel);
$total = $orderModel->count();
$res = $orderModel->forPage($current, $pageSize)
->get();
$plan = Plan::get();
for ($i = 0; $i < count($res); $i++) {
for ($k = 0; $k < count($plan); $k++) {
if ($plan[$k]['id'] == $res[$i]['plan_id']) {
$res[$i]['plan_name'] = $plan[$k]['name'];
}
}
}
return response([
'data' => $res,
'total' => $total
]);
}
public function paid(Request $request)
{
$order = Order::where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
return $this->fail([400202 ,'订单不存在']);
}
if ($order->status !== 0) return $this->fail([400 ,'只能对待支付的订单进行操作']);
$orderService = new OrderService($order);
if (!$orderService->paid('manual_operation')) {
return $this->fail([500 ,'更新失败']);
}
return $this->success(true);
}
public function cancel(Request $request)
{
$order = Order::where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
return $this->fail([400202 ,'订单不存在']);
}
if ($order->status !== 0) return $this->fail([400 ,'只能对待支付的订单进行操作']);
$orderService = new OrderService($order);
if (!$orderService->cancel()) {
return $this->fail([400 ,'更新失败']);
}
return $this->success(true);
}
public function update(OrderUpdate $request)
{
$params = $request->only([
'commission_status'
]);
$order = Order::where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
return $this->fail([400202 ,'订单不存在']);
}
try {
$order->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500 ,'更新失败']);
}
return $this->success(true);
}
public function assign(OrderAssign $request)
{
$plan = Plan::find($request->input('plan_id'));
$user = User::where('email', $request->input('email'))->first();
if (!$user) {
return $this->fail([400202 ,'该用户不存在']);
}
if (!$plan) {
return $this->fail([400202 ,'该订阅不存在']);
}
$userService = new UserService();
if ($userService->isNotCompleteOrderByUserId($user->id)) {
return $this->fail([400 ,'该用户还有待支付的订单,无法分配']);
}
try {
DB::beginTransaction();
$order = new Order();
$orderService = new OrderService($order);
$order->user_id = $user->id;
$order->plan_id = $plan->id;
$order->period = $request->input('period');
$order->trade_no = Helper::guid();
$order->total_amount = $request->input('total_amount');
if ($order->period === 'reset_price') {
$order->type = Order::TYPE_RESET_TRAFFIC;
} else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id) {
$order->type = Order::TYPE_UPGRADE;
} else if ($user->expired_at > time() && $order->plan_id == $user->plan_id) {
$order->type = Order::TYPE_RENEWAL;
} else {
$order->type = Order::TYPE_NEW_PURCHASE;
}
$orderService->setInvite($user);
if (!$order->save()) {
DB::rollBack();
return $this->fail([500 ,'订单创建失败']);
}
DB::commit();
}catch(\Exception $e){
DB::rollBack();
throw $e;
}
return $this->success($order->trade_no);
}
}
@@ -1,75 +0,0 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Models\Plan;
use App\Models\ServerGroup;
use App\Models\ServerVmess;
use App\Models\User;
use App\Services\ServerService;
use Illuminate\Http\Request;
class GroupController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('group_id')) {
return $this->success([ServerGroup::find($request->input('group_id'))]);
}
$serverGroups = ServerGroup::get();
$servers = ServerService::getAllServers();
foreach ($serverGroups as $k => $v) {
$serverGroups[$k]['user_count'] = User::where('group_id', $v['id'])->count();
$serverGroups[$k]['server_count'] = 0;
foreach ($servers as $server) {
if (in_array($v['id'], $server['group_id'])) {
$serverGroups[$k]['server_count'] = $serverGroups[$k]['server_count']+1;
}
}
}
return $this->success($serverGroups);
}
public function save(Request $request)
{
if (empty($request->input('name'))) {
return $this->fail([422,'组名不能为空']);
}
if ($request->input('id')) {
$serverGroup = ServerGroup::find($request->input('id'));
} else {
$serverGroup = new ServerGroup();
}
$serverGroup->name = $request->input('name');
return $this->success($serverGroup->save());
}
public function drop(Request $request)
{
if ($request->input('id')) {
$serverGroup = ServerGroup::find($request->input('id'));
if (!$serverGroup) {
return $this->fail([400202,'组不存在']);
}
}
$servers = ServerVmess::all();
foreach ($servers as $server) {
if (in_array($request->input('id'), $server->group_id)) {
return $this->fail([400,'该组已被节点所使用,无法删除']);
}
}
if (Plan::where('group_id', $request->input('id'))->first()) {
return $this->fail([400, '该组已被订阅所使用,无法删除']);
}
if (User::where('group_id', $request->input('id'))->first()) {
return $this->fail([400, '该组已被用户所使用,无法删除']);
}
return $this->success($serverGroup->delete());
}
}
@@ -1,113 +0,0 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Models\ServerHysteria;
use Illuminate\Http\Request;
class HysteriaController extends Controller
{
public function save(Request $request)
{
$params = $request->validate([
'show' => '',
'name' => 'required',
'group_id' => 'required|array',
'route_id' => 'nullable|array',
'parent_id' => 'nullable|integer',
'host' => 'required',
'port' => 'required',
'server_port' => 'required',
'tags' => 'nullable|array',
'excludes' => 'nullable|array',
'ips' => 'nullable|array',
'rate' => 'required|numeric',
'up_mbps' => 'required|numeric|min:1',
'down_mbps' => 'required|numeric|min:1',
'server_name' => 'nullable',
'insecure' => 'required|in:0,1',
'alpn' => 'nullable|in:0,1,2,3',
'version' => 'nullable|in:1,2',
'is_obfs' => 'nullable'
],[
'name.required' => '节点名称不能为空',
'group_id.required' => '权限组不能为空',
'host.required' => '节点地址不能为空',
'port.required' => '连接端口不能为空',
'server_port' => '服务端口不能为空',
'rate.required' => '倍率不能为空',
'up_mbps.required' => '上行带宽不能为空',
'down_mbps.required' => '下行带宽不能为空',
]);
if ($request->input('id')) {
$server = ServerHysteria::find($request->input('id'));
if (!$server) {
return $this->fail([400202, '服务器不存在']);
}
try {
$server->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'保存失败']);
}
return $this->success(true);
}
if (!ServerHysteria::create($params)) {
return $this->fail([500,'创建失败']);
}
return $this->success(true);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerHysteria::find($request->input('id'));
if (!$server) {
return $this->fail([400202,'节点ID不存在']);
}
}
return $this->success($server->delete());
}
public function update(Request $request)
{
$request->validate([
'show' => 'in:0,1'
], [
'show.in' => '显示状态格式不正确'
]);
$params = $request->only([
'show',
]);
$server = ServerHysteria::find($request->input('id'));
if (!$server) {
return $this->fail([400202,'该服务器不存在']);
}
try {
$server->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'保存失败']);
}
return $this->success(true);
}
public function copy(Request $request)
{
$server = ServerHysteria::find($request->input('id'));
$server->show = 0;
if (!$server) {
return $this->fail([400202,'服务器不存在']);
}
ServerHysteria::create($server->toArray());
return $this->success(true);
}
}
@@ -1,45 +0,0 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Services\ServerService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ManageController extends Controller
{
public function getNodes(Request $request)
{
return $this->success(ServerService::getAllServers());
}
public function sort(Request $request)
{
ini_set('post_max_size', '1m');
$params = $request->only(
'shadowsocks',
'vmess',
'trojan',
'hysteria',
'vless'
) ?? [];
try{
DB::beginTransaction();
foreach ($params as $k => $v) {
$model = 'App\\Models\\Server' . ucfirst($k);
foreach($v as $id => $sort) {
$model::where('id', $id)->update(['sort' => $sort]);
}
}
DB::commit();
}catch (\Exception $e){
DB::rollBack();
\Log::error($e);
return $this->fail([500,'保存失败']);
}
return $this->success(true);
}
}
@@ -1,84 +0,0 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ServerShadowsocksSave;
use App\Http\Requests\Admin\ServerShadowsocksUpdate;
use App\Models\ServerShadowsocks;
use Illuminate\Http\Request;
class ShadowsocksController extends Controller
{
public function save(ServerShadowsocksSave $request)
{
$params = $request->validated();
if ($request->input('id')) {
$server = ServerShadowsocks::find($request->input('id'));
if (!$server) {
return $this->fail([400202, '服务器不存在']);
}
try {
$server->update($params);
return $this->success(true);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'保存失败']);
}
}
try{
ServerShadowsocks::create($params);
return $this->success(true);
}catch(\Exception $e){
\Log::error($e);
return $this->fail([500,'创建失败']);
}
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerShadowsocks::find($request->input('id'));
if (!$server) {
return $this->fail([400202, '节点不存在']);
}
}
return $this->success($server->delete());
}
public function update(ServerShadowsocksUpdate $request)
{
$params = $request->only([
'show',
]);
$server = ServerShadowsocks::find($request->input('id'));
if (!$server) {
return $this->fail([400202, '该服务器不存在']);
}
try {
$server->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'保存失败']);
}
return $this->success(true);
}
public function copy(Request $request)
{
$server = ServerShadowsocks::find($request->input('id'));
$server->show = 0;
if (!$server) {
return $this->fail([400202,'服务器不存在']);
}
ServerShadowsocks::create($server->toArray());
return $this->success(true);
}
}
@@ -1,76 +0,0 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ServerTrojanSave;
use App\Http\Requests\Admin\ServerTrojanUpdate;
use App\Models\ServerTrojan;
use Illuminate\Http\Request;
class TrojanController extends Controller
{
public function save(ServerTrojanSave $request)
{
$params = $request->validated();
if ($request->input('id')) {
$server = ServerTrojan::find($request->input('id'));
if (!$server) {
return $this->fail([400202,'服务器不存在']);
}
try {
$server->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}
ServerTrojan::create($params);
return $this->success(true);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerTrojan::find($request->input('id'));
if (!$server) {
return $this->fail([400202,'节点ID不存在']);
}
}
return $this->success($server->delete());
}
public function update(ServerTrojanUpdate $request)
{
$params = $request->only([
'show',
]);
$server = ServerTrojan::find($request->input('id'));
if (!$server) {
return $this->fail([400202,'该服务器不存在']);
}
try {
$server->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'保存失败']);
}
return $this->success(true);
}
public function copy(Request $request)
{
$server = ServerTrojan::find($request->input('id'));
$server->show = 0;
if (!$server) {
return $this->fail([400202,'服务器不存在']);
}
ServerTrojan::create($server->toArray());
return $this->success(true);
}
}
@@ -1,122 +0,0 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Models\ServerVless;
use Illuminate\Http\Request;
use ParagonIE_Sodium_Compat as SodiumCompat;
use App\Utils\Helper;
class VlessController extends Controller
{
public function save(Request $request)
{
$params = $request->validate([
'group_id' => 'required',
'route_id' => 'nullable|array',
'name' => 'required',
'parent_id' => 'nullable|integer',
'host' => 'required',
'port' => 'required',
'server_port' => 'required',
'tls' => 'required|in:0,1,2',
'tls_settings' => 'nullable|array',
'flow' => 'nullable|in:xtls-rprx-vision',
'network' => 'required',
'network_settings' => 'nullable|array',
'tags' => 'nullable|array',
'excludes' => 'nullable|array',
'ips' => 'nullable|array',
'rate' => 'required',
'show' => 'nullable|in:0,1',
'sort' => 'nullable'
],[
'name.required' => '节点名称不能为空',
'group_id.required' => '权限组不能为空',
'host.required' => '节点地址不能为空',
'port.required' => '连接端口不能为空',
'server_port' => '服务端口不能为空',
'rate.required' => '倍率不能为空',
'network.required' => '协议不能为空',
]);
if (isset($params['tls']) && (int)$params['tls'] === 2) {
$keyPair = SodiumCompat::crypto_box_keypair();
$params['tls_settings'] = $params['tls_settings'] ?? [];
if (!isset($params['tls_settings']['public_key'])) {
$params['tls_settings']['public_key'] = Helper::base64EncodeUrlSafe(SodiumCompat::crypto_box_publickey($keyPair));
}
if (!isset($params['tls_settings']['private_key'])) {
$params['tls_settings']['private_key'] = Helper::base64EncodeUrlSafe(SodiumCompat::crypto_box_secretkey($keyPair));
}
if (!isset($params['tls_settings']['short_id'])) {
$params['tls_settings']['short_id'] = substr(sha1($params['tls_settings']['private_key']), 0, 8);
}
if (!isset($params['tls_settings']['server_port'])) {
$params['tls_settings']['server_port'] = "443";
}
}
if ($request->input('id')) {
$server = ServerVless::find($request->input('id'));
if (!$server) {
return $this->fail([400202, '服务器不存在']);
}
try {
$server->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}
ServerVless::create($params);
return $this->success(true);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerVless::find($request->input('id'));
if (!$server) {
return $this->fail([400202,'节点不存在']);
}
}
return $this->success($server->delete());
}
public function update(Request $request)
{
$params = $request->validate([
'show' => 'nullable|in:0,1',
]);
$server = ServerVless::find($request->input('id'));
if (!$server) {
return $this->fail([400202, '该服务器不存在']);
}
try {
$server->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}
public function copy(Request $request)
{
$server = ServerVless::find($request->input('id'));
$server->show = 0;
if (!$server) {
return $this->fail([400202, '该服务器不存在']);
}
ServerVless::create($server->toArray());
return $this->success(true);
}
}
@@ -1,79 +0,0 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ServerVmessSave;
use App\Http\Requests\Admin\ServerVmessUpdate;
use App\Models\ServerVmess;
use Illuminate\Http\Request;
class VmessController extends Controller
{
public function save(ServerVmessSave $request)
{
$params = $request->validated();
if ($request->input('id')) {
$server = ServerVmess::find($request->input('id'));
if (!$server) {
return $this->fail([400202, '服务器不存在']);
}
try {
$server->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}
ServerVmess::create($params);
return $this->success(true);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerVmess::find($request->input('id'));
if (!$server) {
return $this->fail([400202, '节点不存在']);
}
}
return $this->success($server->delete());
}
public function update(ServerVmessUpdate $request)
{
$params = $request->only([
'show',
]);
$server = ServerVmess::find($request->input('id'));
if (!$server) {
return $this->fail([400202, '该服务器不存在']);
}
try {
$server->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}
public function copy(Request $request)
{
$server = ServerVmess::find($request->input('id'));
$server->show = 0;
if (!$server) {
return $this->fail([400202, '该服务器不存在']);
}
ServerVmess::create($server->toArray());
return $this->success(true);
}
}
@@ -1,206 +0,0 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Models\CommissionLog;
use App\Models\Order;
use App\Models\ServerHysteria;
use App\Models\ServerVless;
use App\Models\ServerShadowsocks;
use App\Models\ServerTrojan;
use App\Models\ServerVmess;
use App\Models\Stat;
use App\Models\StatServer;
use App\Models\StatUser;
use App\Models\Ticket;
use App\Models\User;
use App\Services\StatisticalService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class StatController extends Controller
{
public function getOverride(Request $request)
{
return [
'data' => [
'month_income' => Order::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
->whereNotIn('status', [0, 2])
->sum('total_amount'),
'month_register_total' => User::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
->count(),
'ticket_pending_total' => Ticket::where('status', 0)
->count(),
'commission_pending_total' => Order::where('commission_status', 0)
->where('invite_user_id', '!=', NULL)
->whereNotIn('status', [0, 2])
->where('commission_balance', '>', 0)
->count(),
'day_income' => Order::where('created_at', '>=', strtotime(date('Y-m-d')))
->where('created_at', '<', time())
->whereNotIn('status', [0, 2])
->sum('total_amount'),
'last_month_income' => Order::where('created_at', '>=', strtotime('-1 month', strtotime(date('Y-m-1'))))
->where('created_at', '<', strtotime(date('Y-m-1')))
->whereNotIn('status', [0, 2])
->sum('total_amount'),
'commission_month_payout' => CommissionLog::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
->sum('get_amount'),
'commission_last_month_payout' => CommissionLog::where('created_at', '>=', strtotime('-1 month', strtotime(date('Y-m-1'))))
->where('created_at', '<', strtotime(date('Y-m-1')))
->sum('get_amount'),
]
];
}
public function getOrder(Request $request)
{
$statistics = Stat::where('record_type', 'd')
->limit(31)
->orderBy('record_at', 'DESC')
->get()
->toArray();
$result = [];
foreach ($statistics as $statistic) {
$date = date('m-d', $statistic['record_at']);
$result[] = [
'type' => '收款金额',
'date' => $date,
'value' => $statistic['paid_total'] / 100
];
$result[] = [
'type' => '收款笔数',
'date' => $date,
'value' => $statistic['paid_count']
];
$result[] = [
'type' => '佣金金额(已发放)',
'date' => $date,
'value' => $statistic['commission_total'] / 100
];
$result[] = [
'type' => '佣金笔数(已发放)',
'date' => $date,
'value' => $statistic['commission_count']
];
}
$result = array_reverse($result);
return [
'data' => $result
];
}
// 获取当日实时流量排行
public function getServerLastRank()
{
$servers = [
'shadowsocks' => ServerShadowsocks::with(['parent'])->get()->toArray(),
'v2ray' => ServerVmess::with(['parent'])->get()->toArray(),
'trojan' => ServerTrojan::with(['parent'])->get()->toArray(),
'vmess' => ServerVmess::with(['parent'])->get()->toArray(),
'hysteria' => ServerHysteria::with(['parent'])->get()->toArray(),
'vless' => ServerVless::with(['parent'])->get()->toArray(),
];
$recordAt = strtotime(date('Y-m-d'));
$statService = new StatisticalService();
$statService->setStartAt($recordAt);
$stats = $statService->getStatServer();
$statistics = collect($stats)->map(function ($item){
$item['total'] = $item['u'] + $item['d'];
return $item;
})->sortByDesc('total')->values()->all();
foreach ($statistics as $k => $v) {
foreach ($servers[$v['server_type']] as $server) {
if ($server['id'] === $v['server_id']) {
$statistics[$k]['server_name'] = $server['name'];
if($server['parent']) $statistics[$k]['server_name'] .= "({$server['parent']['name']})";
}
}
$statistics[$k]['total'] = $statistics[$k]['total'] / 1073741824;
}
array_multisort(array_column($statistics, 'total'), SORT_DESC, $statistics);
return [
'data' => collect($statistics)->take(15)->all()
];
}
// 获取昨日节点流量排行
public function getServerYesterdayRank()
{
$servers = [
'shadowsocks' => ServerShadowsocks::with(['parent'])->get()->toArray(),
'v2ray' => ServerVmess::with(['parent'])->get()->toArray(),
'trojan' => ServerTrojan::with(['parent'])->get()->toArray(),
'vmess' => ServerVmess::with(['parent'])->get()->toArray(),
'hysteria' => ServerHysteria::with(['parent'])->get()->toArray(),
'vless' => ServerVless::with(['parent'])->get()->toArray(),
];
$startAt = strtotime('-1 day', strtotime(date('Y-m-d')));
$endAt = strtotime(date('Y-m-d'));
$statistics = StatServer::select([
'server_id',
'server_type',
'u',
'd',
DB::raw('(u+d) as total')
])
->where('record_at', '>=', $startAt)
->where('record_at', '<', $endAt)
->where('record_type', 'd')
->limit(15)
->orderBy('total', 'DESC')
->get()
->toArray();
foreach ($statistics as $k => $v) {
foreach ($servers[$v['server_type']] as $server) {
if ($server['id'] === $v['server_id']) {
$statistics[$k]['server_name'] = $server['name'];
if($server['parent']) $statistics[$k]['server_name'] .= "({$server['parent']['name']})";
}
}
$statistics[$k]['total'] = $statistics[$k]['total'] / 1073741824;
}
array_multisort(array_column($statistics, 'total'), SORT_DESC, $statistics);
return [
'data' => $statistics
];
}
public function getStatUser(Request $request)
{
$request->validate([
'user_id' => 'required|integer'
]);
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$builder = StatUser::orderBy('record_at', 'DESC')->where('user_id', $request->input('user_id'));
$total = $builder->count();
$records = $builder->forPage($current, $pageSize)
->get();
// 追加当天流量
$recordAt = strtotime(date('Y-m-d'));
$statService = new StatisticalService();
$statService->setStartAt($recordAt);
$todayTraffics = $statService->getStatUserByUserID($request->input('user_id'));
if (($current == 1) && count($todayTraffics) > 0) {
foreach ($todayTraffics as $todayTraffic){
$todayTraffic['server_rate'] = number_format($todayTraffic['server_rate'], 2);
$records->prepend($todayTraffic);
}
};
return [
'data' => $records,
'total' => $total + count($todayTraffics),
];
}
}
@@ -1,81 +0,0 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Services\ThemeService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
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.json";
if (!File::exists($themeConfigFile)) continue;
$themeConfig = json_decode(File::get($themeConfigFile), true);
if (!isset($themeConfig['configs']) || !is_array($themeConfig)) continue;
$themeConfigs[$theme] = $themeConfig;
if (admin_setting("theme_{$theme}")) continue;
$themeService = new ThemeService($theme);
$themeService->init();
}
$data = [
'themes' => $themeConfigs,
'active' => admin_setting('frontend_theme', 'Xboard')
];
return $this->success($data);
}
public function getThemeConfig(Request $request)
{
$payload = $request->validate([
'name' => 'required|in:' . join(',', $this->themes)
]);
return $this->success(admin_setting("theme_{$payload['name']}") ?? 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'])) return $this->fail([422,'参数不正确']);
$themeConfigFile = public_path("theme/{$payload['name']}/config.json");
if (!File::exists($themeConfigFile)) return $this->fail([400202,'主题不存在']);
$themeConfig = json_decode(File::get($themeConfigFile), true);
if (!isset($themeConfig['configs']) || !is_array($themeConfig)) return $this->fail([422,'主题配置文件有误']);
$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);
try {
admin_setting(["theme_{$payload['name']}" => $config]);
// sleep(2);
} catch (\Exception $e) {
return $this->fail([200002, '保存失败']);
}
return $this->success($config);
}
}
@@ -1,92 +0,0 @@
<?php
namespace App\Http\Controllers\V1\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;
class TicketController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('id')) {
$ticket = Ticket::where('id', $request->input('id'))
->first();
if (!$ticket) {
return $this->fail([400202,'工单不存在']);
}
$ticket['message'] = TicketMessage::where('ticket_id', $ticket->id)->get();
for ($i = 0; $i < count($ticket['message']); $i++) {
if ($ticket['message'][$i]['user_id'] !== $ticket->user_id) {
$ticket['message'][$i]['is_me'] = true;
} else {
$ticket['message'][$i]['is_me'] = false;
}
}
return $this->success($ticket);
}
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$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();
return response([
'data' => $res,
'total' => $total
]);
}
public function reply(Request $request)
{
$request->validate([
'id' => 'required|numeric',
'message' => 'required|string'
],[
'id.required' => '工单ID不能为空',
'message.required' => '消息不能为空'
]);
$ticketService = new TicketService();
$ticketService->replyByAdmin(
$request->input('id'),
$request->input('message'),
$request->user['id']
);
return $this->success(true);
}
public function close(Request $request)
{
$request->validate([
'id' => 'required|numeric'
],[
'id.required' => '工单ID不能为空'
]);
try {
$ticket = Ticket::findOrFail($request->input('id'));
$ticket->status = Ticket::STATUS_CLOSED;
$ticket->save();
return $this->success(true);
} catch (ModelNotFoundException $e) {
return $this->fail([400202, '工单不存在']);
} catch (\Exception $e) {
return $this->fail([500101, '关闭失败']);
}
}
}
@@ -14,7 +14,7 @@ class AppController extends Controller
public function getConfig(Request $request)
{
$servers = [];
$user = $request->user;
$user = $request->user();
$userService = new UserService();
if ($userService->isAvailable($user)) {
$servers = ServerService::getAvailableServers($user);
@@ -30,8 +30,9 @@ class AppController extends Controller
$proxies = [];
foreach ($servers as $item) {
$protocol_settings = $item['protocol_settings'];
if ($item['type'] === 'shadowsocks'
&& in_array($item['cipher'], [
&& in_array(data_get($protocol_settings, 'cipher'), [
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
@@ -8,12 +8,27 @@ use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Validation\Rule;
class ClientController extends Controller
{
/**
* Protocol prefix mapping for server names
*/
private const PROTOCOL_PREFIXES = [
'hysteria' => [
1 => '[Hy]',
2 => '[Hy2]'
],
'vless' => '[vless]',
'shadowsocks' => '[ss]',
'vmess' => '[vmess]',
'trojan' => '[trojan]',
];
// 支持hy2 的客户端版本列表
const SupportedHy2ClientVersions = [
private const CLIENT_VERSIONS = [
'NekoBox' => '1.2.7',
'sing-box' => '1.5.0',
'stash' => '2.5.0',
@@ -26,121 +41,191 @@ class ClientController extends Controller
'loon' => '637',
'v2rayng' => '1.9.5',
'v2rayN' => '6.31',
'surge' => '2398'
'surge' => '2398',
'flclash' => '0.8.0'
];
// allowed types
const AllowedTypes = ['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks', 'hysteria2'];
private const ALLOWED_TYPES = ['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks', 'hysteria2'];
/**
* 处理浏览器访问订阅的情况
*/
private function handleBrowserSubscribe($user, UserService $userService)
{
$useTraffic = $user['u'] + $user['d'];
$totalTraffic = $user['transfer_enable'];
$remainingTraffic = Helper::trafficConvert($totalTraffic - $useTraffic);
$expiredDate = $user['expired_at'] ? date('Y-m-d', $user['expired_at']) : __('Unlimited');
$resetDay = $userService->getResetDay($user);
// 获取通用订阅地址
$subscriptionUrl = Helper::getSubscribeUrl($user->token);
// 生成二维码
$writer = new \BaconQrCode\Writer(
new \BaconQrCode\Renderer\ImageRenderer(
new \BaconQrCode\Renderer\RendererStyle\RendererStyle(200),
new \BaconQrCode\Renderer\Image\SvgImageBackEnd()
)
);
$qrCode = base64_encode($writer->writeString($subscriptionUrl));
$data = [
'username' => $user->email,
'status' => $userService->isAvailable($user) ? 'active' : 'inactive',
'data_limit' => $totalTraffic ? Helper::trafficConvert($totalTraffic) : '∞',
'data_used' => Helper::trafficConvert($useTraffic),
'expired_date' => $expiredDate,
'reset_day' => $resetDay,
'subscription_url' => $subscriptionUrl,
'qr_code' => $qrCode
];
// 只有当 device_limit 不为 null 时才添加到返回数据中
if ($user->device_limit !== null) {
$data['device_limit'] = $user->device_limit;
}
return response()->view('client.subscribe', $data);
}
/**
* 检查是否是浏览器访问
*/
private function isBrowserAccess(Request $request): bool
{
$userAgent = strtolower($request->input('flag', $request->header('User-Agent')));
return str_contains($userAgent, 'mozilla')
|| str_contains($userAgent, 'chrome')
|| str_contains($userAgent, 'safari')
|| str_contains($userAgent, 'edge');
}
public function subscribe(Request $request)
{
// filter types
$types = $request->input('types', 'all');
$typesArr = $types === 'all' ? self::AllowedTypes : array_values(array_intersect(explode('|', str_replace(['|', '', ','], "|", $types)), self::AllowedTypes));
// filter keyword
$filterArr = mb_strlen($filter = $request->input('filter')) > 20 ? null : explode("|", str_replace(['|', '', ','], "|", $filter));
$flag = strtolower($request->input('flag') ?? $request->header('User-Agent', ''));
$ip = $request->input('ip', $request->ip());
// get client version
$version = preg_match('/\/v?(\d+(\.\d+){0,2})/', $flag, $matches) ? $matches[1] : null;
$supportHy2 = $version ? collect(self::SupportedHy2ClientVersions)
->contains(fn($minVersion, $client) => stripos($flag, $client) !== false && $this->versionCompare($version, $minVersion)) : true;
$user = $request->user;
// account not expired and is not banned.
$request->validate([
'types' => ['nullable', 'string'],
'filter' => ['nullable', 'string'],
'flag' => ['nullable', 'string'],
]);
$user = $request->user();
$userService = new UserService();
if ($userService->isAvailable($user)) {
// get ip location
$ip2region = new \Ip2Region();
$region = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? ($ip2region->memorySearch($ip)['region'] ?? null) : null;
// get available servers
$servers = ServerService::getAvailableServers($user);
// filter servers
$serversFiltered = $this->serverFilter($servers, $typesArr, $filterArr, $region, $supportHy2);
$this->setSubscribeInfoToServers($serversFiltered, $user, count($servers) - count($serversFiltered));
$servers = $serversFiltered;
$this->addPrefixToServerName($servers);
if ($flag) {
foreach (array_reverse(glob(app_path('Protocols') . '/*.php')) as $file) {
$file = 'App\\Protocols\\' . basename($file, '.php');
$class = new $file($user, $servers);
$classFlags = explode(',', $class->flag);
foreach ($classFlags as $classFlag) {
if (stripos($flag, $classFlag) !== false) {
return $class->handle();
}
}
}
}
$class = new General($user, $servers);
return $class->handle();
if (!$userService->isAvailable($user)) {
return response()->json(['message' => 'Account unavailable'], 403);
}
// 检测是否是浏览器访问
if ($this->isBrowserAccess($request)) {
return $this->handleBrowserSubscribe($user, $userService);
}
$clientInfo = $this->getClientInfo($request);
$types = $this->getFilteredTypes($request->input('types'), $clientInfo['supportHy2']);
$filterArr = $this->getFilterArray($request->input('filter'));
// Get available servers and apply filters
$servers = ServerService::getAvailableServers($user);
$serversFiltered = $this->filterServers(
servers: $servers,
types: $types,
filters: $filterArr,
);
$this->setSubscribeInfoToServers($serversFiltered, $user, count($servers) - count($serversFiltered));
$serversFiltered = $this->addPrefixToServerName($serversFiltered);
// Handle protocol response
if ($clientInfo['flag']) {
foreach (array_reverse(glob(app_path('Protocols') . '/*.php')) as $file) {
$className = 'App\\Protocols\\' . basename($file, '.php');
$protocol = new $className($user, $serversFiltered);
if (
collect($protocol->getFlags())
->contains(fn($f) => stripos($clientInfo['flag'], $f) !== false)
) {
return $protocol->handle();
}
}
}
return (new General($user, $serversFiltered))->handle();
}
/**
* Summary of serverFilter
* @param mixed $typesArr
* @param mixed $filterArr
* @param mixed $region
* @param mixed $supportHy2
* @return array
*/
private function serverFilter($servers, $typesArr, $filterArr, $region, $supportHy2)
private function getFilteredTypes(string|null $types, bool $supportHy2): array
{
return collect($servers)->reject(function ($server) use ($typesArr, $filterArr, $region, $supportHy2) {
if ($server['type'] == "hysteria" && $server['version'] == 2) {
if(!in_array('hysteria2', $typesArr)){
if ($types === 'all') {
return self::ALLOWED_TYPES;
}
$allowedTypes = $supportHy2
? self::ALLOWED_TYPES
: array_diff(self::ALLOWED_TYPES, ['hysteria2']);
if (!$types) {
return array_values($allowedTypes);
}
$userTypes = explode('|', str_replace(['|', '', ','], '|', $types));
return array_values(array_intersect($userTypes, $allowedTypes));
}
private function getFilterArray(?string $filter): ?array
{
if ($filter === null) {
return null;
}
return mb_strlen($filter) > 20 ? null :
explode('|', str_replace(['|', '', ','], '|', $filter));
}
private function getClientInfo(Request $request): array
{
$flag = strtolower($request->input('flag') ?? $request->header('User-Agent', ''));
preg_match('/\/v?(\d+(\.\d+){0,2})/', $flag, $matches);
$version = $matches[1] ?? null;
$supportHy2 = $version ? $this->checkHy2Support($flag, $version) : true;
return [
'flag' => $flag,
'version' => $version,
'supportHy2' => $supportHy2
];
}
private function checkHy2Support(string $flag, string $version): bool
{
$result = false;
foreach (self::CLIENT_VERSIONS as $client => $minVersion) {
if (stripos($flag, $client) !== false) {
$result = $result || version_compare($version, $minVersion, '>=');
}
}
return $result || !count(self::CLIENT_VERSIONS);
}
private function filterServers(array $servers, array $types, ?array $filters): array
{
return collect($servers)->reject(function ($server) use ($types, $filters) {
// Check Hysteria2 compatibility
if ($server['type'] === 'hysteria' && optional($server['protocol_settings'])['version'] === 2) {
if (!in_array('hysteria2', $types)) {
return true;
}elseif(false == $supportHy2){
}
} else {
if (!in_array($server['type'], $types)) {
return true;
}
}
if ($filterArr) {
foreach ($filterArr as $filter) {
if (stripos($server['name'], $filter) !== false || in_array($filter, $server['tags'] ?? [])) {
return false;
}
}
return true;
}
if (strpos($region, '中国') !== false) {
$excludes = $server['excludes'] ?? [];
if (empty($excludes)) {
return false;
}
foreach ($excludes as $v) {
$excludeList = explode("|", str_replace(["", ",", " ", ""], "|", $v));
foreach ($excludeList as $needle) {
if (stripos($region, $needle) !== false) {
return true;
}
}
}
// Apply custom filters
if ($filters) {
return !collect($filters)->contains(function ($filter) use ($server) {
return stripos($server['name'], $filter) !== false
|| in_array($filter, $server['tags'] ?? []);
});
}
return false;
})->values()->all();
}
/*
* add prefix to server name
*/
private function addPrefixToServerName(&$servers)
{
// 线路名称增加协议类型
if (admin_setting('show_protocol_to_server_enable')) {
$typePrefixes = [
'hysteria' => [1 => '[Hy]', 2 => '[Hy2]'],
'vless' => '[vless]',
'shadowsocks' => '[ss]',
'vmess' => '[vmess]',
'trojan' => '[trojan]',
];
$servers = collect($servers)->map(function ($server) use ($typePrefixes) {
if (isset($typePrefixes[$server['type']])) {
$prefix = is_array($typePrefixes[$server['type']]) ? $typePrefixes[$server['type']][$server['version']] : $typePrefixes[$server['type']];
$server['name'] = $prefix . $server['name'];
}
return $server;
})->toArray();
}
}
/**
* Summary of setSubscribeInfoToServers
@@ -163,7 +248,7 @@ class ClientController extends Controller
$useTraffic = $user['u'] + $user['d'];
$totalTraffic = $user['transfer_enable'];
$remainingTraffic = Helper::trafficConvert($totalTraffic - $useTraffic);
$expiredDate = $user['expired_at'] ? date('Y-m-d', $user['expired_at']) : '长期有效';
$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], [
@@ -179,33 +264,41 @@ class ClientController extends Controller
]));
}
/**
* 判断版本号
* Add protocol prefix to server names if enabled in admin settings
*
* @param array<int, array<string, mixed>> $servers
* @return array<int, array<string, mixed>>
*/
function versionCompare($version1, $version2)
private function addPrefixToServerName(array $servers): array
{
if (!preg_match('/^\d+(\.\d+){0,2}/', $version1) || !preg_match('/^\d+(\.\d+){0,2}/', $version2)) {
return false;
}
$v1Parts = explode('.', $version1);
$v2Parts = explode('.', $version2);
$maxParts = max(count($v1Parts), count($v2Parts));
for ($i = 0; $i < $maxParts; $i++) {
$part1 = isset($v1Parts[$i]) ? (int) $v1Parts[$i] : 0;
$part2 = isset($v2Parts[$i]) ? (int) $v2Parts[$i] : 0;
if ($part1 < $part2) {
return false;
} elseif ($part1 > $part2) {
return true;
}
if (!admin_setting('show_protocol_to_server_enable', false)) {
return $servers;
}
// 版本号相等
return true;
return collect($servers)
->map(function (array $server): array {
$server['name'] = $this->getPrefixedServerName($server);
return $server;
})
->all();
}
/**
* Get server name with protocol prefix
*
* @param array<string, mixed> $server
*/
private function getPrefixedServerName(array $server): string
{
$type = $server['type'] ?? '';
if (!isset(self::PROTOCOL_PREFIXES[$type])) {
return $server['name'] ?? '';
}
$prefix = is_array(self::PROTOCOL_PREFIXES[$type])
? self::PROTOCOL_PREFIXES[$type][$server['protocol_settings']['version'] ?? 1] ?? ''
: self::PROTOCOL_PREFIXES[$type];
return $prefix . ($server['name'] ?? '');
}
}
@@ -3,14 +3,23 @@
namespace App\Http\Controllers\V1\Guest;
use App\Http\Controllers\Controller;
use App\Http\Resources\PlanResource;
use App\Models\Plan;
use App\Services\PlanService;
use Auth;
use Illuminate\Http\Request;
class PlanController extends Controller
{
protected $planService;
public function __construct(PlanService $planService)
{
$this->planService = $planService;
}
public function fetch(Request $request)
{
$plan = Plan::where('show', 1)->get();
return $this->success($plan);
$plan = $this->planService->getAvailablePlans();
return $this->success(PlanResource::collection($plan));
}
}
@@ -23,7 +23,7 @@ class TelegramController extends Controller
if ($request->input('access_token') !== md5(admin_setting('telegram_bot_token'))) {
throw new ApiException('access_token is error', 401);
}
$data = json_decode(get_request_content(),true);
$data = json_decode(request()->getContent(),true);
$this->formatMessage($data);
$this->formatChatJoinRequest($data);
$this->handle();
@@ -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);
@@ -1,222 +0,0 @@
<?php
namespace App\Http\Controllers\V1\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Models\ServerVmess;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
/*
* V2ray Aurora
* Github: https://github.com/tokumeikoi/aurora
*/
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 user(Request $request)
{
ini_set('memory_limit', -1);
$nodeId = $request->input('node_id');
$server = ServerVmess::find($nodeId);
if (!$server) {
return $this->fail([400,'节点不存在']);
}
Cache::put(CacheKey::get('SERVER_VMESS_LAST_CHECK_AT', $server->id), time(), 3600);
$users = ServerService::getAvailableUsers($server->group_id);
$result = [];
foreach ($users as $user) {
$user->v2ray_user = [
"uuid" => $user->uuid,
"email" => sprintf("%s@v2board.user", $user->uuid),
"alter_id" => 0,
"level" => 0,
];
unset($user->uuid);
array_push($result, $user);
}
$eTag = sha1(json_encode($result));
if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
return response(null,304);
}
return response([
'msg' => 'ok',
'data' => $result,
])->header('ETag', "\"{$eTag}\"");
}
// 后端提交数据
public function submit(Request $request)
{
$server = ServerVmess::find($request->input('node_id'));
if (!$server) {
return response([
'ret' => 0,
'msg' => 'server is not found'
]);
}
$data = get_request_content();
$data = json_decode($data, true);
Cache::put(CacheKey::get('SERVER_VMESS_ONLINE_USER', $server->id), count($data), 3600);
Cache::put(CacheKey::get('SERVER_VMESS_LAST_PUSH_AT', $server->id), time(), 3600);
$userService = new UserService();
$formatData = [];
foreach ($data as $item) {
$formatData[$item['user_id']] = [$item['u'], $item['d']];
}
$userService->trafficFetch($server->toArray(), 'vmess', $formatData);
return response([
'ret' => 1,
'msg' => 'ok'
]);
}
// 后端获取配置
public function config(Request $request)
{
$request->validate([
'node_id' => 'required',
'local_port' => 'required'
],[
'node_id.required' => '节点ID不能为空',
'local_port.required' => '本地端口不能为空'
]);
try {
$json = $this->getV2RayConfig($request->input('node_id'), $request->input('local_port'));
} catch (\Exception $e) {
\Log::error($e);
throw new ApiException($e->getMessage());
}
return(json_encode($json, JSON_UNESCAPED_UNICODE));
}
private function getV2RayConfig(int $nodeId, int $localPort)
{
$server = ServerVmess::find($nodeId);
if (!$server) {
return $this->fail([400,'节点不存在']);
}
$json = json_decode(self::V2RAY_CONFIG);
$json->log->loglevel = (int)admin_setting('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(ServerVmess $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(ServerVmess $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(ServerVmess $server, object $json)
{
$domainRules = array_filter(explode(PHP_EOL, admin_setting('server_v2ray_domain')));
$protocolRules = array_filter(explode(PHP_EOL, admin_setting('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(ServerVMess $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;
}
}
}
@@ -21,13 +21,9 @@ class ShadowsocksTidalabController extends Controller
public function user(Request $request)
{
ini_set('memory_limit', -1);
$nodeId = $request->input('node_id');
$server = ServerShadowsocks::find($nodeId);
if (!$server) {
return $this->fail([400,'节点不存在']);
}
$server = $request->input('node_info');
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_CHECK_AT', $server->id), time(), 3600);
$users = ServerService::getAvailableUsers($server->group_id);
$users = ServerService::getAvailableUsers($server->group_ids);
$result = [];
foreach ($users as $user) {
array_push($result, [
@@ -49,15 +45,8 @@ class ShadowsocksTidalabController extends Controller
// 后端提交数据
public function submit(Request $request)
{
$server = ServerShadowsocks::find($request->input('node_id'));
if (!$server) {
return response([
'ret' => 0,
'msg' => 'server is not found'
]);
}
$data = get_request_content();
$data = json_decode($data, true);
$server = $request->input('node_info');
$data = json_decode(request()->getContent(), true);
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_ONLINE_USER', $server->id), count($data), 3600);
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_PUSH_AT', $server->id), time(), 3600);
$userService = new UserService();
@@ -73,4 +62,4 @@ class ShadowsocksTidalabController extends Controller
'msg' => 'ok'
]);
}
}
}
@@ -23,9 +23,8 @@ class TrojanTidalabController extends Controller
public function user(Request $request)
{
ini_set('memory_limit', -1);
$nodeId = $request->input('node_id');
$server = ServerTrojan::find($nodeId);
if (!$server) {
$server = $request->input('node_info');
if ($server->type !== 'trojan') {
return $this->fail([400, '节点不存在']);
}
Cache::put(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $server->id), time(), 3600);
@@ -51,15 +50,11 @@ class TrojanTidalabController extends Controller
// 后端提交数据
public function submit(Request $request)
{
$server = ServerTrojan::find($request->input('node_id'));
if (!$server) {
return response([
'ret' => 0,
'msg' => 'server is not found'
]);
$server = $request->input('node_info');
if ($server->type !== 'trojan') {
return $this->fail([400, '节点不存在']);
}
$data = get_request_content();
$data = json_decode($data, true);
$data = json_decode(request()->getContent(), true);
Cache::put(CacheKey::get('SERVER_TROJAN_ONLINE_USER', $server->id), count($data), 3600);
Cache::put(CacheKey::get('SERVER_TROJAN_LAST_PUSH_AT', $server->id), time(), 3600);
$userService = new UserService();
@@ -78,6 +73,10 @@ class TrojanTidalabController extends Controller
// 后端获取配置
public function config(Request $request)
{
$server = $request->input('node_info');
if ($server->type !== 'trojan') {
return $this->fail([400, '节点不存在']);
}
$request->validate([
'node_id' => 'required',
'local_port' => 'required'
@@ -86,7 +85,7 @@ class TrojanTidalabController extends Controller
'local_port.required' => '本地端口不能为空'
]);
try {
$json = $this->getTrojanConfig($request->input('node_id'), $request->input('local_port'));
$json = $this->getTrojanConfig($server, $request->input('local_port'));
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '配置获取失败']);
@@ -95,19 +94,15 @@ class TrojanTidalabController extends Controller
return (json_encode($json, JSON_UNESCAPED_UNICODE));
}
private function getTrojanConfig(int $nodeId, int $localPort)
private function getTrojanConfig($server, int $localPort)
{
$server = ServerTrojan::find($nodeId);
if (!$server) {
return $this->fail([400, '节点不存在']);
}
$protocolSettings = $server->protocol_settings;
$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->sni = data_get($protocolSettings, '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;
}
}
}
@@ -9,22 +9,30 @@ use App\Utils\CacheKey;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
use App\Services\UserOnlineService;
use Illuminate\Http\JsonResponse;
class UniProxyController extends Controller
{
public function __construct(
private readonly UserOnlineService $userOnlineService
) {
}
// 后端获取用户
public function user(Request $request)
{
ini_set('memory_limit', -1);
Cache::put(CacheKey::get('SERVER_' . strtoupper($request->input('node_type')) . '_LAST_CHECK_AT', $request->input('node_id')), time(), 3600);
$users = ServerService::getAvailableUsers($request->input('node_info')->group_id)->toArray();
$node = $request->input('node_info');
$nodeType = $node->type;
$nodeId = $node->id;
Cache::put(CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_CHECK_AT', $nodeId), time(), 3600);
$users = ServerService::getAvailableUsers($node->group_ids);
$response['users'] = $users;
$eTag = sha1(json_encode($response));
if (strpos($request->header('If-None-Match'), $eTag) !== false) {
if (strpos($request->header('If-None-Match', ''), $eTag) !== false) {
return response(null, 304);
}
@@ -34,131 +42,142 @@ class UniProxyController extends Controller
// 后端提交数据
public function push(Request $request)
{
$res = json_decode(get_request_content(), true);
$data = array_filter($res, function ($item) {
return is_array($item) && count($item) === 2 && is_numeric($item[0]) && is_numeric($item[1]);
});
$nodeType = $request->input('node_type');
$nodeId = $request->input('node_id');
// 增加单节点多服务器统计在线人数
$ip = $request->ip();
$id = $request->input("id");
$time = time();
$cacheKey = CacheKey::get('MULTI_SERVER_' . strtoupper($nodeType) . '_ONLINE_USER', $nodeId);
// 1、获取节点节点在线人数缓存
$onlineUsers = Cache::get($cacheKey) ?? [];
$onlineCollection = collect($onlineUsers);
// 过滤掉超过600秒的记录
$onlineCollection = $onlineCollection->reject(function ($item) use ($time) {
return $item['time'] < ($time - 600);
});
// 定义数据
$updatedItem = [
'id' => $id ?? $ip,
'ip' => $ip,
'online_user' => count($data),
'time' => $time
];
$existingItemIndex = $onlineCollection->search(function ($item) use ($updatedItem) {
return ($item['id'] ?? '') === $updatedItem['id'];
});
if ($existingItemIndex !== false) {
$onlineCollection[$existingItemIndex] = $updatedItem;
} else {
$onlineCollection->push($updatedItem);
$res = json_decode(request()->getContent(), true);
if (!is_array($res)) {
return $this->fail([422, 'Invalid data format']);
}
$onlineUsers = $onlineCollection->all();
Cache::put($cacheKey, $onlineUsers, 3600);
$data = array_filter($res, function ($item) {
return is_array($item)
&& count($item) === 2
&& is_numeric($item[0])
&& is_numeric($item[1]);
});
if (empty($data)) {
return $this->success(true);
}
$node = $request->input('node_info');
$nodeType = $node->type;
$nodeId = $node->id;
Cache::put(
CacheKey::get('SERVER_' . strtoupper($nodeType) . '_ONLINE_USER', $nodeId),
count($data),
3600
);
Cache::put(
CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_PUSH_AT', $nodeId),
time(),
3600
);
$online_user = $onlineCollection->sum('online_user');
Cache::put(CacheKey::get('SERVER_' . strtoupper($nodeType) . '_ONLINE_USER', $nodeId), $online_user, 3600);
Cache::put(CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_PUSH_AT', $nodeId), time(), 3600);
$userService = new UserService();
$userService->trafficFetch($request->input('node_info')->toArray(), $nodeType, $data, $ip);
$userService->trafficFetch($node->toArray(), $nodeType, $data);
return $this->success(true);
}
// 后端获取配置
public function config(Request $request)
{
$nodeType = $request->input('node_type');
$nodeInfo = $request->input('node_info');
switch ($nodeType) {
case 'shadowsocks':
$response = [
'server_port' => $nodeInfo->server_port,
'cipher' => $nodeInfo->cipher,
'obfs' => $nodeInfo->obfs,
'obfs_settings' => $nodeInfo->obfs_settings
];
$node = $request->input('node_info');
$nodeType = $node->type;
$protocolSettings = $node->protocol_settings;
$serverPort = $node->server_port;
$host = $node->host;
$baseConfig = [
'server_port' => (int) $serverPort,
'network' => data_get($protocolSettings, 'network'),
'networkSettings' => data_get($protocolSettings, 'network_settings') ?: null,
];
$response = match ($nodeType) {
'shadowsocks' => [
...$baseConfig,
'cipher' => $protocolSettings['cipher'],
'obfs' => $protocolSettings['obfs'],
'obfs_settings' => $protocolSettings['obfs_settings'],
'server_key' => match ($protocolSettings['cipher']) {
'2022-blake3-aes-128-gcm' => Helper::getServerKey($node->created_at, 16),
'2022-blake3-aes-256-gcm' => Helper::getServerKey($node->created_at, 32),
default => null
}
],
'vmess' => [
...$baseConfig,
'tls' => (int) $protocolSettings['tls']
],
'trojan' => [
...$baseConfig,
'host' => $host,
'server_name' => $protocolSettings['server_name'],
],
'vless' => [
...$baseConfig,
'tls' => (int) $protocolSettings['tls'],
'flow' => $protocolSettings['flow'],
'tls_settings' =>
match ((int) $protocolSettings['tls']) {
2 => $protocolSettings['reality_settings'],
default => $protocolSettings['tls_settings']
}
],
'hysteria' => [
'server_port' => (int) $serverPort,
'version' => (int) $protocolSettings['version'],
'host' => $host,
'server_name' => $protocolSettings['tls']['server_name'],
'up_mbps' => (int) $protocolSettings['bandwidth']['up'],
'down_mbps' => (int) $protocolSettings['bandwidth']['down'],
...match ((int) $protocolSettings['version']) {
1 => ['obfs' => $protocolSettings['obfs']['password'] ?? null],
2 => [
'obfs' => $protocolSettings['obfs']['open'] ? $protocolSettings['obfs']['type'] : null,
'obfs-password' => $protocolSettings['obfs']['password'] ?? null
],
default => []
}
],
default => []
};
if ($nodeInfo->cipher === '2022-blake3-aes-128-gcm') {
$response['server_key'] = Helper::getServerKey($nodeInfo->created_at, 16);
}
if ($nodeInfo->cipher === '2022-blake3-aes-256-gcm') {
$response['server_key'] = Helper::getServerKey($nodeInfo->created_at, 32);
}
break;
case 'vmess':
$response = [
'server_port' => $nodeInfo->server_port,
'network' => $nodeInfo->network,
'networkSettings' => $nodeInfo->networkSettings,
'tls' => $nodeInfo->tls
];
break;
case 'trojan':
$response = [
'host' => $nodeInfo->host,
'server_port' => $nodeInfo->server_port,
'server_name' => $nodeInfo->server_name,
'network' => $nodeInfo->network,
'networkSettings' => $nodeInfo->networkSettings,
];
break;
case 'hysteria':
$response = [
'version' => $nodeInfo->version,
'host' => $nodeInfo->host,
'server_port' => $nodeInfo->server_port,
'server_name' => $nodeInfo->server_name,
'up_mbps' => $nodeInfo->up_mbps,
'down_mbps' => $nodeInfo->down_mbps,
'obfs' => $nodeInfo->is_obfs ? Helper::getServerKey($nodeInfo->created_at, 16) : null
];
break;
case "vless":
$response = [
'server_port' => $nodeInfo->server_port,
'network' => $nodeInfo->network,
'network_settings' => $nodeInfo->network_settings,
'networkSettings' => $nodeInfo->network_settings,
'tls' => $nodeInfo->tls,
'flow' => $nodeInfo->flow,
'tls_settings' => $nodeInfo->tls_settings
];
break;
}
$response['base_config'] = [
'push_interval' => (int) admin_setting('server_push_interval', 60),
'pull_interval' => (int) admin_setting('server_pull_interval', 60)
];
if ($nodeInfo['route_id']) {
$response['routes'] = ServerService::getRoutes($nodeInfo['route_id']);
}
$eTag = sha1(json_encode($response));
if (strpos($request->header('If-None-Match'), $eTag) !== false) {
return response(null, 304);
if (!empty($node['route_ids'])) {
$response['routes'] = ServerService::getRoutes($node['route_ids']);
}
$eTag = sha1(json_encode($response));
if (strpos($request->header('If-None-Match', '') ?? '', $eTag) !== false) {
return response(null, 304);
}
return response($response)->header('ETag', "\"{$eTag}\"");
}
// 后端提交在线数据
public function alive(Request $request)
// 获取在线用户数据(wyx2685
public function alivelist(Request $request): JsonResponse
{
return $this->success(true);
$node = $request->input('node_info');
$deviceLimitUsers = ServerService::getAvailableUsers($node->group_ids)
->where('device_limit', '>', 0);
$alive = $this->userOnlineService->getAliveList($deviceLimitUsers);
return response()->json(['alive' => (object) $alive]);
}
// 后端提交在线数据
public function alive(Request $request): JsonResponse
{
$node = $request->input('node_info');
$data = json_decode(request()->getContent(), true);
if ($data === null) {
return response()->json([
'error' => 'Invalid online data'
], 400);
}
$this->userOnlineService->updateAliveData($data, $node->type, $node->id);
return response()->json(['data' => true]);
}
}
@@ -57,7 +57,7 @@ class TicketController extends Controller
$ticketService->replyByAdmin(
$request->input('id'),
$request->input('message'),
$request->user['id']
$request->user()->id
);
return $this->success(true);
}
@@ -16,7 +16,7 @@ class CouponController extends Controller
}
$couponService = new CouponService($request->input('code'));
$couponService->setPlanId($request->input('plan_id'));
$couponService->setUserId($request->user['id']);
$couponService->setUserId($request->user()->id);
$couponService->check();
return $this->success($couponService->getCoupon());
}
@@ -17,11 +17,11 @@ class InviteController extends Controller
{
public function save(Request $request)
{
if (InviteCode::where('user_id', $request->user['id'])->where('status', 0)->count() >= admin_setting('invite_gen_limit', 5)) {
if (InviteCode::where('user_id', $request->user()->id)->where('status', 0)->count() >= admin_setting('invite_gen_limit', 5)) {
return $this->fail([400,__('The maximum number of creations has been reached')]);
}
$inviteCode = new InviteCode();
$inviteCode->user_id = $request->user['id'];
$inviteCode->user_id = $request->user()->id;
$inviteCode->code = Helper::randomChar(8);
return $this->success($inviteCode->save());
}
@@ -30,7 +30,7 @@ class InviteController extends Controller
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('page_size') >= 10 ? $request->input('page_size') : 10;
$builder = CommissionLog::where('invite_user_id', $request->user['id'])
$builder = CommissionLog::where('invite_user_id', $request->user()->id)
->where('get_amount', '>', 0)
->orderBy('created_at', 'DESC');
$total = $builder->count();
@@ -45,7 +45,7 @@ class InviteController extends Controller
public function fetch(Request $request)
{
$commission_rate = admin_setting('invite_commission', 10);
$user = User::find($request->user['id'])
$user = User::find($request->user()->id)
->load(['codes' => fn($query) => $query->where('status', 0)]);
if ($user->commission_rate) {
$commission_rate = $user->commission_rate;
@@ -20,7 +20,7 @@ class KnowledgeController extends Controller
->first()
->toArray();
if (!$knowledge) return $this->fail([500, __('Article does not exist')]);
$user = User::find($request->user['id']);
$user = User::find($request->user()->id);
$userService = new UserService();
if (!$userService->isAvailable($user)) {
$this->formatAccessData($knowledge['body']);
@@ -12,8 +12,8 @@ class NoticeController extends Controller
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = 5;
$model = Notice::orderBy('created_at', 'DESC')
->where('show', 1);
$model = Notice::orderBy('sort', 'ASC')
->where('show', true);
$total = $model->count();
$res = $model->forPage($current, $pageSize)
->get();
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\V1\User;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\OrderSave;
use App\Http\Resources\OrderResource;
use App\Models\Order;
use App\Models\Payment;
use App\Models\Plan;
@@ -13,6 +14,7 @@ use App\Services\CouponService;
use App\Services\OrderService;
use App\Services\PaymentService;
use App\Services\PlanService;
use App\Services\Plugin\HookManager;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
@@ -22,138 +24,127 @@ class OrderController extends Controller
{
public function fetch(Request $request)
{
$model = Order::where('user_id', $request->user['id'])
->orderBy('created_at', 'DESC');
if ($request->input('status') !== null) {
$model->where('status', $request->input('status'));
}
$order = $model->get();
$plan = Plan::get();
for ($i = 0; $i < count($order); $i++) {
for ($x = 0; $x < count($plan); $x++) {
if ($order[$i]['plan_id'] === $plan[$x]['id']) {
$order[$i]['plan'] = $plan[$x];
}
}
}
return $this->success($order->makeHidden(['id', 'user_id']));
$request->validate([
'status' => 'nullable|integer|in:0,1,2,3',
]);
$orders = Order::with('plan')
->where('user_id', $request->user()->id)
->when($request->input('status') !== null, function ($query) use ($request) {
$query->where('status', $request->input('status'));
})
->orderBy('created_at', 'DESC')
->get();
return $this->success(OrderResource::collection($orders));
}
public function detail(Request $request)
{
$order = Order::where('user_id', $request->user['id'])
$request->validate([
'trade_no' => 'required|string',
]);
$order = Order::with('payment')
->where('user_id', $request->user()->id)
->where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
return $this->fail([400, __('Order does not exist or has been paid')]);
}
$order['plan'] = Plan::find($order->plan_id);
$order['try_out_plan_id'] = (int)admin_setting('try_out_plan_id');
$order['try_out_plan_id'] = (int) admin_setting('try_out_plan_id');
if (!$order['plan']) {
return $this->fail([400, __('Subscription plan does not exist')]);
}
if ($order->surplus_order_ids) {
$order['surplus_orders'] = Order::whereIn('id', $order->surplus_order_ids)->get();
}
return $this->success($order);
return $this->success(OrderResource::make($order));
}
public function save(OrderSave $request)
{
$userService = new UserService();
if ($userService->isNotCompleteOrderByUserId($request->user['id'])) {
return $this->fail([400, __('You have an unpaid or pending order, please try again later or cancel it')]);
$request->validate([
'plan_id' => 'required|exists:App\Models\Plan,id',
'period' => 'required|string'
]);
$user = User::findOrFail($request->user()->id);
$userService = app(UserService::class);
if ($userService->isNotCompleteOrderByUserId($user->id)) {
throw new ApiException(__('You have an unpaid or pending order, please try again later or cancel it'));
}
$planService = new PlanService($request->input('plan_id'));
$plan = Plan::findOrFail($request->input('plan_id'));
$planService = new PlanService($plan);
$plan = $planService->plan;
$user = User::find($request->user['id']);
// Validate plan purchase
$planService->validatePurchase($user, $request->input('period'));
if (!$plan) {
return $this->fail([400, __('Subscription plan does not exist')]);
}
return DB::transaction(function () use ($request, $plan, $user, $userService, $planService) {
$period = $request->input('period');
$newPeriod = PlanService::getPeriodKey($period);
if ($user->plan_id !== $plan->id && !$planService->haveCapacity() && $request->input('period') !== 'reset_price') {
throw new ApiException(__('Current product is sold out'));
}
if ($plan[$request->input('period')] === NULL) {
return $this->fail([400, __('This payment period cannot be purchased, please choose another period')]);
}
if ($request->input('period') === 'reset_price') {
if (!$userService->isAvailable($user) || $plan->id !== $user->plan_id) {
return $this->fail([400, __('Subscription has expired or no active subscription, unable to purchase Data Reset Package')]);
}
}
if ((!$plan->show && !$plan->renew) || (!$plan->show && $user->plan_id !== $plan->id)) {
if ($request->input('period') !== 'reset_price') {
return $this->fail([400, __('This subscription has been sold out, please choose another subscription')]);
}
}
if (!$plan->renew && $user->plan_id == $plan->id && $request->input('period') !== 'reset_price') {
return $this->fail([400, __('This subscription cannot be renewed, please change to another subscription')]);
}
if (!$plan->show && $plan->renew && !$userService->isAvailable($user)) {
return $this->fail([400, __('This subscription has expired, please change to another subscription')]);
}
try{
DB::beginTransaction();
$order = new Order();
$orderService = new OrderService($order);
$order->user_id = $request->user['id'];
$order->plan_id = $plan->id;
$order->period = $request->input('period');
$order->trade_no = Helper::generateOrderNo();
$order->total_amount = $plan[$request->input('period')];
// Create order
$order = new Order([
'user_id' => $user->id,
'plan_id' => $plan->id,
'period' => $newPeriod,
'trade_no' => Helper::generateOrderNo(),
'total_amount' => optional($plan->prices)[$newPeriod] * 100
]);
// Apply coupon if provided
if ($request->input('coupon_code')) {
$couponService = new CouponService($request->input('coupon_code'));
if (!$couponService->use($order)) {
return $this->fail([400, __('Coupon failed')]);
}
$order->coupon_id = $couponService->getId();
$this->applyCoupon($order, $request->input('coupon_code'));
}
// Set order attributes
$orderService = new OrderService($order);
$orderService->setVipDiscount($user);
$orderService->setOrderType($user);
$orderService->setInvite($user);
// Handle user balance
if ($user->balance && $order->total_amount > 0) {
$remainingBalance = $user->balance - $order->total_amount;
$userService = new UserService();
if ($remainingBalance > 0) {
if (!$userService->addBalance($order->user_id, - $order->total_amount)) {
return $this->fail([400, __('Insufficient balance')]);
}
$order->balance_amount = $order->total_amount;
$order->total_amount = 0;
} else {
if (!$userService->addBalance($order->user_id, - $user->balance)) {
return $this->fail([400, __('Insufficient balance')]);
}
$order->balance_amount = $user->balance;
$order->total_amount = $order->total_amount - $user->balance;
}
$this->handleUserBalance($order, $user, $userService);
}
if (!$order->save()) {
DB::rollBack();
return $this->fail([400, __('Failed to create order')]);
throw new ApiException(__('Failed to create order'));
}
DB::commit();
}catch (\Exception $e){
DB::rollBack();
throw $e;
}
HookManager::call('order.after_create', $order);
return $this->success($order->trade_no);
return $this->success($order->trade_no);
});
}
protected function applyCoupon(Order $order, string $couponCode): void
{
$couponService = new CouponService($couponCode);
if (!$couponService->use($order)) {
throw new ApiException(__('Coupon failed'));
}
$order->coupon_id = $couponService->getId();
}
protected function handleUserBalance(Order $order, User $user, UserService $userService): void
{
$remainingBalance = $user->balance - $order->total_amount;
if ($remainingBalance > 0) {
if (!$userService->addBalance($order->user_id, -$order->total_amount)) {
throw new ApiException(__('Insufficient balance'));
}
$order->balance_amount = $order->total_amount;
$order->total_amount = 0;
} else {
if (!$userService->addBalance($order->user_id, -$user->balance)) {
throw new ApiException(__('Insufficient balance'));
}
$order->balance_amount = $user->balance;
$order->total_amount = $order->total_amount - $user->balance;
}
}
public function checkout(Request $request)
@@ -161,7 +152,7 @@ class OrderController extends Controller
$tradeNo = $request->input('trade_no');
$method = $request->input('method');
$order = Order::where('trade_no', $tradeNo)
->where('user_id', $request->user['id'])
->where('user_id', $request->user()->id)
->where('status', 0)
->first();
if (!$order) {
@@ -170,21 +161,24 @@ class OrderController extends Controller
// free process
if ($order->total_amount <= 0) {
$orderService = new OrderService($order);
if (!$orderService->paid($order->trade_no)) return $this->fail([400, '支付失败']);
if (!$orderService->paid($order->trade_no))
return $this->fail([400, '支付失败']);
return response([
'type' => -1,
'data' => true
]);
}
$payment = Payment::find($method);
if (!$payment || $payment->enable !== 1) return $this->fail([400, __('Payment method is not available')]);
if (!$payment || $payment->enable !== 1)
return $this->fail([400, __('Payment method is not available')]);
$paymentService = new PaymentService($payment->payment, $payment->id);
$order->handling_amount = NULL;
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()) return $this->fail([400, __('Request failed, please try again later')]);
if (!$order->save())
return $this->fail([400, __('Request failed, please try again later')]);
$result = $paymentService->pay([
'trade_no' => $tradeNo,
'total_amount' => isset($order->handling_amount) ? ($order->total_amount + $order->handling_amount) : $order->total_amount,
@@ -201,7 +195,7 @@ class OrderController extends Controller
{
$tradeNo = $request->input('trade_no');
$order = Order::where('trade_no', $tradeNo)
->where('user_id', $request->user['id'])
->where('user_id', $request->user()->id)
->first();
if (!$order) {
return $this->fail([400, __('Order does not exist')]);
@@ -232,7 +226,7 @@ class OrderController extends Controller
return $this->fail([422, __('Invalid parameter')]);
}
$order = Order::where('trade_no', $request->input('trade_no'))
->where('user_id', $request->user['id'])
->where('user_id', $request->user()->id)
->first();
if (!$order) {
return $this->fail([400, __('Order does not exist')]);
+12 -13
View File
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\V1\User;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Resources\PlanResource;
use App\Models\Plan;
use App\Models\User;
use App\Services\PlanService;
@@ -11,29 +12,27 @@ use Illuminate\Http\Request;
class PlanController extends Controller
{
protected PlanService $planService;
public function __construct(PlanService $planService)
{
$this->planService = $planService;
}
public function fetch(Request $request)
{
$user = User::find($request->user['id']);
$user = User::find($request->user()->id);
if ($request->input('id')) {
$plan = Plan::where('id', $request->input('id'))->first();
if (!$plan) {
return $this->fail([400, __('Subscription plan does not exist')]);
}
if ((!$plan->show && !$plan->renew) || (!$plan->show && $user->plan_id !== $plan->id)) {
if (!$this->planService->isPlanAvailableForUser($plan, $user)) {
return $this->fail([400, __('Subscription plan does not exist')]);
}
return $this->success($plan);
return $this->success(PlanResource::make($plan));
}
$counts = PlanService::countActiveUsers();
$plans = Plan::where('show', 1)
->orderBy('sort', 'ASC')
->get();
foreach ($plans as $k => $v) {
if ($plans[$k]->capacity_limit === NULL) continue;
if (!isset($counts[$plans[$k]->id])) continue;
$plans[$k]->capacity_limit = $plans[$k]->capacity_limit - $counts[$plans[$k]->id]->count;
}
return $this->success($plans);
$plans = $this->planService->getAvailablePlans();
return $this->success(PlanResource::collection($plans));
}
}
@@ -13,14 +13,14 @@ class ServerController extends Controller
{
public function fetch(Request $request)
{
$user = User::find($request->user['id']);
$user = User::find($request->user()->id);
$servers = [];
$userService = new UserService();
if ($userService->isAvailable($user)) {
$servers = ServerService::getAvailableServers($user);
}
$eTag = sha1(json_encode(array_column($servers, 'cache_key')));
if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
if (strpos($request->header('If-None-Match', ''), $eTag) !== false ) {
return response(null,304);
}
$data = NodeResource::collection($servers);
@@ -15,23 +15,11 @@ class StatController extends Controller
{
$startDate = now()->startOfMonth()->timestamp;
$records = StatUser::query()
->where('user_id', $request->user['id'])
->where('user_id', $request->user()->id)
->where('record_at', '>=', $startDate)
->orderBy('record_at', 'DESC')
->get();
// 追加当天流量
$recordAt = strtotime(date('Y-m-d'));
$statService = new StatisticalService();
$statService->setStartAt($recordAt);
$todayTraffics = $statService->getStatUserByUserID($request->user['id']);
if (count($todayTraffics) > 0) {
$todayTraffics = collect($todayTraffics)->map(function ($todayTraffic) {
$todayTraffic['server_rate'] = number_format($todayTraffic['server_rate'], 2);
return $todayTraffic;
});
$records = $todayTraffics->merge($records);
}
$data = TrafficLogResource::collection(collect($records));
return $this->success($data);
}
@@ -21,6 +21,6 @@ class TelegramController extends Controller
public function unbind(Request $request)
{
$user = User::where('user_id', $request->user['id'])->first();
$user = User::where('user_id', $request->user()->id)->first();
}
}
@@ -21,7 +21,7 @@ class TicketController extends Controller
{
if ($request->input('id')) {
$ticket = Ticket::where('id', $request->input('id'))
->where('user_id', $request->user['id'])
->where('user_id', $request->user()->id)
->first()
->load('message');
if (!$ticket) {
@@ -33,7 +33,7 @@ class TicketController extends Controller
});
return $this->success(TicketResource::make($ticket)->additional(['message' => true]));
}
$ticket = Ticket::where('user_id', $request->user['id'])
$ticket = Ticket::where('user_id', $request->user()->id)
->orderBy('created_at', 'DESC')
->get();
return $this->success(TicketResource::collection($ticket));
@@ -43,20 +43,20 @@ class TicketController extends Controller
{
try{
DB::beginTransaction();
if ((int)Ticket::where('status', 0)->where('user_id', $request->user['id'])->lockForUpdate()->count()) {
if ((int)Ticket::where('status', 0)->where('user_id', $request->user()->id)->lockForUpdate()->count()) {
throw new \Exception(__('There are other unresolved tickets'));
}
$ticket = Ticket::create(array_merge($request->only([
'subject',
'level'
]), [
'user_id' => $request->user['id']
'user_id' => $request->user()->id
]));
if (!$ticket) {
throw new \Exception(__('There are other unresolved tickets'));
}
$ticketMessage = TicketMessage::create([
'user_id' => $request->user['id'],
'user_id' => $request->user()->id,
'ticket_id' => $ticket->id,
'message' => $request->input('message')
]);
@@ -64,7 +64,7 @@ class TicketController extends Controller
throw new \Exception(__('Failed to open ticket'));
}
DB::commit();
$this->sendNotify($ticket, $request->input('message'), $request->user['id']);
$this->sendNotify($ticket, $request->input('message'), $request->user()->id);
return $this->success(true);
}catch(\Exception $e){
DB::rollBack();
@@ -83,7 +83,7 @@ class TicketController extends Controller
return $this->fail([400, __('Message cannot be empty')]);
}
$ticket = Ticket::where('id', $request->input('id'))
->where('user_id', $request->user['id'])
->where('user_id', $request->user()->id)
->first();
if (!$ticket) {
return $this->fail([400, __('Ticket does not exist')]);
@@ -91,18 +91,18 @@ class TicketController extends Controller
if ($ticket->status) {
return $this->fail([400, __('The ticket is closed and cannot be replied')]);
}
if ($request->user['id'] == $this->getLastMessage($ticket->id)->user_id) {
if ($request->user()->id == $this->getLastMessage($ticket->id)->user_id) {
return $this->fail([400, __('Please wait for the technical enginneer to reply')]);
}
$ticketService = new TicketService();
if (!$ticketService->reply(
$ticket,
$request->input('message'),
$request->user['id']
$request->user()->id
)) {
return $this->fail([400, __('Ticket reply failed')]);
}
$this->sendNotify($ticket, $request->input('message'), $request->user['id']);
$this->sendNotify($ticket, $request->input('message'), $request->user()->id);
return $this->success(true);
}
@@ -113,7 +113,7 @@ class TicketController extends Controller
return $this->fail([422, __('Invalid parameter')]);
}
$ticket = Ticket::where('id', $request->input('id'))
->where('user_id', $request->user['id'])
->where('user_id', $request->user()->id)
->first();
if (!$ticket) {
return $this->fail([400, __('Ticket does not exist')]);
@@ -143,7 +143,7 @@ class TicketController extends Controller
)) {
return $this->fail([422, __('Unsupported withdrawal method')]);
}
$user = User::find($request->user['id']);
$user = User::find($request->user()->id);
$limit = admin_setting('commission_withdraw_limit', 100);
if ($limit > ($user->commission_balance / 100)) {
return $this->fail([422, __('The current required minimum withdrawal commission is :limit', ['limit' => $limit])]);
@@ -154,7 +154,7 @@ class TicketController extends Controller
$ticket = Ticket::create([
'subject' => $subject,
'level' => 2,
'user_id' => $request->user['id']
'user_id' => $request->user()->id
]);
if (!$ticket) {
return $this->fail([400, __('Failed to open ticket')]);
@@ -164,7 +164,7 @@ class TicketController extends Controller
__('Withdrawal account') . "" . $request->input('withdraw_account')
);
$ticketMessage = TicketMessage::create([
'user_id' => $request->user['id'],
'user_id' => $request->user()->id,
'ticket_id' => $ticket->id,
'message' => $message
]);
@@ -177,7 +177,7 @@ class TicketController extends Controller
DB::rollBack();
throw $e;
}
$this->sendNotify($ticket, $message, $request->user['id']);
$this->sendNotify($ticket, $message, $request->user()->id);
return $this->success(true);
}
+15 -14
View File
@@ -15,6 +15,7 @@ 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 +23,7 @@ class UserController extends Controller
{
public function getActiveSession(Request $request)
{
$user = User::find($request->user['id']);
$user = User::find($request->user()->id);
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
@@ -32,7 +33,7 @@ class UserController extends Controller
public function removeActiveSession(Request $request)
{
$user = User::find($request->user['id']);
$user = User::find($request->user()->id);
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
@@ -43,9 +44,9 @@ class UserController extends Controller
public function checkLogin(Request $request)
{
$data = [
'is_login' => $request->user['id'] ? true : false
'is_login' => $request->user()?->id ? true : false
];
if ($request->user['is_admin']) {
if ($request->user()?->is_admin) {
$data['is_admin'] = true;
}
return $this->success($data);
@@ -53,7 +54,7 @@ class UserController extends Controller
public function changePassword(UserChangePassword $request)
{
$user = User::find($request->user['id']);
$user = User::find($request->user()->id);
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
@@ -76,7 +77,7 @@ class UserController extends Controller
public function info(Request $request)
{
$user = User::where('id', $request->user['id'])
$user = User::where('id', $request->user()->id)
->select([
'email',
'transfer_enable',
@@ -106,12 +107,12 @@ class UserController extends Controller
{
$stat = [
Order::where('status', 0)
->where('user_id', $request->user['id'])
->where('user_id', $request->user()->id)
->count(),
Ticket::where('status', 0)
->where('user_id', $request->user['id'])
->where('user_id', $request->user()->id)
->count(),
User::where('invite_user_id', $request->user['id'])
User::where('invite_user_id', $request->user()->id)
->count()
];
return $this->success($stat);
@@ -119,7 +120,7 @@ class UserController extends Controller
public function getSubscribe(Request $request)
{
$user = User::where('id', $request->user['id'])
$user = User::where('id', $request->user()->id)
->select([
'plan_id',
'token',
@@ -148,7 +149,7 @@ class UserController extends Controller
public function resetSecurity(Request $request)
{
$user = User::find($request->user['id']);
$user = User::find($request->user()->id);
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
@@ -167,7 +168,7 @@ class UserController extends Controller
'remind_traffic'
]);
$user = User::find($request->user['id']);
$user = User::find($request->user()->id);
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
@@ -182,7 +183,7 @@ class UserController extends Controller
public function transfer(UserTransfer $request)
{
$user = User::find($request->user['id']);
$user = User::find($request->user()->id);
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
@@ -199,7 +200,7 @@ class UserController extends Controller
public function getQuickLoginUrl(Request $request)
{
$user = User::find($request->user['id']);
$user = User::find($request->user()->id);
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
@@ -1,15 +1,15 @@
<?php
namespace App\Http\Controllers\V1\Admin;
namespace App\Http\Controllers\V2\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ConfigSave;
use App\Models\Setting;
use App\Services\MailService;
use App\Services\TelegramService;
use App\Services\ThemeService;
use App\Utils\Dict;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class ConfigController extends Controller
{
@@ -34,7 +34,7 @@ class ConfigController extends Controller
public function testSendMail(Request $request)
{
$mailLog = MailService::sendEmail([
'email' => $request->user['email'],
'email' => $request->user()->email,
'subject' => 'This is xboard test email',
'template_name' => 'notify',
'template_value' => [
@@ -44,8 +44,7 @@ class ConfigController extends Controller
]
]);
return response([
'data' => true,
'log' => $mailLog
'data' => $mailLog,
]);
}
@@ -53,8 +52,9 @@ class ConfigController extends Controller
{
// 判断站点网址
$app_url = admin_setting('app_url');
if(blank($app_url)) return $this->fail([422, '请先设置站点网址']);
$hookUrl = $app_url .'/api/v1/guest/telegram/webhook?' . http_build_query([
if (blank($app_url))
return $this->fail([422, '请先设置站点网址']);
$hookUrl = $app_url . '/api/v1/guest/telegram/webhook?' . http_build_query([
'access_token' => md5(admin_setting('telegram_bot_token', $request->input('telegram_bot_token')))
]);
$telegramService = new TelegramService($request->input('telegram_bot_token'));
@@ -68,45 +68,46 @@ class ConfigController extends Controller
$key = $request->input('key');
$data = [
'invite' => [
'invite_force' => (int)admin_setting('invite_force', 0),
'invite_force' => (bool) admin_setting('invite_force', 0),
'invite_commission' => admin_setting('invite_commission', 10),
'invite_gen_limit' => admin_setting('invite_gen_limit', 5),
'invite_never_expire' => admin_setting('invite_never_expire', 0),
'commission_first_time_enable' => admin_setting('commission_first_time_enable', 1),
'commission_auto_check_enable' => admin_setting('commission_auto_check_enable', 1),
'invite_never_expire' => (bool) admin_setting('invite_never_expire', 0),
'commission_first_time_enable' => (bool) admin_setting('commission_first_time_enable', 1),
'commission_auto_check_enable' => (bool) admin_setting('commission_auto_check_enable', 1),
'commission_withdraw_limit' => admin_setting('commission_withdraw_limit', 100),
'commission_withdraw_method' => admin_setting('commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT),
'withdraw_close_enable' => admin_setting('withdraw_close_enable', 0),
'commission_distribution_enable' => admin_setting('commission_distribution_enable', 0),
'withdraw_close_enable' => (bool) admin_setting('withdraw_close_enable', 0),
'commission_distribution_enable' => (bool) admin_setting('commission_distribution_enable', 0),
'commission_distribution_l1' => admin_setting('commission_distribution_l1'),
'commission_distribution_l2' => admin_setting('commission_distribution_l2'),
'commission_distribution_l3' => admin_setting('commission_distribution_l3')
],
'site' => [
'logo' => admin_setting('logo'),
'force_https' => (int)admin_setting('force_https', 0),
'stop_register' => (int)admin_setting('stop_register', 0),
'force_https' => (int) admin_setting('force_https', 0),
'stop_register' => (int) admin_setting('stop_register', 0),
'app_name' => admin_setting('app_name', 'XBoard'),
'app_description' => admin_setting('app_description', 'XBoard is best!'),
'app_url' => admin_setting('app_url'),
'subscribe_url' => admin_setting('subscribe_url'),
'try_out_plan_id' => (int)admin_setting('try_out_plan_id', 0),
'try_out_hour' => (int)admin_setting('try_out_hour', 1),
'try_out_plan_id' => (int) admin_setting('try_out_plan_id', 0),
'try_out_hour' => (int) admin_setting('try_out_hour', 1),
'tos_url' => admin_setting('tos_url'),
'currency' => admin_setting('currency', 'CNY'),
'currency_symbol' => admin_setting('currency_symbol', '¥'),
],
'subscribe' => [
'plan_change_enable' => (int)admin_setting('plan_change_enable', 1),
'reset_traffic_method' => (int)admin_setting('reset_traffic_method', 0),
'surplus_enable' => (int)admin_setting('surplus_enable', 1),
'new_order_event_id' => (int)admin_setting('new_order_event_id', 0),
'renew_order_event_id' => (int)admin_setting('renew_order_event_id', 0),
'change_order_event_id' => (int)admin_setting('change_order_event_id', 0),
'show_info_to_server_enable' => (int)admin_setting('show_info_to_server_enable', 0),
'show_protocol_to_server_enable' => (int)admin_setting('show_protocol_to_server_enable', 0),
'default_remind_expire' => (int)admin_setting('default_remind_expire',1),
'default_remind_traffic' => (int)admin_setting('default_remind_traffic',1),
'plan_change_enable' => (bool) admin_setting('plan_change_enable', 1),
'reset_traffic_method' => (int) admin_setting('reset_traffic_method', 0),
'surplus_enable' => (bool) admin_setting('surplus_enable', 1),
'new_order_event_id' => (int) admin_setting('new_order_event_id', 0),
'renew_order_event_id' => (int) admin_setting('renew_order_event_id', 0),
'change_order_event_id' => (int) admin_setting('change_order_event_id', 0),
'show_info_to_server_enable' => (bool) admin_setting('show_info_to_server_enable', 0),
'show_protocol_to_server_enable' => (bool) admin_setting('show_protocol_to_server_enable', 0),
'default_remind_expire' => (bool) admin_setting('default_remind_expire', 1),
'default_remind_traffic' => (bool) admin_setting('default_remind_traffic', 1),
'subscribe_path' => admin_setting('subscribe_path', 's'),
],
'frontend' => [
@@ -120,6 +121,7 @@ class ConfigController extends Controller
'server_token' => admin_setting('server_token'),
'server_pull_interval' => admin_setting('server_pull_interval', 60),
'server_push_interval' => admin_setting('server_push_interval', 60),
'device_limit_mode' => (int) admin_setting('device_limit_mode', 0),
],
'email' => [
'email_template' => admin_setting('email_template', 'default'),
@@ -128,35 +130,36 @@ class ConfigController extends Controller
'email_username' => admin_setting('email_username'),
'email_password' => admin_setting('email_password'),
'email_encryption' => admin_setting('email_encryption'),
'email_from_address' => admin_setting('email_from_address')
'email_from_address' => admin_setting('email_from_address'),
'remind_mail_enable' => (bool) admin_setting('remind_mail_enable', false),
],
'telegram' => [
'telegram_bot_enable' => admin_setting('telegram_bot_enable', 0),
'telegram_bot_enable' => (bool) admin_setting('telegram_bot_enable', 0),
'telegram_bot_token' => admin_setting('telegram_bot_token'),
'telegram_discuss_link' => admin_setting('telegram_discuss_link')
],
'app' => [
'windows_version' => admin_setting('windows_version'),
'windows_download_url' => admin_setting('windows_download_url'),
'macos_version' => admin_setting('macos_version'),
'macos_download_url' => admin_setting('macos_download_url'),
'android_version' => admin_setting('android_version'),
'android_download_url' => admin_setting('android_download_url')
'windows_version' => admin_setting('windows_version', ''),
'windows_download_url' => admin_setting('windows_download_url', ''),
'macos_version' => admin_setting('macos_version', ''),
'macos_download_url' => admin_setting('macos_download_url', ''),
'android_version' => admin_setting('android_version', ''),
'android_download_url' => admin_setting('android_download_url', '')
],
'safe' => [
'email_verify' => (int)admin_setting('email_verify', 0),
'safe_mode_enable' => (int)admin_setting('safe_mode_enable', 0),
'email_verify' => (bool) admin_setting('email_verify', 0),
'safe_mode_enable' => (bool) admin_setting('safe_mode_enable', 0),
'secure_path' => admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key')))),
'email_whitelist_enable' => (int)admin_setting('email_whitelist_enable', 0),
'email_whitelist_enable' => (bool) admin_setting('email_whitelist_enable', 0),
'email_whitelist_suffix' => admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT),
'email_gmail_limit_enable' => admin_setting('email_gmail_limit_enable', 0),
'recaptcha_enable' => (int)admin_setting('recaptcha_enable', 0),
'recaptcha_key' => admin_setting('recaptcha_key'),
'recaptcha_site_key' => admin_setting('recaptcha_site_key'),
'register_limit_by_ip_enable' => (int)admin_setting('register_limit_by_ip_enable', 0),
'email_gmail_limit_enable' => (bool) admin_setting('email_gmail_limit_enable', 0),
'recaptcha_enable' => (bool) admin_setting('recaptcha_enable', 0),
'recaptcha_key' => admin_setting('recaptcha_key', ''),
'recaptcha_site_key' => admin_setting('recaptcha_site_key', ''),
'register_limit_by_ip_enable' => (bool) admin_setting('register_limit_by_ip_enable', 0),
'register_limit_count' => admin_setting('register_limit_count', 3),
'register_limit_expire' => admin_setting('register_limit_expire', 60),
'password_limit_enable' => (int)admin_setting('password_limit_enable', 1),
'password_limit_enable' => (bool) admin_setting('password_limit_enable', 1),
'password_limit_count' => admin_setting('password_limit_count', 5),
'password_limit_expire' => admin_setting('password_limit_expire', 60)
]
@@ -165,7 +168,8 @@ class ConfigController extends Controller
return $this->success([
$key => $data[$key]
]);
};
}
;
// TODO: default should be in Dict
return $this->success($data);
}
@@ -173,26 +177,13 @@ class ConfigController extends Controller
public function save(ConfigSave $request)
{
$data = $request->validated();
$config = config('v2board');
foreach (ConfigSave::RULES as $k => $v) {
if (!in_array($k, array_keys(ConfigSave::RULES))) {
unset($config[$k]);
continue;
}
if (array_key_exists($k, $data)) {
$value = $data[$k];
if (is_array($value)) $value = json_encode($value);
Setting::updateOrCreate(
['name' => $k],
['name' => $k, 'value' => $value]
);
foreach ($data as $k => $v) {
if ($k == 'frontend_theme') {
$themeService = new ThemeService();
$themeService->switch($v);
}
admin_setting([$k => $v]);
}
// 如果是workerman环境,则触发reload
if(isset(get_defined_constants(true)['user']['Workerman'])){
posix_kill(posix_getppid(), SIGUSR1);
}
Cache::forget('admin_settings');
// \Artisan::call('horizon:terminate'); //重启队列使配置生效
return $this->success(true);
}
@@ -0,0 +1,189 @@
<?php
namespace App\Http\Controllers\V2\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\CouponGenerate;
use App\Http\Requests\Admin\CouponSave;
use App\Models\Coupon;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class CouponController extends Controller
{
private function applyFiltersAndSorts(Request $request, $builder)
{
if ($request->has('filter')) {
collect($request->input('filter'))->each(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}%");
}
});
});
}
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);
});
}
}
public function fetch(Request $request)
{
$current = $request->input('current', 1);
$pageSize = $request->input('pageSize', 10);
$builder = Coupon::query();
$this->applyFiltersAndSorts($request, $builder);
$coupons = $builder
->orderBy('created_at', 'desc')
->paginate($pageSize, ["*"], 'page', $current);
return response([
'data' => $coupons->items(),
'total' => $coupons->total()
]);
}
public function update(Request $request)
{
$params = $request->validate([
'id' => 'required|numeric',
'show' => 'nullable|boolean'
], [
'id.required' => '优惠券ID不能为空',
'id.numeric' => '优惠券ID必须为数字'
]);
try {
DB::beginTransaction();
$coupon = Coupon::find($request->input('id'));
if (!$coupon) {
throw new ApiException(400201, '优惠券不存在');
}
$coupon->update($params);
DB::commit();
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '保存失败']);
}
}
public function show(Request $request)
{
$request->validate([
'id' => 'required|numeric'
], [
'id.required' => '优惠券ID不能为空',
'id.numeric' => '优惠券ID必须为数字'
]);
$coupon = Coupon::find($request->input('id'));
if (!$coupon) {
return $this->fail([400202, '优惠券不存在']);
}
$coupon->show = !$coupon->show;
if (!$coupon->save()) {
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}
public function generate(CouponGenerate $request)
{
if ($request->input('generate_count')) {
$this->multiGenerate($request);
return;
}
$params = $request->validated();
if (!$request->input('id')) {
if (!isset($params['code'])) {
$params['code'] = Helper::randomChar(8);
}
if (!Coupon::create($params)) {
return $this->fail([500, '创建失败']);
}
} else {
try {
Coupon::find($request->input('id'))->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '保存失败']);
}
}
return $this->success(true);
}
private function multiGenerate(CouponGenerate $request)
{
$coupons = [];
$coupon = $request->validated();
$coupon['created_at'] = $coupon['updated_at'] = time();
$coupon['show'] = 1;
unset($coupon['generate_count']);
for ($i = 0; $i < $request->input('generate_count'); $i++) {
$coupon['code'] = Helper::randomChar(8);
array_push($coupons, $coupon);
}
try {
DB::beginTransaction();
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))
) {
throw new \Exception();
}
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
return $this->fail([500, '生成失败']);
}
$data = "名称,类型,金额或比例,开始时间,结束时间,可用次数,可用于订阅,券码,生成时间\r\n";
foreach ($coupons as $coupon) {
$type = ['', '金额', '比例'][$coupon['type']];
$value = ['', ($coupon['value'] / 100), $coupon['value']][$coupon['type']];
$startTime = date('Y-m-d H:i:s', $coupon['started_at']);
$endTime = date('Y-m-d H:i:s', $coupon['ended_at']);
$limitUse = $coupon['limit_use'] ?? '不限制';
$createTime = date('Y-m-d H:i:s', $coupon['created_at']);
$limitPlanIds = isset($coupon['limit_plan_ids']) ? implode("/", $coupon['limit_plan_ids']) : '不限制';
$data .= "{$coupon['name']},{$type},{$value},{$startTime},{$endTime},{$limitUse},{$limitPlanIds},{$coupon['code']},{$createTime}\r\n";
}
echo $data;
}
public function drop(Request $request)
{
$request->validate([
'id' => 'required|numeric'
], [
'id.required' => '优惠券ID不能为空',
'id.numeric' => '优惠券ID必须为数字'
]);
$coupon = Coupon::find($request->input('id'));
if (!$coupon) {
return $this->fail([400202, '优惠券不存在']);
}
if (!$coupon->delete()) {
return $this->fail([500, '删除失败']);
}
return $this->success(true);
}
}
@@ -1,6 +1,6 @@
<?php
namespace App\Http\Controllers\V1\Admin;
namespace App\Http\Controllers\V2\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
@@ -16,12 +16,13 @@ class KnowledgeController extends Controller
{
if ($request->input('id')) {
$knowledge = Knowledge::find($request->input('id'))->toArray();
if (!$knowledge) return $this->fail([400202,'知识不存在']);
if (!$knowledge)
return $this->fail([400202, '知识不存在']);
return $this->success($knowledge);
}
$data = Knowledge::select(['title', 'id', 'updated_at', 'category', 'show'])
->orderBy('sort', 'ASC')
->get();
->orderBy('sort', 'ASC')
->get();
return $this->success($data);
}
@@ -36,14 +37,14 @@ class KnowledgeController extends Controller
if (!$request->input('id')) {
if (!Knowledge::create($params)) {
return $this->fail([500,'创建失败']);
return $this->fail([500, '创建失败']);
}
} else {
try {
Knowledge::find($request->input('id'))->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'创建失败']);
return $this->fail([500, '创建失败']);
}
}
@@ -54,8 +55,8 @@ class KnowledgeController extends Controller
{
$request->validate([
'id' => 'required|numeric'
],[
'id.required' => '知识库ID不能为空'
], [
'id.required' => '知识库ID不能为空'
]);
$knowledge = Knowledge::find($request->input('id'));
if (!$knowledge) {
@@ -69,11 +70,17 @@ class KnowledgeController extends Controller
return $this->success(true);
}
public function sort(KnowledgeSort $request)
public function sort(Request $request)
{
$request->validate([
'ids' => 'required|array'
], [
'ids.required' => '参数有误',
'ids.array' => '参数有误'
]);
try {
DB::beginTransaction();
foreach ($request->input('knowledge_ids') as $k => $v) {
foreach ($request->input('ids') as $k => $v) {
$knowledge = Knowledge::find($v);
$knowledge->timestamps = false;
$knowledge->update(['sort' => $k + 1]);
@@ -90,15 +97,15 @@ class KnowledgeController extends Controller
{
$request->validate([
'id' => 'required|numeric'
],[
'id.required' => '知识库ID不能为空'
], [
'id.required' => '知识库ID不能为空'
]);
$knowledge = Knowledge::find($request->input('id'));
if (!$knowledge) {
return $this->fail([400202,'知识不存在']);
return $this->fail([400202, '知识不存在']);
}
if (!$knowledge->delete()) {
return $this->fail([500,'删除失败']);
return $this->fail([500, '删除失败']);
}
return $this->success(true);
@@ -0,0 +1,101 @@
<?php
namespace App\Http\Controllers\V2\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\NoticeSave;
use App\Models\Notice;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class NoticeController extends Controller
{
public function fetch(Request $request)
{
return $this->success(
Notice::orderBy('sort', 'ASC')
->orderBy('id', 'DESC')
->get()
);
}
public function save(NoticeSave $request)
{
$data = $request->only([
'title',
'content',
'img_url',
'tags',
'show',
'popup'
]);
if (!$request->input('id')) {
if (!Notice::create($data)) {
return $this->fail([500, '保存失败']);
}
} else {
try {
Notice::find($request->input('id'))->update($data);
} catch (\Exception $e) {
return $this->fail([500, '保存失败']);
}
}
return $this->success(true);
}
public function show(Request $request)
{
if (empty($request->input('id'))) {
return $this->fail([500, '公告ID不能为空']);
}
$notice = Notice::find($request->input('id'));
if (!$notice) {
return $this->fail([400202, '公告不存在']);
}
$notice->show = $notice->show ? 0 : 1;
if (!$notice->save()) {
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}
public function drop(Request $request)
{
if (empty($request->input('id'))) {
return $this->fail([422, '公告ID不能为空']);
}
$notice = Notice::find($request->input('id'));
if (!$notice) {
return $this->fail([400202, '公告不存在']);
}
if (!$notice->delete()) {
return $this->fail([500, '删除失败']);
}
return $this->success(true);
}
public function sort(Request $request)
{
$params = $request->validate([
'ids' => 'required|array'
]);
try {
DB::beginTransaction();
foreach ($params['ids'] as $k => $v) {
$notice = Notice::findOrFail($v);
$notice->update(['sort' => $k + 1]);
}
DB::commit();
return $this->success(true);
} catch (\Exception $e) {
DB::rollBack();
\Log::error($e);
return $this->fail([500, '排序保存失败']);
}
}
}
@@ -0,0 +1,246 @@
<?php
namespace App\Http\Controllers\V2\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\OrderAssign;
use App\Http\Requests\Admin\OrderUpdate;
use App\Models\Order;
use App\Models\Plan;
use App\Models\User;
use App\Services\OrderService;
use App\Services\PlanService;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Builder;
class OrderController extends Controller
{
public function detail(Request $request)
{
$order = Order::with(['user', 'plan', 'commission_log'])->find($request->input('id'));
if (!$order)
return $this->fail([400202, '订单不存在']);
if ($order->surplus_order_ids) {
$order['surplus_orders'] = Order::whereIn('id', $order->surplus_order_ids)->get();
}
$order['period'] = PlanService::getLegacyPeriod($order->period);
return $this->success($order);
}
public function fetch(Request $request)
{
$current = $request->input('current', 1);
$pageSize = $request->input('pageSize', 10);
$orderModel = Order::with('plan:id,name');
if ($request->boolean('is_commission')) {
$orderModel->whereNotNull('invite_user_id')
->whereNotIn('status', [0, 2])
->where('commission_balance', '>', 0);
}
$this->applyFiltersAndSorts($request, $orderModel);
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 $builder): void
{
$this->applyFilters($request, $builder);
$this->applySorting($request, $builder);
}
private function applyFilters(Request $request, Builder $builder): void
{
if (!$request->has('filter')) {
return;
}
collect($request->input('filter'))->each(function ($filter) use ($builder) {
$field = $filter['id'];
$value = $filter['value'];
$builder->where(function ($query) use ($field, $value) {
$this->buildFilterQuery($query, $field, $value);
});
});
}
private function buildFilterQuery(Builder $query, string $field, mixed $value): void
{
// Handle array values for 'in' operations
if (is_array($value)) {
$query->whereIn($field, $value);
return;
}
// Handle operator-based filtering
if (!is_string($value) || !str_contains($value, ':')) {
$query->where($field, 'like', "%{$value}%");
return;
}
[$operator, $filterValue] = explode(':', $value, 2);
// Convert numeric strings to appropriate type
if (is_numeric($filterValue)) {
$filterValue = strpos($filterValue, '.') !== false
? (float) $filterValue
: (int) $filterValue;
}
// Apply operator
$query->where($field, match (strtolower($operator)) {
'eq' => '=',
'gt' => '>',
'gte' => '>=',
'lt' => '<',
'lte' => '<=',
'like' => 'like',
'notlike' => 'not like',
'null' => static fn($q) => $q->whereNull($queryField),
'notnull' => static fn($q) => $q->whereNotNull($queryField),
default => 'like'
}, match (strtolower($operator)) {
'like', 'notlike' => "%{$filterValue}%",
'null', 'notnull' => null,
default => $filterValue
});
}
private function applySorting(Request $request, Builder $builder): void
{
if (!$request->has('sort')) {
return;
}
collect($request->input('sort'))->each(function ($sort) use ($builder) {
$field = $sort['id'];
$direction = $sort['desc'] ? 'DESC' : 'ASC';
$builder->orderBy($field, $direction);
});
}
public function paid(Request $request)
{
$order = Order::where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
return $this->fail([400202, '订单不存在']);
}
if ($order->status !== 0)
return $this->fail([400, '只能对待支付的订单进行操作']);
$orderService = new OrderService($order);
if (!$orderService->paid('manual_operation')) {
return $this->fail([500, '更新失败']);
}
return $this->success(true);
}
public function cancel(Request $request)
{
$order = Order::where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
return $this->fail([400202, '订单不存在']);
}
if ($order->status !== 0)
return $this->fail([400, '只能对待支付的订单进行操作']);
$orderService = new OrderService($order);
if (!$orderService->cancel()) {
return $this->fail([400, '更新失败']);
}
return $this->success(true);
}
public function update(OrderUpdate $request)
{
$params = $request->only([
'commission_status'
]);
$order = Order::where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
return $this->fail([400202, '订单不存在']);
}
try {
$order->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '更新失败']);
}
return $this->success(true);
}
public function assign(OrderAssign $request)
{
$plan = Plan::find($request->input('plan_id'));
$user = User::where('email', $request->input('email'))->first();
if (!$user) {
return $this->fail([400202, '该用户不存在']);
}
if (!$plan) {
return $this->fail([400202, '该订阅不存在']);
}
$userService = new UserService();
if ($userService->isNotCompleteOrderByUserId($user->id)) {
return $this->fail([400, '该用户还有待支付的订单,无法分配']);
}
try {
DB::beginTransaction();
$order = new Order();
$orderService = new OrderService($order);
$order->user_id = $user->id;
$order->plan_id = $plan->id;
$order->period = PlanService::getPeriodKey($request->input('period'));
$order->trade_no = Helper::guid();
$order->total_amount = $request->input('total_amount');
if (PlanService::getPeriodKey($order->period) === Plan::PERIOD_RESET_TRAFFIC) {
$order->type = Order::TYPE_RESET_TRAFFIC;
} else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id) {
$order->type = Order::TYPE_UPGRADE;
} else if ($user->expired_at > time() && $order->plan_id == $user->plan_id) {
$order->type = Order::TYPE_RENEWAL;
} else {
$order->type = Order::TYPE_NEW_PURCHASE;
}
$orderService->setInvite($user);
if (!$order->save()) {
DB::rollBack();
return $this->fail([500, '订单创建失败']);
}
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
return $this->success($order->trade_no);
}
}
@@ -1,6 +1,6 @@
<?php
namespace App\Http\Controllers\V1\Admin;
namespace App\Http\Controllers\V2\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
@@ -38,7 +38,7 @@ class PaymentController extends Controller
public function getPaymentForm(Request $request)
{
$paymentService = new PaymentService($request->input('payment'), $request->input('id'));
return $this->success($paymentService->form());
return $this->success(collect($paymentService->form())->values());
}
public function show(Request $request)
@@ -1,16 +1,11 @@
<?php
namespace App\Http\Controllers\V1\Admin;
namespace App\Http\Controllers\V2\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\PlanSave;
use App\Http\Requests\Admin\PlanSort;
use App\Http\Requests\Admin\PlanUpdate;
use App\Models\Order;
use App\Models\Plan;
use App\Models\User;
use App\Services\PlanService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@@ -18,24 +13,34 @@ class PlanController extends Controller
{
public function fetch(Request $request)
{
$counts = PlanService::countActiveUsers();
$plans = Plan::orderBy('sort', 'ASC')->get();
foreach ($plans as $k => $v) {
$plans[$k]->count = 0;
foreach ($counts as $kk => $vv) {
if ($plans[$k]->id === $counts[$kk]->plan_id) $plans[$k]->count = $counts[$kk]->count;
}
}
$plans = Plan::orderBy('sort', 'ASC')
->with([
'group:id,name'
])
->withCount('users')
->get();
return $this->success($plans);
}
public function save(PlanSave $request)
public function save(Request $request)
{
$params = $request->validated();
$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',
]);
if ($request->input('id')) {
$plan = Plan::find($request->input('id'));
if (!$plan) {
return $this->fail([400202 ,'该订阅不存在']);
return $this->fail([400202, '该订阅不存在']);
}
DB::beginTransaction();
// update user group id and transfer
@@ -44,7 +49,8 @@ class PlanController extends Controller
User::where('plan_id', $plan->id)->update([
'group_id' => $params['group_id'],
'transfer_enable' => $params['transfer_enable'] * 1073741824,
'speed_limit' => $params['speed_limit']
'speed_limit' => $params['speed_limit'],
'device_limit' => $params['device_limit'],
]);
}
$plan->update($params);
@@ -53,11 +59,11 @@ class PlanController extends Controller
} catch (\Exception $e) {
DB::rollBack();
\Log::error($e);
return $this->fail([500 ,'保存失败']);
return $this->fail([500, '保存失败']);
}
}
if (!Plan::create($params)) {
return $this->fail([500 ,'创建失败']);
return $this->fail([500, '创建失败']);
}
return $this->success(true);
}
@@ -65,57 +71,61 @@ class PlanController extends Controller
public function drop(Request $request)
{
if (Order::where('plan_id', $request->input('id'))->first()) {
return $this->fail([400201 ,'该订阅下存在订单无法删除']);
return $this->fail([400201, '该订阅下存在订单无法删除']);
}
if (User::where('plan_id', $request->input('id'))->first()) {
return $this->fail([400201 ,'该订阅下存在用户无法删除']);
return $this->fail([400201, '该订阅下存在用户无法删除']);
}
if ($request->input('id')) {
$plan = Plan::find($request->input('id'));
if (!$plan) {
return $this->fail([400202 ,'该订阅不存在']);
return $this->fail([400202, '该订阅不存在']);
}
}
return $this->success($plan->delete());
}
public function update(PlanUpdate $request)
public function update(Request $request)
{
$updateData = $request->only([
'show',
'renew'
'renew',
'sell'
]);
$plan = Plan::find($request->input('id'));
if (!$plan) {
return $this->fail([400202 ,'该订阅不存在']);
return $this->fail([400202, '该订阅不存在']);
}
try {
$plan->update($updateData);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500 ,'保存失败']);
return $this->fail([500, '保存失败']);
}
return $this->success();
return $this->success(true);
}
public function sort(PlanSort $request)
public function sort(Request $request)
{
try{
$params = $request->validate([
'ids' => 'required|array'
]);
try {
DB::beginTransaction();
foreach ($request->input('plan_ids') as $k => $v) {
foreach ($params['ids'] as $k => $v) {
if (!Plan::find($v)->update(['sort' => $k + 1])) {
throw new \Exception();
}
}
DB::commit();
}catch (\Exception $e){
} catch (\Exception $e) {
DB::rollBack();
\Log::error($e);
return $this->fail([500 ,'保存失败']);
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}
@@ -0,0 +1,195 @@
<?php
namespace App\Http\Controllers\V2\Admin;
use App\Http\Controllers\Controller;
use App\Models\Plugin;
use App\Services\Plugin\PluginManager;
use App\Services\Plugin\PluginConfigService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
class PluginController extends Controller
{
protected PluginManager $pluginManager;
protected PluginConfigService $configService;
public function __construct(
PluginManager $pluginManager,
PluginConfigService $configService
) {
$this->pluginManager = $pluginManager;
$this->configService = $configService;
}
/**
* 获取插件列表
*/
public function index()
{
$installedPlugins = Plugin::get()
->keyBy('code')
->toArray();
$pluginPath = base_path('plugins');
$plugins = [];
if (File::exists($pluginPath)) {
$directories = File::directories($pluginPath);
foreach ($directories as $directory) {
$pluginName = basename($directory);
$configFile = $directory . '/config.json';
if (File::exists($configFile)) {
$config = json_decode(File::get($configFile), true);
$installed = isset($installedPlugins[$pluginName]);
// 使用配置服务获取配置
$pluginConfig = $installed ? $this->configService->getConfig($pluginName) : ($config['config'] ?? []);
$plugins[] = [
'code' => $config['code'],
'name' => $config['name'],
'version' => $config['version'],
'description' => $config['description'],
'author' => $config['author'],
'is_installed' => $installed,
'is_enabled' => $installed ? $installedPlugins[$pluginName]['is_enabled'] : false,
'config' => $pluginConfig,
];
}
}
}
return response()->json([
'data' => $plugins
]);
}
/**
* 安装插件
*/
public function install(Request $request)
{
$request->validate([
'code' => 'required|string'
]);
try {
$this->pluginManager->install($request->input('code'));
return response()->json([
'message' => '插件安装成功'
]);
} catch (\Exception $e) {
return response()->json([
'message' => '插件安装失败:' . $e->getMessage()
], 400);
}
}
/**
* 卸载插件
*/
public function uninstall(Request $request)
{
$request->validate([
'code' => 'required|string'
]);
try {
$this->pluginManager->uninstall($request->input('code'));
return response()->json([
'message' => '插件卸载成功'
]);
} catch (\Exception $e) {
return response()->json([
'message' => '插件卸载失败:' . $e->getMessage()
], 400);
}
}
/**
* 启用插件
*/
public function enable(Request $request)
{
$request->validate([
'code' => 'required|string'
]);
try {
$this->pluginManager->enable($request->input('code'));
return response()->json([
'message' => '插件启用成功'
]);
} catch (\Exception $e) {
return response()->json([
'message' => '插件启用失败:' . $e->getMessage()
], 400);
}
}
/**
* 禁用插件
*/
public function disable(Request $request)
{
$request->validate([
'code' => 'required|string'
]);
try {
$this->pluginManager->disable($request->input('code'));
return response()->json([
'message' => '插件禁用成功'
]);
} catch (\Exception $e) {
return response()->json([
'message' => '插件禁用失败:' . $e->getMessage()
], 400);
}
}
/**
* 获取插件配置
*/
public function getConfig(Request $request)
{
$request->validate([
'code' => 'required|string'
]);
try {
$config = $this->configService->getConfig($request->input('code'));
return response()->json([
'data' => $config
]);
} catch (\Exception $e) {
return response()->json([
'message' => '获取配置失败:' . $e->getMessage()
], 400);
}
}
/**
* 更新插件配置
*/
public function updateConfig(Request $request)
{
$request->validate([
'code' => 'required|string',
'config' => 'required|array'
]);
try {
$this->configService->updateConfig(
$request->input('code'),
$request->input('config')
);
return response()->json([
'message' => '配置更新成功'
]);
} catch (\Exception $e) {
return response()->json([
'message' => '配置更新失败:' . $e->getMessage()
], 400);
}
}
}
@@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers\V2\Admin\Server;
use App\Http\Controllers\Controller;
use App\Models\Plan;
use App\Models\Server;
use App\Models\ServerGroup;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class GroupController extends Controller
{
public function fetch(Request $request): JsonResponse
{
$serverGroups = ServerGroup::query()
->orderByDesc('id')
->withCount('users')
->get()
->transform(function ($group) {
$group->server_count = $group->servers()->count();
return $group;
});
return $this->success($serverGroups);
}
public function save(Request $request)
{
if (empty($request->input('name'))) {
return $this->fail([422, '组名不能为空']);
}
if ($request->input('id')) {
$serverGroup = ServerGroup::find($request->input('id'));
} else {
$serverGroup = new ServerGroup();
}
$serverGroup->name = $request->input('name');
return $this->success($serverGroup->save());
}
public function drop(Request $request)
{
$groupId = $request->input('id');
$serverGroup = ServerGroup::find($groupId);
if (!$serverGroup) {
return $this->fail([400202, '组不存在']);
}
if (Server::whereJsonContains('group_ids', $groupId)->exists()) {
return $this->fail([400, '该组已被节点所使用,无法删除']);
}
if (Plan::where('group_id', $groupId)->exists()) {
return $this->fail([400, '该组已被订阅所使用,无法删除']);
}
if (User::where('group_id', $groupId)->exists()) {
return $this->fail([400, '该组已被用户所使用,无法删除']);
}
return $this->success($serverGroup->delete());
}
}
@@ -0,0 +1,125 @@
<?php
namespace App\Http\Controllers\V2\Admin\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ServerSave;
use App\Models\Server;
use App\Models\ServerGroup;
use App\Services\ServerService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ManageController extends Controller
{
public function getNodes(Request $request)
{
$servers = collect(ServerService::getAllServers())->map(function ($item) {
$item['groups'] = ServerGroup::whereIn('id', $item['group_ids'])->get(['name', 'id']);
$item['parent'] = $item->parent;
return $item;
});
return $this->success($servers);
}
public function sort(Request $request)
{
ini_set('post_max_size', '1m');
$params = $request->validate([
'*.id' => 'numeric',
'*.order' => 'numeric'
]);
try {
DB::beginTransaction();
collect($params)->each(function ($item) {
if (isset($item['id']) && isset($item['order'])) {
Server::where('id', $item['id'])->update(['sort' => $item['order']]);
}
});
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
\Log::error($e);
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}
public function save(ServerSave $request)
{
$params = $request->validated();
if ($request->input('id')) {
$server = Server::find($request->input('id'));
if (!$server) {
return $this->fail([400202, '服务器不存在']);
}
try {
$server->update($params);
return $this->success(true);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '保存失败']);
}
}
try {
Server::create($params);
return $this->success(true);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '创建失败']);
}
}
public function update(Request $request)
{
$request->validate([
'id' => 'required|integer',
'show' => 'integer',
]);
if (Server::where('id', $request->id)->update(['show' => $request->show]) === false) {
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}
/**
* 删除
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function drop(Request $request)
{
$request->validate([
'id' => 'required|integer',
]);
if (Server::where('id', $request->id)->delete() === false) {
return $this->fail([500, '删除失败']);
}
return $this->success(true);
}
/**
* 复制节点
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function copy(Request $request)
{
$server = Server::find($request->input('id'));
$server->show = 0;
$server->code = null;
if (!$server) {
return $this->fail([400202, '服务器不存在']);
}
Server::create($server->toArray());
return $this->success(true);
}
}
@@ -1,6 +1,6 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
namespace App\Http\Controllers\V2\Admin\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
+480 -63
View File
@@ -5,9 +5,7 @@ namespace App\Http\Controllers\V2\Admin;
use App\Http\Controllers\Controller;
use App\Models\CommissionLog;
use App\Models\Order;
use App\Models\ServerShadowsocks;
use App\Models\ServerTrojan;
use App\Models\ServerVmess;
use App\Models\Server;
use App\Models\Stat;
use App\Models\StatServer;
use App\Models\StatUser;
@@ -15,78 +13,497 @@ use App\Models\Ticket;
use App\Models\User;
use App\Services\StatisticalService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class StatController extends Controller
{
public function override(Request $request)
private $service;
public function __construct(StatisticalService $service)
{
$params = $request->validate([
'start_at' => '',
'end_at' => ''
$this->service = $service;
}
public function getOverride(Request $request)
{
// 获取在线节点数
$onlineNodes = Server::all()->filter(function ($server) {
$server->loadServerStatus();
return $server->is_online;
})->count();
// 获取在线设备数和在线用户数
$onlineDevices = User::where('t', '>=', time() - 600)
->sum('online_count');
$onlineUsers = User::where('t', '>=', time() - 600)
->count();
// 获取今日流量统计
$todayStart = strtotime('today');
$todayTraffic = StatServer::where('record_at', '>=', $todayStart)
->where('record_at', '<', time())
->selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
->first();
// 获取本月流量统计
$monthStart = strtotime(date('Y-m-1'));
$monthTraffic = StatServer::where('record_at', '>=', $monthStart)
->where('record_at', '<', time())
->selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
->first();
// 获取总流量统计
$totalTraffic = StatServer::selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
->first();
return [
'data' => [
'month_income' => Order::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
->whereNotIn('status', [0, 2])
->sum('total_amount'),
'month_register_total' => User::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
->count(),
'ticket_pending_total' => Ticket::where('status', 0)
->count(),
'commission_pending_total' => Order::where('commission_status', 0)
->where('invite_user_id', '!=', NULL)
->whereNotIn('status', [0, 2])
->where('commission_balance', '>', 0)
->count(),
'day_income' => Order::where('created_at', '>=', strtotime(date('Y-m-d')))
->where('created_at', '<', time())
->whereNotIn('status', [0, 2])
->sum('total_amount'),
'last_month_income' => Order::where('created_at', '>=', strtotime('-1 month', strtotime(date('Y-m-1'))))
->where('created_at', '<', strtotime(date('Y-m-1')))
->whereNotIn('status', [0, 2])
->sum('total_amount'),
'commission_month_payout' => CommissionLog::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
->sum('get_amount'),
'commission_last_month_payout' => CommissionLog::where('created_at', '>=', strtotime('-1 month', strtotime(date('Y-m-1'))))
->where('created_at', '<', strtotime(date('Y-m-1')))
->sum('get_amount'),
// 新增统计数据
'online_nodes' => $onlineNodes,
'online_devices' => $onlineDevices,
'online_users' => $onlineUsers,
'today_traffic' => [
'upload' => $todayTraffic->upload ?? 0,
'download' => $todayTraffic->download ?? 0,
'total' => $todayTraffic->total ?? 0
],
'month_traffic' => [
'upload' => $monthTraffic->upload ?? 0,
'download' => $monthTraffic->download ?? 0,
'total' => $monthTraffic->total ?? 0
],
'total_traffic' => [
'upload' => $totalTraffic->upload ?? 0,
'download' => $totalTraffic->download ?? 0,
'total' => $totalTraffic->total ?? 0
]
]
];
}
/**
* Get order statistics with filtering and pagination
*
* @param Request $request
* @return array
*/
public function getOrder(Request $request)
{
$request->validate([
'start_date' => 'nullable|date_format:Y-m-d',
'end_date' => 'nullable|date_format:Y-m-d',
'type' => 'nullable|in:paid_total,paid_count,commission_total,commission_count',
]);
if (isset($params['start_at']) && isset($params['end_at'])) {
$stats = Stat::where('record_at', '>=', $params['start_at'])
->where('record_at', '<', $params['end_at'])
$query = Stat::where('record_type', 'd');
// Apply date filters
if ($request->input('start_date')) {
$query->where('record_at', '>=', strtotime($request->input('start_date')));
}
if ($request->input('end_date')) {
$query->where('record_at', '<=', strtotime($request->input('end_date') . ' 23:59:59'));
}
$statistics = $query->orderBy('record_at', 'DESC')
->get();
$summary = [
'paid_total' => 0,
'paid_count' => 0,
'commission_total' => 0,
'commission_count' => 0,
'start_date' => $request->input('start_date', date('Y-m-d', $statistics->last()?->record_at)),
'end_date' => $request->input('end_date', date('Y-m-d', $statistics->first()?->record_at)),
'avg_paid_amount' => 0,
'avg_commission_amount' => 0
];
$dailyStats = [];
foreach ($statistics as $statistic) {
$date = date('Y-m-d', $statistic['record_at']);
// Update summary
$summary['paid_total'] += $statistic['paid_total'];
$summary['paid_count'] += $statistic['paid_count'];
$summary['commission_total'] += $statistic['commission_total'];
$summary['commission_count'] += $statistic['commission_count'];
// Calculate daily stats
$dailyData = [
'date' => $date,
'paid_total' => $statistic['paid_total'],
'paid_count' => $statistic['paid_count'],
'commission_total' => $statistic['commission_total'],
'commission_count' => $statistic['commission_count'],
'avg_order_amount' => $statistic['paid_count'] > 0 ? round($statistic['paid_total'] / $statistic['paid_count'], 2) : 0,
'avg_commission_amount' => $statistic['commission_count'] > 0 ? round($statistic['commission_total'] / $statistic['commission_count'], 2) : 0
];
if ($request->input('type')) {
$dailyStats[] = [
'date' => $date,
'value' => $statistic[$request->input('type')],
'type' => $this->getTypeLabel($request->input('type'))
];
} else {
$dailyStats[] = $dailyData;
}
}
// Calculate averages for summary
if ($summary['paid_count'] > 0) {
$summary['avg_paid_amount'] = round($summary['paid_total'] / $summary['paid_count'], 2);
}
if ($summary['commission_count'] > 0) {
$summary['avg_commission_amount'] = round($summary['commission_total'] / $summary['commission_count'], 2);
}
// Add percentage calculations to summary
$summary['commission_rate'] = $summary['paid_total'] > 0
? round(($summary['commission_total'] / $summary['paid_total']) * 100, 2)
: 0;
return [
'code' => 0,
'message' => 'success',
'data' => [
'list' => array_reverse($dailyStats),
'summary' => $summary,
]
];
}
/**
* Get human readable label for statistic type
*
* @param string $type
* @return string
*/
private function getTypeLabel(string $type): string
{
return match ($type) {
'paid_total' => '收款金额',
'paid_count' => '收款笔数',
'commission_total' => '佣金金额(已发放)',
'commission_count' => '佣金笔数(已发放)',
default => $type
};
}
// 获取当日实时流量排行
public function getServerLastRank()
{
$data = $this->service->getServerRank();
return $this->success(data: $data);
}
// 获取昨日节点流量排行
public function getServerYesterdayRank()
{
$data = $this->service->getServerRank('yesterday');
return $this->success($data);
}
public function getStatUser(Request $request)
{
$request->validate([
'user_id' => 'required|integer'
]);
$pageSize = $request->input('pageSize', 10);
$records = StatUser::orderBy('record_at', 'DESC')
->where('user_id', $request->input('user_id'))
->paginate($pageSize);
$data = $records->items();
return [
'data' => $data,
'total' => $records->total(),
];
}
public function getStatRecord(Request $request)
{
return [
'data' => $this->service->getStatRecord($request->input('type'))
];
}
/**
* Get comprehensive statistics data including income, users, and growth rates
*/
public function getStats()
{
$currentMonthStart = strtotime(date('Y-m-01'));
$lastMonthStart = strtotime('-1 month', $currentMonthStart);
$twoMonthsAgoStart = strtotime('-2 month', $currentMonthStart);
// Today's start timestamp
$todayStart = strtotime('today');
$yesterdayStart = strtotime('-1 day', $todayStart);
// 获取在线节点数
$onlineNodes = Server::all()->filter(function ($server) {
$server->loadServerStatus();
return $server->is_online;
})->count();
// 获取在线设备数和在线用户数
$onlineDevices = User::where('t', '>=', time() - 600)
->sum('online_count');
$onlineUsers = User::where('t', '>=', time() - 600)
->count();
// 获取今日流量统计
$todayTraffic = StatServer::where('record_at', '>=', $todayStart)
->where('record_at', '<', time())
->selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
->first();
// 获取本月流量统计
$monthTraffic = StatServer::where('record_at', '>=', $currentMonthStart)
->where('record_at', '<', time())
->selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
->first();
// 获取总流量统计
$totalTraffic = StatServer::selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
->first();
// Today's income
$todayIncome = Order::where('created_at', '>=', $todayStart)
->where('created_at', '<', time())
->whereNotIn('status', [0, 2])
->sum('total_amount');
// Yesterday's income for day growth calculation
$yesterdayIncome = Order::where('created_at', '>=', $yesterdayStart)
->where('created_at', '<', $todayStart)
->whereNotIn('status', [0, 2])
->sum('total_amount');
// Current month income
$currentMonthIncome = Order::where('created_at', '>=', $currentMonthStart)
->where('created_at', '<', time())
->whereNotIn('status', [0, 2])
->sum('total_amount');
// Last month income
$lastMonthIncome = Order::where('created_at', '>=', $lastMonthStart)
->where('created_at', '<', $currentMonthStart)
->whereNotIn('status', [0, 2])
->sum('total_amount');
// Last month commission payout
$lastMonthCommissionPayout = CommissionLog::where('created_at', '>=', $lastMonthStart)
->where('created_at', '<', $currentMonthStart)
->sum('get_amount');
// Current month commission payout
$currentMonthCommissionPayout = CommissionLog::where('created_at', '>=', $currentMonthStart)
->where('created_at', '<', time())
->sum('get_amount');
// Current month new users
$currentMonthNewUsers = User::where('created_at', '>=', $currentMonthStart)
->where('created_at', '<', time())
->count();
// Total users
$totalUsers = User::count();
// Active users (users with valid subscription)
$activeUsers = User::where(function ($query) {
$query->where('expired_at', '>=', time())
->orWhere('expired_at', NULL);
})->count();
// Previous month income for growth calculation
$twoMonthsAgoIncome = Order::where('created_at', '>=', $twoMonthsAgoStart)
->where('created_at', '<', $lastMonthStart)
->whereNotIn('status', [0, 2])
->sum('total_amount');
// Previous month commission for growth calculation
$twoMonthsAgoCommission = CommissionLog::where('created_at', '>=', $twoMonthsAgoStart)
->where('created_at', '<', $lastMonthStart)
->sum('get_amount');
// Previous month users for growth calculation
$lastMonthNewUsers = User::where('created_at', '>=', $lastMonthStart)
->where('created_at', '<', $currentMonthStart)
->count();
// Calculate growth rates
$monthIncomeGrowth = $lastMonthIncome > 0 ? round(($currentMonthIncome - $lastMonthIncome) / $lastMonthIncome * 100, 1) : 0;
$lastMonthIncomeGrowth = $twoMonthsAgoIncome > 0 ? round(($lastMonthIncome - $twoMonthsAgoIncome) / $twoMonthsAgoIncome * 100, 1) : 0;
$commissionGrowth = $twoMonthsAgoCommission > 0 ? round(($lastMonthCommissionPayout - $twoMonthsAgoCommission) / $twoMonthsAgoCommission * 100, 1) : 0;
$userGrowth = $lastMonthNewUsers > 0 ? round(($currentMonthNewUsers - $lastMonthNewUsers) / $lastMonthNewUsers * 100, 1) : 0;
$dayIncomeGrowth = $yesterdayIncome > 0 ? round(($todayIncome - $yesterdayIncome) / $yesterdayIncome * 100, 1) : 0;
// 获取待处理工单和佣金数据
$ticketPendingTotal = Ticket::where('status', 0)->count();
$commissionPendingTotal = Order::where('commission_status', 0)
->where('invite_user_id', '!=', NULL)
->whereIn('status', [Order::STATUS_COMPLETED])
->where('commission_balance', '>', 0)
->count();
return [
'data' => [
// 收入相关
'todayIncome' => $todayIncome,
'dayIncomeGrowth' => $dayIncomeGrowth,
'currentMonthIncome' => $currentMonthIncome,
'lastMonthIncome' => $lastMonthIncome,
'monthIncomeGrowth' => $monthIncomeGrowth,
'lastMonthIncomeGrowth' => $lastMonthIncomeGrowth,
// 佣金相关
'currentMonthCommissionPayout' => $currentMonthCommissionPayout,
'lastMonthCommissionPayout' => $lastMonthCommissionPayout,
'commissionGrowth' => $commissionGrowth,
'commissionPendingTotal' => $commissionPendingTotal,
// 用户相关
'currentMonthNewUsers' => $currentMonthNewUsers,
'totalUsers' => $totalUsers,
'activeUsers' => $activeUsers,
'userGrowth' => $userGrowth,
'onlineUsers' => $onlineUsers,
'onlineDevices' => $onlineDevices,
// 工单相关
'ticketPendingTotal' => $ticketPendingTotal,
// 节点相关
'onlineNodes' => $onlineNodes,
// 流量统计
'todayTraffic' => [
'upload' => $todayTraffic->upload ?? 0,
'download' => $todayTraffic->download ?? 0,
'total' => $todayTraffic->total ?? 0
],
'monthTraffic' => [
'upload' => $monthTraffic->upload ?? 0,
'download' => $monthTraffic->download ?? 0,
'total' => $monthTraffic->total ?? 0
],
'totalTraffic' => [
'upload' => $totalTraffic->upload ?? 0,
'download' => $totalTraffic->download ?? 0,
'total' => $totalTraffic->total ?? 0
]
]
];
}
/**
* Get traffic ranking data for nodes or users
*
* @param Request $request
* @return array
*/
public function getTrafficRank(Request $request)
{
$request->validate([
'type' => 'required|in:node,user',
'start_time' => 'nullable|integer|min:1000000000|max:9999999999',
'end_time' => 'nullable|integer|min:1000000000|max:9999999999'
]);
$type = $request->input('type');
$startDate = $request->input('start_time', strtotime('-7 days'));
$endDate = $request->input('end_time', time());
$previousStartDate = $startDate - ($endDate - $startDate);
$previousEndDate = $startDate;
if ($type === 'node') {
// Get node traffic data
$currentData = StatServer::selectRaw('server_id as id, SUM(u + d) as value')
->where('record_at', '>=', $startDate)
->where('record_at', '<=', $endDate)
->groupBy('server_id')
->orderBy('value', 'DESC')
->limit(10)
->get();
// Get previous period data for comparison
$previousData = StatServer::selectRaw('server_id as id, SUM(u + d) as value')
->where('record_at', '>=', $previousStartDate)
->where('record_at', '<', $previousEndDate)
->whereIn('server_id', $currentData->pluck('id'))
->groupBy('server_id')
->get()
->makeHidden(['record_at', 'created_at', 'updated_at', 'id', 'record_type'])
->toArray();
->keyBy('id');
} else {
$statisticalService = new StatisticalService();
return [
'data' => $statisticalService->generateStatData()
// Get user traffic data
$currentData = StatUser::selectRaw('user_id as id, SUM(u + d) as value')
->where('record_at', '>=', $startDate)
->where('record_at', '<=', $endDate)
->groupBy('user_id')
->orderBy('value', 'DESC')
->limit(10)
->get();
// Get previous period data for comparison
$previousData = StatUser::selectRaw('user_id as id, SUM(u + d) as value')
->where('record_at', '>=', $previousStartDate)
->where('record_at', '<', $previousEndDate)
->whereIn('user_id', $currentData->pluck('id'))
->groupBy('user_id')
->get()
->keyBy('id');
}
$result = [];
foreach ($currentData as $data) {
$previousValue = isset($previousData[$data->id]) ? $previousData[$data->id]->value : 0;
$change = $previousValue > 0 ? round(($data->value - $previousValue) / $previousValue * 100, 1) : 0;
$name = $type === 'node'
? optional(Server::find($data->id))->name ?? "Node {$data->id}"
: optional(User::find($data->id))->email ?? "User {$data->id}";
$result[] = [
'id' => (string) $data->id,
'name' => $name,
'value' => $data->value, // Convert to GB
'previousValue' => $previousValue, // Convert to GB
'change' => $change,
'timestamp' => date('c', $endDate)
];
}
$stats = array_reduce($stats, function($carry, $item) {
foreach($item as $key => $value) {
if(isset($carry[$key]) && $carry[$key]) {
$carry[$key] += $value;
} else {
$carry[$key] = $value;
}
}
return $carry;
}, []);
return [
'data' => $stats
];
}
public function record(Request $request)
{
$request->validate([
'type' => 'required|in:paid_total,commission_total,register_count',
'start_at' => '',
'end_at' => ''
]);
$statisticalService = new StatisticalService();
$statisticalService->setStartAt($request->input('start_at'));
$statisticalService->setEndAt($request->input('end_at'));
return [
'data' => $statisticalService->getStatRecord($request->input('type'))
];
}
public function ranking(Request $request)
{
$request->validate([
'type' => 'required|in:server_traffic_rank,user_consumption_rank,invite_rank',
'start_at' => '',
'end_at' => '',
'limit' => 'nullable|integer'
]);
$statisticalService = new StatisticalService();
$statisticalService->setStartAt($request->input('start_at'));
$statisticalService->setEndAt($request->input('end_at'));
return [
'data' => $statisticalService->getRanking($request->input('type'), $request->input('limit') ?? 20)
'timestamp' => date('c'),
'data' => $result
];
}
}
@@ -1,14 +1,12 @@
<?php
namespace App\Http\Controllers\V1\Admin;
namespace App\Http\Controllers\V2\Admin;
use App\Http\Controllers\Controller;
use App\Models\Log as LogModel;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Laravel\Horizon\Contracts\JobRepository;
use Laravel\Horizon\Contracts\MasterSupervisorRepository;
use Laravel\Horizon\Contracts\MetricsRepository;
@@ -0,0 +1,149 @@
<?php
namespace App\Http\Controllers\V2\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Services\ThemeService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
class ThemeController extends Controller
{
private $themeService;
public function __construct(ThemeService $themeService)
{
$this->themeService = $themeService;
}
/**
* 上传新主题
*
* @throws ApiException
*/
public function upload(Request $request)
{
$request->validate([
'file' => [
'required',
'file',
'mimes:zip',
'max:10240', // 最大10MB
]
], [
'file.required' => '请选择主题包文件',
'file.file' => '无效的文件类型',
'file.mimes' => '主题包必须是zip格式',
'file.max' => '主题包大小不能超过10MB'
]);
try {
// 检查上传目录权限
$uploadPath = storage_path('tmp');
if (!File::exists($uploadPath)) {
File::makeDirectory($uploadPath, 0755, true);
}
if (!is_writable($uploadPath)) {
throw new ApiException('上传目录无写入权限');
}
// 检查主题目录权限
$themePath = base_path('theme');
if (!is_writable($themePath)) {
throw new ApiException('主题目录无写入权限');
}
$file = $request->file('file');
// 检查文件MIME类型
$mimeType = $file->getMimeType();
if (!in_array($mimeType, ['application/zip', 'application/x-zip-compressed'])) {
throw new ApiException('无效的文件类型,仅支持ZIP格式');
}
// 检查文件名安全性
$originalName = $file->getClientOriginalName();
if (!preg_match('/^[a-zA-Z0-9\-\_\.]+\.zip$/', $originalName)) {
throw new ApiException('主题包文件名只能包含字母、数字、下划线、中划线和点');
}
$this->themeService->upload($file);
return $this->success(true);
} catch (ApiException $e) {
throw $e;
} catch (\Exception $e) {
\Log::error('Theme upload failed', [
'error' => $e->getMessage(),
'file' => $request->file('file')?->getClientOriginalName()
]);
throw new ApiException('主题上传失败:' . $e->getMessage());
}
}
/**
* 删除主题
*/
public function delete(Request $request)
{
$payload = $request->validate([
'name' => 'required'
]);
$this->themeService->delete($payload['name']);
return $this->success(true);
}
/**
* 获取所有主题和其配置列
*
* @return \Illuminate\Http\JsonResponse
*/
public function getThemes()
{
$data = [
'themes' => $this->themeService->getList(),
'active' => admin_setting('frontend_theme', 'Xboard')
];
return $this->success($data);
}
/**
* 切换主题
*/
public function switchTheme(Request $request)
{
$payload = $request->validate([
'name' => 'required'
]);
$this->themeService->switch($payload['name']);
return $this->success(true);
}
/**
* 获取主题配置
*/
public function getThemeConfig(Request $request)
{
$payload = $request->validate([
'name' => 'required'
]);
$data = $this->themeService->getConfig($payload['name']);
return $this->success($data);
}
/**
* 保存主题配置
*/
public function saveThemeConfig(Request $request)
{
$payload = $request->validate([
'name' => 'required',
'config' => 'required'
]);
$this->themeService->updateConfig($payload['name'], $payload['config']);
$config = $this->themeService->getConfig($payload['name']);
return $this->success($config);
}
}
@@ -0,0 +1,132 @@
<?php
namespace App\Http\Controllers\V2\Admin;
use App\Http\Controllers\Controller;
use App\Models\Ticket;
use App\Services\TicketService;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
class TicketController extends Controller
{
private function applyFiltersAndSorts(Request $request, $builder)
{
if ($request->has('filter')) {
collect($request->input('filter'))->each(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}%");
}
});
});
}
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);
});
}
}
public function fetch(Request $request)
{
if ($request->input('id')) {
return $this->fetchTicketById($request);
} else {
return $this->fetchTickets($request);
}
}
/**
* Summary of fetchTicketById
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
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);
}
/**
* Summary of fetchTickets
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response
*/
private function fetchTickets(Request $request)
{
$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'));
});
});
$this->applyFiltersAndSorts($request, $ticketModel);
return response()->json($ticketModel
->latest('updated_at')
->paginate(
perPage: $request->integer('pageSize', 10),
page: $request->integer('current', 1)
));
}
public function reply(Request $request)
{
$request->validate([
'id' => 'required|numeric',
'message' => 'required|string'
], [
'id.required' => '工单ID不能为空',
'message.required' => '消息不能为空'
]);
$ticketService = new TicketService();
$ticketService->replyByAdmin(
$request->input('id'),
$request->input('message'),
$request->user()->id
);
return $this->success(true);
}
public function close(Request $request)
{
$request->validate([
'id' => 'required|numeric'
], [
'id.required' => '工单ID不能为空'
]);
try {
$ticket = Ticket::findOrFail($request->input('id'));
$ticket->status = Ticket::STATUS_CLOSED;
$ticket->save();
return $this->success(true);
} catch (ModelNotFoundException $e) {
return $this->fail([400202, '工单不存在']);
} catch (\Exception $e) {
return $this->fail([500101, '关闭失败']);
}
}
}
@@ -1,10 +1,8 @@
<?php
namespace App\Http\Controllers\V1\Admin;
namespace App\Http\Controllers\V2\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\UserFetch;
use App\Http\Requests\Admin\UserGenerate;
use App\Http\Requests\Admin\UserSendMail;
use App\Http\Requests\Admin\UserUpdate;
@@ -13,6 +11,7 @@ use App\Models\Plan;
use App\Models\User;
use App\Services\AuthService;
use App\Utils\Helper;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@@ -21,71 +20,173 @@ class UserController extends Controller
public function resetSecret(Request $request)
{
$user = User::find($request->input('id'));
if (!$user) return $this->fail([400202,'用户不存在']);
if (!$user)
return $this->fail([400202, '用户不存在']);
$user->token = Helper::guid();
$user->uuid = Helper::guid(true);
return $this->success($user->save());
}
private function filter(Request $request, $builder)
/**
* Apply filters and sorts to the query builder
*
* @param Request $request
* @param Builder $builder
* @return void
*/
private function applyFiltersAndSorts(Request $request, Builder $builder): void
{
$filters = $request->input('filter');
if ($filters) {
foreach ($filters as $k => $filter) {
if ($filter['condition'] === '模糊') {
$filter['condition'] = 'like';
$filter['value'] = "%{$filter['value']}%";
}
if ($filter['key'] === 'd' || $filter['key'] === 'transfer_enable') {
$filter['value'] = $filter['value'] * 1073741824;
}
if ($filter['key'] === 'invite_by_email') {
$user = User::where('email', $filter['condition'], $filter['value'])->first();
$inviteUserId = isset($user->id) ? $user->id : 0;
$builder->where('invite_user_id', $inviteUserId);
unset($filters[$k]);
continue;
}
$builder->where($filter['key'], $filter['condition'], $filter['value']);
}
}
$this->applyFilters($request, $builder);
$this->applySorting($request, $builder);
}
public function fetch(UserFetch $request)
/**
* Apply filters to the query builder
*
* @param Request $request
* @param Builder $builder
* @return void
*/
private function applyFilters(Request $request, Builder $builder): void
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$userModel = User::select(
DB::raw('*'),
DB::raw('(u+d) as total_used')
)
->orderBy($sort, $sortType);
$this->filter($request, $userModel);
$total = $userModel->count();
$res = $userModel->forPage($current, $pageSize)
->get();
$plan = Plan::get();
for ($i = 0; $i < count($res); $i++) {
for ($k = 0; $k < count($plan); $k++) {
if ($plan[$k]['id'] == $res[$i]['plan_id']) {
$res[$i]['plan_name'] = $plan[$k]['name'];
}
}
$res[$i]['subscribe_url'] = Helper::getSubscribeUrl( $res[$i]['token']);
if (!$request->has('filter')) {
return;
}
collect($request->input('filter'))->each(function ($filter) use ($builder) {
$field = $filter['id'];
$value = $filter['value'];
$builder->where(function ($query) use ($field, $value) {
$this->buildFilterQuery($query, $field, $value);
});
});
}
/**
* Build the filter query based on field and value
*
* @param Builder $query
* @param string $field
* @param mixed $value
* @return void
*/
private function buildFilterQuery(Builder $query, string $field, mixed $value): void
{
// Handle array values for 'in' operations
if (is_array($value)) {
$query->whereIn($field === 'group_ids' ? 'group_id' : $field, $value);
return;
}
// Handle operator-based filtering
if (!is_string($value) || !str_contains($value, ':')) {
$query->where($field, 'like', "%{$value}%");
return;
}
[$operator, $filterValue] = explode(':', $value, 2);
// Convert numeric strings to appropriate type
if (is_numeric($filterValue)) {
$filterValue = strpos($filterValue, '.') !== false
? (float) $filterValue
: (int) $filterValue;
}
// Handle computed fields
$queryField = match ($field) {
'total_used' => DB::raw('(u + d)'),
default => $field
};
// Apply operator
$query->where($queryField, match (strtolower($operator)) {
'eq' => '=',
'gt' => '>',
'gte' => '>=',
'lt' => '<',
'lte' => '<=',
'like' => 'like',
'notlike' => 'not like',
'null' => static fn($q) => $q->whereNull($queryField),
'notnull' => static fn($q) => $q->whereNotNull($queryField),
default => 'like'
}, match (strtolower($operator)) {
'like', 'notlike' => "%{$filterValue}%",
'null', 'notnull' => null,
default => $filterValue
});
}
/**
* Apply sorting to the query builder
*
* @param Request $request
* @param Builder $builder
* @return void
*/
private function applySorting(Request $request, Builder $builder): void
{
if (!$request->has('sort')) {
return;
}
collect($request->input('sort'))->each(function ($sort) use ($builder) {
$field = $sort['id'];
$direction = $sort['desc'] ? 'DESC' : 'ASC';
$builder->orderBy($field, $direction);
});
}
/**
* Fetch paginated user list with filters and sorting
*
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function fetch(Request $request)
{
$current = $request->input('current', 1);
$pageSize = $request->input('pageSize', 10);
$userModel = User::with(['plan:id,name', 'invite_user:id,email', 'group:id,name'])
->select(DB::raw('*, (u+d) as total_used'));
$this->applyFiltersAndSorts($request, $userModel);
$users = $userModel->orderBy('id', 'desc')
->paginate($pageSize, ['*'], 'page', $current);
$users->getCollection()->transform(function ($user) {
return $this->transformUserData($user);
});
return response([
'data' => $res,
'total' => $total
'data' => $users->items(),
'total' => $users->total()
]);
}
/**
* Transform user data for response
*
* @param User $user
* @return User
*/
private function transformUserData(User $user): User
{
$user->subscribe_url = Helper::getSubscribeUrl($user->token);
$user->balance = $user->balance / 100;
$user->commission_balance = $user->commission_balance / 100;
return $user;
}
public function getUserInfoById(Request $request)
{
$request->validate([
'id'=> 'required|numeric'
],[
'id' => 'required|numeric'
], [
'id.required' => '用户ID不能为空'
]);
$user = User::find($request->input('id'))->load('invite_user');
@@ -117,6 +218,7 @@ class UserController extends Controller
if (!$plan) {
return $this->fail([400202, '订阅计划不存在']);
}
// return json_encode($plan);
$params['group_id'] = $plan->group_id;
}
// 处理邀请用户
@@ -126,16 +228,22 @@ class UserController extends Controller
$params['invite_user_id'] = null;
}
if (isset($params['banned']) && (int)$params['banned'] === 1) {
if (isset($params['banned']) && (int) $params['banned'] === 1) {
$authService = new AuthService($user);
$authService->removeAllSession();
$authService->removeSession();
}
if (isset($params['balance'])) {
$params['balance'] = $params['balance'] * 100;
}
if (isset($params['commission_balance'])) {
$params['commission_balance'] = $params['commission_balance'] * 100;
}
try {
$user->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'保存失败']);
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}
@@ -155,14 +263,14 @@ class UserController extends Controller
}
$data = "邮箱,余额,推广佣金,总流量,剩余流量,套餐到期时间,订阅计划,订阅地址\r\n";
foreach($res as $user) {
foreach ($res as $user) {
$expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
$balance = $user['balance'] / 100;
$commissionBalance = $user['commission_balance'] / 100;
$transferEnable = $user['transfer_enable'] ? $user['transfer_enable'] / 1073741824 : 0;
$notUseFlow = (($user['transfer_enable'] - ($user['u'] + $user['d'])) / 1073741824) ?? 0;
$planName = $user['plan_name'] ?? '无订阅';
$subscribeUrl = Helper::getSubscribeUrl($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;
@@ -174,7 +282,7 @@ class UserController extends Controller
if ($request->input('plan_id')) {
$plan = Plan::find($request->input('plan_id'));
if (!$plan) {
return $this->fail([400202,'订阅计划不存在']);
return $this->fail([400202, '订阅计划不存在']);
}
}
$user = [
@@ -187,11 +295,11 @@ class UserController extends Controller
'token' => Helper::guid()
];
if (User::where('email', $user['email'])->first()) {
return $this->fail([400201,'邮箱已存在于系统中']);
return $this->fail([400201, '邮箱已存在于系统中']);
}
$user['password'] = password_hash($request->input('password') ?? $user['email'], PASSWORD_DEFAULT);
if (!User::create($user)) {
return $this->fail([500,'生成失败']);
return $this->fail([500, '生成失败']);
}
return $this->success(true);
}
@@ -205,11 +313,11 @@ class UserController extends Controller
if ($request->input('plan_id')) {
$plan = Plan::find($request->input('plan_id'));
if (!$plan) {
return $this->fail([400202,'订阅计划不存在']);
return $this->fail([400202, '订阅计划不存在']);
}
}
$users = [];
for ($i = 0;$i < $request->input('generate_count');$i++) {
for ($i = 0; $i < $request->input('generate_count'); $i++) {
$user = [
'email' => Helper::randomChar(6) . '@' . $request->input('email_suffix'),
'plan_id' => isset($plan->id) ? $plan->id : NULL,
@@ -224,23 +332,23 @@ class UserController extends Controller
$user['password'] = password_hash($request->input('password') ?? $user['email'], PASSWORD_DEFAULT);
array_push($users, $user);
}
try{
try {
DB::beginTransaction();
if (!User::insert($users)) {
throw new \Exception();
}
DB::commit();
}catch(\Exception $e){
} catch (\Exception $e) {
DB::rollBack();
\Log::error($e);
return $this->fail([500,'生成失败']);
return $this->fail([500, '生成失败']);
}
$data = "账号,密码,过期时间,UUID,创建时间,订阅地址\r\n";
foreach($users as $user) {
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 = Helper::getSubscribeUrl($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;
@@ -254,17 +362,19 @@ class UserController extends Controller
$this->filter($request, $builder);
$users = $builder->get();
foreach ($users as $user) {
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => $request->input('subject'),
'template_name' => 'notify',
'template_value' => [
'name' => admin_setting('app_name', 'XBoard'),
'url' => admin_setting('app_url'),
'content' => $request->input('content')
]
],
'send_email_mass');
SendEmailJob::dispatch(
[
'email' => $user->email,
'subject' => $request->input('subject'),
'template_name' => 'notify',
'template_value' => [
'name' => admin_setting('app_name', 'XBoard'),
'url' => admin_setting('app_url'),
'content' => $request->input('content')
]
],
'send_email_mass'
);
}
return $this->success(true);
@@ -282,7 +392,7 @@ class UserController extends Controller
]);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'处理失败']);
return $this->fail([500, '处理失败']);
}
return $this->success(true);