mirror of
https://github.com/lkddi/Xboard.git
synced 2026-04-24 20:17:32 +08:00
feat: new xboard
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
|
||||
class ConfigController extends Controller
|
||||
{
|
||||
public function getEmailTemplate()
|
||||
{
|
||||
$path = resource_path('views/mail/');
|
||||
$files = array_map(function ($item) use ($path) {
|
||||
return str_replace($path, '', $item);
|
||||
}, glob($path . '*'));
|
||||
return $this->success($files);
|
||||
}
|
||||
|
||||
public function getThemeTemplate()
|
||||
{
|
||||
$path = public_path('theme/');
|
||||
$files = array_map(function ($item) use ($path) {
|
||||
return str_replace($path, '', $item);
|
||||
}, glob($path . '*'));
|
||||
return $this->success($files);
|
||||
}
|
||||
|
||||
public function testSendMail(Request $request)
|
||||
{
|
||||
$mailLog = MailService::sendEmail([
|
||||
'email' => $request->user()->email,
|
||||
'subject' => 'This is xboard test email',
|
||||
'template_name' => 'notify',
|
||||
'template_value' => [
|
||||
'name' => admin_setting('app_name', 'XBoard'),
|
||||
'content' => 'This is xboard test email',
|
||||
'url' => admin_setting('app_url')
|
||||
]
|
||||
]);
|
||||
return response([
|
||||
'data' => $mailLog,
|
||||
]);
|
||||
}
|
||||
|
||||
public function setTelegramWebhook(Request $request)
|
||||
{
|
||||
// 判断站点网址
|
||||
$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([
|
||||
'access_token' => md5(admin_setting('telegram_bot_token', $request->input('telegram_bot_token')))
|
||||
]);
|
||||
$telegramService = new TelegramService($request->input('telegram_bot_token'));
|
||||
$telegramService->getMe();
|
||||
$telegramService->setWebhook($hookUrl);
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
public function fetch(Request $request)
|
||||
{
|
||||
$key = $request->input('key');
|
||||
$data = [
|
||||
'invite' => [
|
||||
'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' => (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' => (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),
|
||||
'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),
|
||||
'tos_url' => admin_setting('tos_url'),
|
||||
'currency' => admin_setting('currency', 'CNY'),
|
||||
'currency_symbol' => admin_setting('currency_symbol', '¥'),
|
||||
],
|
||||
'subscribe' => [
|
||||
'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' => [
|
||||
'frontend_theme' => admin_setting('frontend_theme', 'Xboard'),
|
||||
'frontend_theme_sidebar' => admin_setting('frontend_theme_sidebar', 'light'),
|
||||
'frontend_theme_header' => admin_setting('frontend_theme_header', 'dark'),
|
||||
'frontend_theme_color' => admin_setting('frontend_theme_color', 'default'),
|
||||
'frontend_background_url' => admin_setting('frontend_background_url'),
|
||||
],
|
||||
'server' => [
|
||||
'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'),
|
||||
'email_host' => admin_setting('email_host'),
|
||||
'email_port' => admin_setting('email_port'),
|
||||
'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'),
|
||||
'remind_mail_enable' => (bool) admin_setting('remind_mail_enable', false),
|
||||
],
|
||||
'telegram' => [
|
||||
'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', '')
|
||||
],
|
||||
'safe' => [
|
||||
'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' => (bool) admin_setting('email_whitelist_enable', 0),
|
||||
'email_whitelist_suffix' => admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT),
|
||||
'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' => (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)
|
||||
]
|
||||
];
|
||||
if ($key && isset($data[$key])) {
|
||||
return $this->success([
|
||||
$key => $data[$key]
|
||||
]);
|
||||
}
|
||||
;
|
||||
// TODO: default should be in Dict
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
public function save(ConfigSave $request)
|
||||
{
|
||||
$data = $request->validated();
|
||||
foreach ($data as $k => $v) {
|
||||
if ($k == 'frontend_theme') {
|
||||
$themeService = new ThemeService();
|
||||
$themeService->switch($v);
|
||||
}
|
||||
admin_setting([$k => $v]);
|
||||
}
|
||||
// \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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V2\Admin;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\KnowledgeSave;
|
||||
use App\Http\Requests\Admin\KnowledgeSort;
|
||||
use App\Models\Knowledge;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class KnowledgeController extends Controller
|
||||
{
|
||||
public function fetch(Request $request)
|
||||
{
|
||||
if ($request->input('id')) {
|
||||
$knowledge = Knowledge::find($request->input('id'))->toArray();
|
||||
if (!$knowledge)
|
||||
return $this->fail([400202, '知识不存在']);
|
||||
return $this->success($knowledge);
|
||||
}
|
||||
$data = Knowledge::select(['title', 'id', 'updated_at', 'category', 'show'])
|
||||
->orderBy('sort', 'ASC')
|
||||
->get();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
public function getCategory(Request $request)
|
||||
{
|
||||
return $this->success(array_keys(Knowledge::get()->groupBy('category')->toArray()));
|
||||
}
|
||||
|
||||
public function save(KnowledgeSave $request)
|
||||
{
|
||||
$params = $request->validated();
|
||||
|
||||
if (!$request->input('id')) {
|
||||
if (!Knowledge::create($params)) {
|
||||
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->success(true);
|
||||
}
|
||||
|
||||
public function show(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'required|numeric'
|
||||
], [
|
||||
'id.required' => '知识库ID不能为空'
|
||||
]);
|
||||
$knowledge = Knowledge::find($request->input('id'));
|
||||
if (!$knowledge) {
|
||||
throw new ApiException('知识不存在');
|
||||
}
|
||||
$knowledge->show = !$knowledge->show;
|
||||
if (!$knowledge->save()) {
|
||||
throw new ApiException('保存失败');
|
||||
}
|
||||
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
public function sort(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'ids' => 'required|array'
|
||||
], [
|
||||
'ids.required' => '参数有误',
|
||||
'ids.array' => '参数有误'
|
||||
]);
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
foreach ($request->input('ids') as $k => $v) {
|
||||
$knowledge = Knowledge::find($v);
|
||||
$knowledge->timestamps = false;
|
||||
$knowledge->update(['sort' => $k + 1]);
|
||||
}
|
||||
DB::commit();
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
throw new ApiException('保存失败');
|
||||
}
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
public function drop(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'required|numeric'
|
||||
], [
|
||||
'id.required' => '知识库ID不能为空'
|
||||
]);
|
||||
$knowledge = Knowledge::find($request->input('id'));
|
||||
if (!$knowledge) {
|
||||
return $this->fail([400202, '知识不存在']);
|
||||
}
|
||||
if (!$knowledge->delete()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V2\Admin;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Payment;
|
||||
use App\Services\PaymentService;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class PaymentController extends Controller
|
||||
{
|
||||
public function getPaymentMethods()
|
||||
{
|
||||
$methods = [];
|
||||
foreach (glob(base_path('app//Payments') . '/*.php') as $file) {
|
||||
array_push($methods, pathinfo($file)['filename']);
|
||||
}
|
||||
return $this->success($methods);
|
||||
}
|
||||
|
||||
public function fetch()
|
||||
{
|
||||
$payments = Payment::orderBy('sort', 'ASC')->get();
|
||||
foreach ($payments as $k => $v) {
|
||||
$notifyUrl = url("/api/v1/guest/payment/notify/{$v->payment}/{$v->uuid}");
|
||||
if ($v->notify_domain) {
|
||||
$parseUrl = parse_url($notifyUrl);
|
||||
$notifyUrl = $v->notify_domain . $parseUrl['path'];
|
||||
}
|
||||
$payments[$k]['notify_url'] = $notifyUrl;
|
||||
}
|
||||
return $this->success($payments);
|
||||
}
|
||||
|
||||
public function getPaymentForm(Request $request)
|
||||
{
|
||||
$paymentService = new PaymentService($request->input('payment'), $request->input('id'));
|
||||
return $this->success(collect($paymentService->form())->values());
|
||||
}
|
||||
|
||||
public function show(Request $request)
|
||||
{
|
||||
$payment = Payment::find($request->input('id'));
|
||||
if (!$payment) return $this->fail([400202 ,'支付方式不存在']);
|
||||
$payment->enable = !$payment->enable;
|
||||
if (!$payment->save()) return $this->fail([500 ,'保存失败']);
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
public function save(Request $request)
|
||||
{
|
||||
if (!admin_setting('app_url')) {
|
||||
return $this->fail([400 ,'请在站点配置中配置站点地址']);
|
||||
}
|
||||
$params = $request->validate([
|
||||
'name' => 'required',
|
||||
'icon' => 'nullable',
|
||||
'payment' => 'required',
|
||||
'config' => 'required',
|
||||
'notify_domain' => 'nullable|url',
|
||||
'handling_fee_fixed' => 'nullable|integer',
|
||||
'handling_fee_percent' => 'nullable|numeric|between:0,100'
|
||||
], [
|
||||
'name.required' => '显示名称不能为空',
|
||||
'payment.required' => '网关参数不能为空',
|
||||
'config.required' => '配置参数不能为空',
|
||||
'notify_domain.url' => '自定义通知域名格式有误',
|
||||
'handling_fee_fixed.integer' => '固定手续费格式有误',
|
||||
'handling_fee_percent.between' => '百分比手续费范围须在0-100之间'
|
||||
]);
|
||||
if ($request->input('id')) {
|
||||
$payment = Payment::find($request->input('id'));
|
||||
if (!$payment) return $this->fail([400202 ,'支付方式不存在']);
|
||||
try {
|
||||
$payment->update($params);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error($e);
|
||||
return $this->fail([500 ,'保存失败']);
|
||||
}
|
||||
return $this->success(true);
|
||||
}
|
||||
$params['uuid'] = Helper::randomChar(8);
|
||||
if (!Payment::create($params)) {
|
||||
return $this->fail([500 ,'保存失败']);
|
||||
}
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
public function drop(Request $request)
|
||||
{
|
||||
$payment = Payment::find($request->input('id'));
|
||||
if (!$payment) return $this->fail([400202 ,'支付方式不存在']);
|
||||
return $this->success($payment->delete());
|
||||
}
|
||||
|
||||
|
||||
public function sort(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'ids' => 'required|array'
|
||||
], [
|
||||
'ids.required' => '参数有误',
|
||||
'ids.array' => '参数有误'
|
||||
]);
|
||||
try{
|
||||
DB::beginTransaction();
|
||||
foreach ($request->input('ids') as $k => $v) {
|
||||
if (!Payment::find($v)->update(['sort' => $k + 1])) {
|
||||
throw new \Exception();
|
||||
}
|
||||
}
|
||||
DB::commit();
|
||||
}catch(\Exception $e){
|
||||
DB::rollBack();
|
||||
return $this->fail([500 ,'保存失败']);
|
||||
}
|
||||
|
||||
return $this->success(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V2\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Order;
|
||||
use App\Models\Plan;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class PlanController extends Controller
|
||||
{
|
||||
public function fetch(Request $request)
|
||||
{
|
||||
$plans = Plan::orderBy('sort', 'ASC')
|
||||
->with([
|
||||
'group:id,name'
|
||||
])
|
||||
->withCount('users')
|
||||
->get();
|
||||
|
||||
return $this->success($plans);
|
||||
}
|
||||
|
||||
public function save(Request $request)
|
||||
{
|
||||
$params = $request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'required|string',
|
||||
'content' => 'nullable|string',
|
||||
'reset_traffic_method' => 'integer|nullable',
|
||||
'transfer_enable' => 'integer|required',
|
||||
'prices' => 'array|nullable',
|
||||
'group_id' => 'integer|nullable',
|
||||
'speed_limit' => 'integer|nullable',
|
||||
'device_limit' => 'integer|nullable',
|
||||
'capacity_limit' => 'integer|nullable',
|
||||
]);
|
||||
if ($request->input('id')) {
|
||||
$plan = Plan::find($request->input('id'));
|
||||
if (!$plan) {
|
||||
return $this->fail([400202, '该订阅不存在']);
|
||||
}
|
||||
DB::beginTransaction();
|
||||
// update user group id and transfer
|
||||
try {
|
||||
if ($request->input('force_update')) {
|
||||
User::where('plan_id', $plan->id)->update([
|
||||
'group_id' => $params['group_id'],
|
||||
'transfer_enable' => $params['transfer_enable'] * 1073741824,
|
||||
'speed_limit' => $params['speed_limit'],
|
||||
'device_limit' => $params['device_limit'],
|
||||
]);
|
||||
}
|
||||
$plan->update($params);
|
||||
DB::commit();
|
||||
return $this->success(true);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
\Log::error($e);
|
||||
return $this->fail([500, '保存失败']);
|
||||
}
|
||||
}
|
||||
if (!Plan::create($params)) {
|
||||
return $this->fail([500, '创建失败']);
|
||||
}
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
public function drop(Request $request)
|
||||
{
|
||||
if (Order::where('plan_id', $request->input('id'))->first()) {
|
||||
return $this->fail([400201, '该订阅下存在订单无法删除']);
|
||||
}
|
||||
if (User::where('plan_id', $request->input('id'))->first()) {
|
||||
return $this->fail([400201, '该订阅下存在用户无法删除']);
|
||||
}
|
||||
if ($request->input('id')) {
|
||||
$plan = Plan::find($request->input('id'));
|
||||
if (!$plan) {
|
||||
return $this->fail([400202, '该订阅不存在']);
|
||||
}
|
||||
}
|
||||
return $this->success($plan->delete());
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$updateData = $request->only([
|
||||
'show',
|
||||
'renew',
|
||||
'sell'
|
||||
]);
|
||||
|
||||
$plan = Plan::find($request->input('id'));
|
||||
if (!$plan) {
|
||||
return $this->fail([400202, '该订阅不存在']);
|
||||
}
|
||||
|
||||
try {
|
||||
$plan->update($updateData);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error($e);
|
||||
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) {
|
||||
if (!Plan::find($v)->update(['sort' => $k + 1])) {
|
||||
throw new \Exception();
|
||||
}
|
||||
}
|
||||
DB::commit();
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
\Log::error($e);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V2\Admin\Server;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ServerRoute;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class RouteController extends Controller
|
||||
{
|
||||
public function fetch(Request $request)
|
||||
{
|
||||
$routes = ServerRoute::get();
|
||||
// TODO: remove on 1.8.0
|
||||
foreach ($routes as $k => $route) {
|
||||
$array = json_decode($route->match, true);
|
||||
if (is_array($array)) $routes[$k]['match'] = $array;
|
||||
}
|
||||
// TODO: remove on 1.8.0
|
||||
return [
|
||||
'data' => $routes
|
||||
];
|
||||
}
|
||||
|
||||
public function save(Request $request)
|
||||
{
|
||||
$params = $request->validate([
|
||||
'remarks' => 'required',
|
||||
'match' => 'required|array',
|
||||
'action' => 'required|in:block,dns',
|
||||
'action_value' => 'nullable'
|
||||
], [
|
||||
'remarks.required' => '备注不能为空',
|
||||
'match.required' => '匹配值不能为空',
|
||||
'action.required' => '动作类型不能为空',
|
||||
'action.in' => '动作类型参数有误'
|
||||
]);
|
||||
$params['match'] = array_filter($params['match']);
|
||||
// TODO: remove on 1.8.0
|
||||
$params['match'] = json_encode($params['match']);
|
||||
// TODO: remove on 1.8.0
|
||||
if ($request->input('id')) {
|
||||
try {
|
||||
$route = ServerRoute::find($request->input('id'));
|
||||
$route->update($params);
|
||||
return $this->success(true);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error($e);
|
||||
return $this->fail([500,'保存失败']);
|
||||
}
|
||||
}
|
||||
try{
|
||||
ServerRoute::create($params);
|
||||
return $this->success(true);
|
||||
}catch(\Exception $e){
|
||||
\Log::error($e);
|
||||
return $this->fail([500,'创建失败']);
|
||||
}
|
||||
}
|
||||
|
||||
public function drop(Request $request)
|
||||
{
|
||||
$route = ServerRoute::find($request->input('id'));
|
||||
if (!$route) throw new ApiException('路由不存在');
|
||||
if (!$route->delete()) throw new ApiException('删除失败');
|
||||
return [
|
||||
'data' => true
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
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 Laravel\Horizon\Contracts\JobRepository;
|
||||
use Laravel\Horizon\Contracts\MasterSupervisorRepository;
|
||||
use Laravel\Horizon\Contracts\MetricsRepository;
|
||||
use Laravel\Horizon\Contracts\SupervisorRepository;
|
||||
use Laravel\Horizon\Contracts\WorkloadRepository;
|
||||
use Laravel\Horizon\WaitTimeCalculator;
|
||||
|
||||
class SystemController extends Controller
|
||||
{
|
||||
public function getSystemStatus()
|
||||
{
|
||||
$data = [
|
||||
'schedule' => $this->getScheduleStatus(),
|
||||
'horizon' => $this->getHorizonStatus(),
|
||||
'schedule_last_runtime' => Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null))
|
||||
];
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
public function getQueueWorkload(WorkloadRepository $workload)
|
||||
{
|
||||
return $this->success(collect($workload->get())->sortBy('name')->values()->toArray());
|
||||
}
|
||||
|
||||
protected function getScheduleStatus():bool
|
||||
{
|
||||
return (time() - 120) < Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null));
|
||||
}
|
||||
|
||||
protected function getHorizonStatus():bool
|
||||
{
|
||||
if (! $masters = app(MasterSupervisorRepository::class)->all()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return collect($masters)->contains(function ($master) {
|
||||
return $master->status === 'paused';
|
||||
}) ? false : true;
|
||||
}
|
||||
|
||||
public function getQueueStats()
|
||||
{
|
||||
$data = [
|
||||
'failedJobs' => app(JobRepository::class)->countRecentlyFailed(),
|
||||
'jobsPerMinute' => app(MetricsRepository::class)->jobsProcessedPerMinute(),
|
||||
'pausedMasters' => $this->totalPausedMasters(),
|
||||
'periods' => [
|
||||
'failedJobs' => config('horizon.trim.recent_failed', config('horizon.trim.failed')),
|
||||
'recentJobs' => config('horizon.trim.recent'),
|
||||
],
|
||||
'processes' => $this->totalProcessCount(),
|
||||
'queueWithMaxRuntime' => app(MetricsRepository::class)->queueWithMaximumRuntime(),
|
||||
'queueWithMaxThroughput' => app(MetricsRepository::class)->queueWithMaximumThroughput(),
|
||||
'recentJobs' => app(JobRepository::class)->countRecent(),
|
||||
'status' => $this->getHorizonStatus(),
|
||||
'wait' => collect(app(WaitTimeCalculator::class)->calculate())->take(1),
|
||||
];
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total process count across all supervisors.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function totalProcessCount()
|
||||
{
|
||||
$supervisors = app(SupervisorRepository::class)->all();
|
||||
|
||||
return collect($supervisors)->reduce(function ($carry, $supervisor) {
|
||||
return $carry + collect($supervisor->processes)->sum();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of master supervisors that are currently paused.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function totalPausedMasters()
|
||||
{
|
||||
if (! $masters = app(MasterSupervisorRepository::class)->all()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return collect($masters)->filter(function ($master) {
|
||||
return $master->status === 'paused';
|
||||
})->count();
|
||||
}
|
||||
|
||||
public function getSystemLog(Request $request) {
|
||||
$current = $request->input('current') ? $request->input('current') : 1;
|
||||
$pageSize = $request->input('page_size') >= 10 ? $request->input('page_size') : 10;
|
||||
$builder = LogModel::orderBy('created_at', 'DESC')
|
||||
->setFilterAllowKeys('level');
|
||||
$total = $builder->count();
|
||||
$res = $builder->forPage($current, $pageSize)
|
||||
->get();
|
||||
return response([
|
||||
'data' => $res,
|
||||
'total' => $total
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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, '关闭失败']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V2\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\UserGenerate;
|
||||
use App\Http\Requests\Admin\UserSendMail;
|
||||
use App\Http\Requests\Admin\UserUpdate;
|
||||
use App\Jobs\SendEmailJob;
|
||||
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;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function resetSecret(Request $request)
|
||||
{
|
||||
$user = User::find($request->input('id'));
|
||||
if (!$user)
|
||||
return $this->fail([400202, '用户不存在']);
|
||||
$user->token = Helper::guid();
|
||||
$user->uuid = Helper::guid(true);
|
||||
return $this->success($user->save());
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filters and sorts to the query builder
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Builder $builder
|
||||
* @return void
|
||||
*/
|
||||
private function applyFiltersAndSorts(Request $request, Builder $builder): void
|
||||
{
|
||||
$this->applyFilters($request, $builder);
|
||||
$this->applySorting($request, $builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filters to the query builder
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Builder $builder
|
||||
* @return void
|
||||
*/
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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' => $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' => '用户ID不能为空'
|
||||
]);
|
||||
$user = User::find($request->input('id'))->load('invite_user');
|
||||
return $this->success($user);
|
||||
}
|
||||
|
||||
public function update(UserUpdate $request)
|
||||
{
|
||||
$params = $request->validated();
|
||||
|
||||
$user = User::find($request->input('id'));
|
||||
if (!$user) {
|
||||
return $this->fail([400202, '用户不存在']);
|
||||
}
|
||||
// 检查邮箱是否被使用
|
||||
if (User::where('email', $params['email'])->first() && $user->email !== $params['email']) {
|
||||
return $this->fail([400201, '邮箱已被使用']);
|
||||
}
|
||||
// 处理密码
|
||||
if (isset($params['password'])) {
|
||||
$params['password'] = password_hash($params['password'], PASSWORD_DEFAULT);
|
||||
$params['password_algo'] = NULL;
|
||||
} else {
|
||||
unset($params['password']);
|
||||
}
|
||||
// 处理订阅计划
|
||||
if (isset($params['plan_id'])) {
|
||||
$plan = Plan::find($params['plan_id']);
|
||||
if (!$plan) {
|
||||
return $this->fail([400202, '订阅计划不存在']);
|
||||
}
|
||||
// return json_encode($plan);
|
||||
$params['group_id'] = $plan->group_id;
|
||||
}
|
||||
// 处理邀请用户
|
||||
if ($request->input('invite_user_email') && $inviteUser = User::where('email', $request->input('invite_user_email'))->first()) {
|
||||
$params['invite_user_id'] = $inviteUser->id;
|
||||
} else {
|
||||
$params['invite_user_id'] = null;
|
||||
}
|
||||
|
||||
if (isset($params['banned']) && (int) $params['banned'] === 1) {
|
||||
$authService = new AuthService($user);
|
||||
$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->success(true);
|
||||
}
|
||||
|
||||
public function dumpCSV(Request $request)
|
||||
{
|
||||
$userModel = User::orderBy('id', 'asc');
|
||||
$this->filter($request, $userModel);
|
||||
$res = $userModel->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'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$data = "邮箱,余额,推广佣金,总流量,剩余流量,套餐到期时间,订阅计划,订阅地址\r\n";
|
||||
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('/api/v1/client/subscribe?token=' . $user['token']);
|
||||
$data .= "{$user['email']},{$balance},{$commissionBalance},{$transferEnable},{$notUseFlow},{$expireDate},{$planName},{$subscribeUrl}\r\n";
|
||||
}
|
||||
echo "\xEF\xBB\xBF" . $data;
|
||||
}
|
||||
|
||||
public function generate(UserGenerate $request)
|
||||
{
|
||||
if ($request->input('email_prefix')) {
|
||||
if ($request->input('plan_id')) {
|
||||
$plan = Plan::find($request->input('plan_id'));
|
||||
if (!$plan) {
|
||||
return $this->fail([400202, '订阅计划不存在']);
|
||||
}
|
||||
}
|
||||
$user = [
|
||||
'email' => $request->input('email_prefix') . '@' . $request->input('email_suffix'),
|
||||
'plan_id' => isset($plan->id) ? $plan->id : NULL,
|
||||
'group_id' => isset($plan->group_id) ? $plan->group_id : NULL,
|
||||
'transfer_enable' => isset($plan->transfer_enable) ? $plan->transfer_enable * 1073741824 : 0,
|
||||
'expired_at' => $request->input('expired_at') ?? NULL,
|
||||
'uuid' => Helper::guid(true),
|
||||
'token' => Helper::guid()
|
||||
];
|
||||
if (User::where('email', $user['email'])->first()) {
|
||||
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->success(true);
|
||||
}
|
||||
if ($request->input('generate_count')) {
|
||||
$this->multiGenerate($request);
|
||||
}
|
||||
}
|
||||
|
||||
private function multiGenerate(Request $request)
|
||||
{
|
||||
if ($request->input('plan_id')) {
|
||||
$plan = Plan::find($request->input('plan_id'));
|
||||
if (!$plan) {
|
||||
return $this->fail([400202, '订阅计划不存在']);
|
||||
}
|
||||
}
|
||||
$users = [];
|
||||
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,
|
||||
'group_id' => isset($plan->group_id) ? $plan->group_id : NULL,
|
||||
'transfer_enable' => isset($plan->transfer_enable) ? $plan->transfer_enable * 1073741824 : 0,
|
||||
'expired_at' => $request->input('expired_at') ?? NULL,
|
||||
'uuid' => Helper::guid(true),
|
||||
'token' => Helper::guid(),
|
||||
'created_at' => time(),
|
||||
'updated_at' => time()
|
||||
];
|
||||
$user['password'] = password_hash($request->input('password') ?? $user['email'], PASSWORD_DEFAULT);
|
||||
array_push($users, $user);
|
||||
}
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
if (!User::insert($users)) {
|
||||
throw new \Exception();
|
||||
}
|
||||
DB::commit();
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
\Log::error($e);
|
||||
return $this->fail([500, '生成失败']);
|
||||
}
|
||||
$data = "账号,密码,过期时间,UUID,创建时间,订阅地址\r\n";
|
||||
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('/api/v1/client/subscribe?token=' . $user['token']);
|
||||
$data .= "{$user['email']},{$password},{$expireDate},{$user['uuid']},{$createDate},{$subscribeUrl}\r\n";
|
||||
}
|
||||
echo $data;
|
||||
}
|
||||
|
||||
public function sendMail(UserSendMail $request)
|
||||
{
|
||||
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
|
||||
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
|
||||
$builder = User::orderBy($sort, $sortType);
|
||||
$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'
|
||||
);
|
||||
}
|
||||
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
public function ban(Request $request)
|
||||
{
|
||||
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
|
||||
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
|
||||
$builder = User::orderBy($sort, $sortType);
|
||||
$this->filter($request, $builder);
|
||||
try {
|
||||
$builder->update([
|
||||
'banned' => 1
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error($e);
|
||||
return $this->fail([500, '处理失败']);
|
||||
}
|
||||
|
||||
return $this->success(true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user