first commit
This commit is contained in:
300
Xboard/app/Http/Controllers/V2/Admin/ConfigController.php
Normal file
300
Xboard/app/Http/Controllers/V2/Admin/ConfigController.php
Normal file
@@ -0,0 +1,300 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V2\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\ConfigSave;
|
||||
use App\Models\SubscribeTemplate;
|
||||
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)
|
||||
{
|
||||
$hookUrl = $this->resolveTelegramWebhookUrl();
|
||||
if (blank($hookUrl)) {
|
||||
return $this->fail([422, 'Telegram Webhook地址未配置']);
|
||||
}
|
||||
$hookUrl .= '?' . 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(url: $hookUrl);
|
||||
$telegramService->registerBotCommands();
|
||||
return $this->success([
|
||||
'success' => true,
|
||||
'webhook_url' => $hookUrl,
|
||||
'webhook_base_url' => $this->getTelegramWebhookBaseUrl(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function fetch(Request $request)
|
||||
{
|
||||
$key = $request->input('key');
|
||||
$configMappings = $this->getConfigMappings();
|
||||
if ($key && isset($configMappings[$key])) {
|
||||
return $this->success([$key => $configMappings[$key]]);
|
||||
}
|
||||
|
||||
return $this->success($configMappings);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置映射数据
|
||||
*
|
||||
* @return array 配置映射数组
|
||||
*/
|
||||
private function getConfigMappings(): array
|
||||
{
|
||||
return [
|
||||
'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', '¥'),
|
||||
'ticket_must_wait_reply' => (bool) admin_setting('ticket_must_wait_reply', 0),
|
||||
],
|
||||
'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),
|
||||
'server_ws_enable' => (bool) admin_setting('server_ws_enable', 1),
|
||||
'server_ws_url' => admin_setting('server_ws_url', ''),
|
||||
],
|
||||
'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_webhook_url' => admin_setting('telegram_webhook_url'),
|
||||
'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),
|
||||
'captcha_enable' => (bool) admin_setting('captcha_enable', 0),
|
||||
'captcha_type' => admin_setting('captcha_type', 'recaptcha'),
|
||||
'recaptcha_key' => admin_setting('recaptcha_key', ''),
|
||||
'recaptcha_site_key' => admin_setting('recaptcha_site_key', ''),
|
||||
'recaptcha_v3_secret_key' => admin_setting('recaptcha_v3_secret_key', ''),
|
||||
'recaptcha_v3_site_key' => admin_setting('recaptcha_v3_site_key', ''),
|
||||
'recaptcha_v3_score_threshold' => admin_setting('recaptcha_v3_score_threshold', 0.5),
|
||||
'turnstile_secret_key' => admin_setting('turnstile_secret_key', ''),
|
||||
'turnstile_site_key' => admin_setting('turnstile_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),
|
||||
// 保持向后兼容
|
||||
'recaptcha_enable' => (bool) admin_setting('captcha_enable', 0)
|
||||
],
|
||||
'subscribe_template' => [
|
||||
'subscribe_template_singbox' => $this->formatTemplateContent(
|
||||
subscribe_template('singbox') ?? '',
|
||||
'json'
|
||||
),
|
||||
'subscribe_template_clash' => subscribe_template('clash') ?? '',
|
||||
'subscribe_template_clashmeta' => subscribe_template('clashmeta') ?? '',
|
||||
'subscribe_template_stash' => subscribe_template('stash') ?? '',
|
||||
'subscribe_template_surge' => subscribe_template('surge') ?? '',
|
||||
'subscribe_template_surfboard' => subscribe_template('surfboard') ?? ''
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function save(ConfigSave $request)
|
||||
{
|
||||
$data = $request->validated();
|
||||
|
||||
$templateKeys = [
|
||||
'subscribe_template_singbox' => 'singbox',
|
||||
'subscribe_template_clash' => 'clash',
|
||||
'subscribe_template_clashmeta' => 'clashmeta',
|
||||
'subscribe_template_stash' => 'stash',
|
||||
'subscribe_template_surge' => 'surge',
|
||||
'subscribe_template_surfboard' => 'surfboard',
|
||||
];
|
||||
|
||||
foreach ($data as $k => $v) {
|
||||
if (isset($templateKeys[$k])) {
|
||||
SubscribeTemplate::setContent($templateKeys[$k], $v);
|
||||
continue;
|
||||
}
|
||||
if ($k == 'frontend_theme') {
|
||||
$themeService = app(ThemeService::class);
|
||||
$themeService->switch($v);
|
||||
}
|
||||
admin_setting([$k => $v]);
|
||||
}
|
||||
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化模板内容
|
||||
*
|
||||
* @param mixed $content 模板内容
|
||||
* @param string $format 输出格式 (json|string)
|
||||
* @return string 格式化后的内容
|
||||
*/
|
||||
private function formatTemplateContent(mixed $content, string $format = 'string'): string
|
||||
{
|
||||
return match ($format) {
|
||||
'json' => match (true) {
|
||||
is_array($content) => json_encode(
|
||||
value: $content,
|
||||
flags: JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
|
||||
),
|
||||
|
||||
is_string($content) && str($content)->isJson() => rescue(
|
||||
callback: fn() => json_encode(
|
||||
value: json_decode($content, associative: true, flags: JSON_THROW_ON_ERROR),
|
||||
flags: JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
|
||||
),
|
||||
rescue: $content,
|
||||
report: false
|
||||
),
|
||||
|
||||
default => str($content)->toString()
|
||||
},
|
||||
|
||||
default => str($content)->toString()
|
||||
};
|
||||
}
|
||||
|
||||
private function getTelegramWebhookBaseUrl(): ?string
|
||||
{
|
||||
$customUrl = trim((string) admin_setting('telegram_webhook_url', ''));
|
||||
if ($customUrl !== '') {
|
||||
return rtrim($customUrl, '/');
|
||||
}
|
||||
|
||||
$appUrl = trim((string) admin_setting('app_url', ''));
|
||||
if ($appUrl !== '') {
|
||||
return rtrim($appUrl, '/');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function resolveTelegramWebhookUrl(): ?string
|
||||
{
|
||||
$baseUrl = $this->getTelegramWebhookBaseUrl();
|
||||
if (!$baseUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (str_contains($baseUrl, '/api/v1/guest/telegram/webhook')) {
|
||||
return $baseUrl;
|
||||
}
|
||||
|
||||
return $baseUrl . '/api/v1/guest/telegram/webhook';
|
||||
}
|
||||
}
|
||||
186
Xboard/app/Http/Controllers/V2/Admin/CouponController.php
Normal file
186
Xboard/app/Http/Controllers/V2/Admin/CouponController.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?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 $this->paginate($coupons);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
622
Xboard/app/Http/Controllers/V2/Admin/GiftCardController.php
Normal file
622
Xboard/app/Http/Controllers/V2/Admin/GiftCardController.php
Normal file
@@ -0,0 +1,622 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V2\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\GiftCardCode;
|
||||
use App\Models\GiftCardTemplate;
|
||||
use App\Models\GiftCardUsage;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class GiftCardController extends Controller
|
||||
{
|
||||
/**
|
||||
* 获取礼品卡模板列表
|
||||
*/
|
||||
public function templates(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'type' => 'integer|min:1|max:10',
|
||||
'status' => 'integer|in:0,1',
|
||||
'page' => 'integer|min:1',
|
||||
'per_page' => 'integer|min:1|max:1000',
|
||||
]);
|
||||
|
||||
$query = GiftCardTemplate::query();
|
||||
|
||||
if ($request->has('type')) {
|
||||
$query->where('type', $request->input('type'));
|
||||
}
|
||||
|
||||
if ($request->has('status')) {
|
||||
$query->where('status', $request->input('status'));
|
||||
}
|
||||
|
||||
$perPage = $request->input('per_page', 15);
|
||||
$templates = $query->orderBy('sort', 'asc')
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate($perPage);
|
||||
|
||||
$data = $templates->getCollection()->map(function ($template) {
|
||||
return [
|
||||
'id' => $template->id,
|
||||
'name' => $template->name,
|
||||
'description' => $template->description,
|
||||
'type' => $template->type,
|
||||
'type_name' => $template->type_name,
|
||||
'status' => $template->status,
|
||||
'conditions' => $template->conditions,
|
||||
'rewards' => $template->rewards,
|
||||
'limits' => $template->limits,
|
||||
'special_config' => $template->special_config,
|
||||
'icon' => $template->icon,
|
||||
'background_image' => $template->background_image,
|
||||
'theme_color' => $template->theme_color,
|
||||
'sort' => $template->sort,
|
||||
'admin_id' => $template->admin_id,
|
||||
'created_at' => $template->created_at,
|
||||
'updated_at' => $template->updated_at,
|
||||
// 统计信息
|
||||
'codes_count' => $template->codes()->count(),
|
||||
'used_count' => $template->usages()->count(),
|
||||
];
|
||||
})->values();
|
||||
|
||||
return $this->paginate( $templates);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建礼品卡模板
|
||||
*/
|
||||
public function createTemplate(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'type' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::in(array_keys(GiftCardTemplate::getTypeMap()))
|
||||
],
|
||||
'status' => 'boolean',
|
||||
'conditions' => 'nullable|array',
|
||||
'rewards' => 'required|array',
|
||||
'limits' => 'nullable|array',
|
||||
'special_config' => 'nullable|array',
|
||||
'icon' => 'nullable|string|max:255',
|
||||
'background_image' => 'nullable|string|url|max:255',
|
||||
'theme_color' => 'nullable|string|regex:/^#[0-9A-Fa-f]{6}$/',
|
||||
'sort' => 'integer|min:0',
|
||||
], [
|
||||
'name.required' => '礼品卡名称不能为空',
|
||||
'type.required' => '礼品卡类型不能为空',
|
||||
'type.in' => '无效的礼品卡类型',
|
||||
'rewards.required' => '奖励配置不能为空',
|
||||
'theme_color.regex' => '主题色格式不正确',
|
||||
'background_image.url' => '背景图片必须是有效的URL',
|
||||
]);
|
||||
|
||||
try {
|
||||
$template = GiftCardTemplate::create([
|
||||
'name' => $request->input('name'),
|
||||
'description' => $request->input('description'),
|
||||
'type' => $request->input('type'),
|
||||
'status' => $request->input('status', true),
|
||||
'conditions' => $request->input('conditions'),
|
||||
'rewards' => $request->input('rewards'),
|
||||
'limits' => $request->input('limits'),
|
||||
'special_config' => $request->input('special_config'),
|
||||
'icon' => $request->input('icon'),
|
||||
'background_image' => $request->input('background_image'),
|
||||
'theme_color' => $request->input('theme_color', '#1890ff'),
|
||||
'sort' => $request->input('sort', 0),
|
||||
'admin_id' => $request->user()->id,
|
||||
'created_at' => time(),
|
||||
'updated_at' => time(),
|
||||
]);
|
||||
|
||||
return $this->success($template);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('创建礼品卡模板失败', [
|
||||
'admin_id' => $request->user()->id,
|
||||
'data' => $request->all(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return $this->fail([500, '创建失败']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新礼品卡模板
|
||||
*/
|
||||
public function updateTemplate(Request $request)
|
||||
{
|
||||
$validatedData = $request->validate([
|
||||
'id' => 'required|integer|exists:v2_gift_card_template,id',
|
||||
'name' => 'sometimes|required|string|max:255',
|
||||
'description' => 'sometimes|nullable|string',
|
||||
'type' => [
|
||||
'sometimes',
|
||||
'required',
|
||||
'integer',
|
||||
Rule::in(array_keys(GiftCardTemplate::getTypeMap()))
|
||||
],
|
||||
'status' => 'sometimes|boolean',
|
||||
'conditions' => 'sometimes|nullable|array',
|
||||
'rewards' => 'sometimes|required|array',
|
||||
'limits' => 'sometimes|nullable|array',
|
||||
'special_config' => 'sometimes|nullable|array',
|
||||
'icon' => 'sometimes|nullable|string|max:255',
|
||||
'background_image' => 'sometimes|nullable|string|url|max:255',
|
||||
'theme_color' => 'sometimes|nullable|string|regex:/^#[0-9A-Fa-f]{6}$/',
|
||||
'sort' => 'sometimes|integer|min:0',
|
||||
]);
|
||||
|
||||
$template = GiftCardTemplate::find($validatedData['id']);
|
||||
if (!$template) {
|
||||
return $this->fail([404, '模板不存在']);
|
||||
}
|
||||
|
||||
try {
|
||||
$updateData = collect($validatedData)->except('id')->all();
|
||||
|
||||
if (empty($updateData)) {
|
||||
return $this->success($template);
|
||||
}
|
||||
|
||||
$updateData['updated_at'] = time();
|
||||
|
||||
$template->update($updateData);
|
||||
|
||||
return $this->success($template->fresh());
|
||||
} catch (\Exception $e) {
|
||||
Log::error('更新礼品卡模板失败', [
|
||||
'admin_id' => $request->user()->id,
|
||||
'template_id' => $template->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return $this->fail([500, '更新失败']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除礼品卡模板
|
||||
*/
|
||||
public function deleteTemplate(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'required|integer|exists:v2_gift_card_template,id',
|
||||
]);
|
||||
|
||||
$template = GiftCardTemplate::find($request->input('id'));
|
||||
if (!$template) {
|
||||
return $this->fail([404, '模板不存在']);
|
||||
}
|
||||
|
||||
// 检查是否有关联的兑换码
|
||||
if ($template->codes()->exists()) {
|
||||
return $this->fail([400, '该模板下存在兑换码,无法删除']);
|
||||
}
|
||||
|
||||
try {
|
||||
$template->delete();
|
||||
return $this->success(true);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('删除礼品卡模板失败', [
|
||||
'admin_id' => $request->user()->id,
|
||||
'template_id' => $template->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return $this->fail([500, '删除失败']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成兑换码
|
||||
*/
|
||||
public function generateCodes(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'template_id' => 'required|integer|exists:v2_gift_card_template,id',
|
||||
'count' => 'required|integer|min:1|max:10000',
|
||||
'prefix' => 'nullable|string|max:10|regex:/^[A-Z0-9]*$/',
|
||||
'expires_hours' => 'nullable|integer|min:1',
|
||||
'max_usage' => 'integer|min:1|max:1000',
|
||||
], [
|
||||
'template_id.required' => '请选择礼品卡模板',
|
||||
'count.required' => '请指定生成数量',
|
||||
'count.max' => '单次最多生成10000个兑换码',
|
||||
'prefix.regex' => '前缀只能包含大写字母和数字',
|
||||
]);
|
||||
|
||||
$template = GiftCardTemplate::find($request->input('template_id'));
|
||||
if (!$template->isAvailable()) {
|
||||
return $this->fail([400, '模板已被禁用']);
|
||||
}
|
||||
|
||||
try {
|
||||
$options = [
|
||||
'prefix' => $request->input('prefix', 'GC'),
|
||||
'max_usage' => $request->input('max_usage', 1),
|
||||
];
|
||||
|
||||
if ($request->has('expires_hours')) {
|
||||
$options['expires_at'] = time() + ($request->input('expires_hours') * 3600);
|
||||
}
|
||||
|
||||
$batchId = GiftCardCode::batchGenerate(
|
||||
$request->input('template_id'),
|
||||
$request->input('count'),
|
||||
$options
|
||||
);
|
||||
|
||||
// 查询本次生成的所有兑换码
|
||||
$codes = GiftCardCode::where('batch_id', $batchId)->get();
|
||||
|
||||
// 判断是否导出 CSV
|
||||
if ($request->input('download_csv')) {
|
||||
$headers = [
|
||||
'Content-Type' => 'text/csv',
|
||||
'Content-Disposition' => 'attachment; filename="gift_codes.csv"',
|
||||
];
|
||||
$callback = function () use ($codes, $template) {
|
||||
$handle = fopen('php://output', 'w');
|
||||
// 表头
|
||||
fputcsv($handle, [
|
||||
'兑换码',
|
||||
'前缀',
|
||||
'有效期',
|
||||
'最大使用次数',
|
||||
'批次号',
|
||||
'创建时间',
|
||||
'模板名称',
|
||||
'模板类型',
|
||||
'模板奖励',
|
||||
'状态',
|
||||
'使用者',
|
||||
'使用时间',
|
||||
'备注'
|
||||
]);
|
||||
foreach ($codes as $code) {
|
||||
$expireDate = $code->expires_at ? date('Y-m-d H:i:s', $code->expires_at) : '长期有效';
|
||||
$createDate = date('Y-m-d H:i:s', $code->created_at);
|
||||
$templateName = $template->name ?? '';
|
||||
$templateType = $template->type ?? '';
|
||||
$templateRewards = $template->rewards ? json_encode($template->rewards, JSON_UNESCAPED_UNICODE) : '';
|
||||
// 状态判断
|
||||
$status = $code->status_name;
|
||||
$usedBy = $code->user_id ?? '';
|
||||
$usedAt = $code->used_at ? date('Y-m-d H:i:s', $code->used_at) : '';
|
||||
$remark = $code->remark ?? '';
|
||||
fputcsv($handle, [
|
||||
$code->code,
|
||||
$code->prefix ?? '',
|
||||
$expireDate,
|
||||
$code->max_usage,
|
||||
$code->batch_id,
|
||||
$createDate,
|
||||
$templateName,
|
||||
$templateType,
|
||||
$templateRewards,
|
||||
$status,
|
||||
$usedBy,
|
||||
$usedAt,
|
||||
$remark,
|
||||
]);
|
||||
}
|
||||
fclose($handle);
|
||||
};
|
||||
return response()->streamDownload($callback, 'gift_codes.csv', $headers);
|
||||
}
|
||||
|
||||
Log::info('批量生成兑换码', [
|
||||
'admin_id' => $request->user()->id,
|
||||
'template_id' => $request->input('template_id'),
|
||||
'count' => $request->input('count'),
|
||||
'batch_id' => $batchId,
|
||||
]);
|
||||
|
||||
return $this->success([
|
||||
'batch_id' => $batchId,
|
||||
'count' => $request->input('count'),
|
||||
'message' => '生成成功',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('生成兑换码失败', [
|
||||
'admin_id' => $request->user()->id,
|
||||
'data' => $request->all(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return $this->fail([500, '生成失败']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取兑换码列表
|
||||
*/
|
||||
public function codes(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'template_id' => 'integer|exists:v2_gift_card_template,id',
|
||||
'batch_id' => 'string',
|
||||
'status' => 'integer|in:0,1,2,3',
|
||||
'page' => 'integer|min:1',
|
||||
'per_page' => 'integer|min:1|max:500',
|
||||
]);
|
||||
|
||||
$query = GiftCardCode::with(['template', 'user']);
|
||||
|
||||
if ($request->has('template_id')) {
|
||||
$query->where('template_id', $request->input('template_id'));
|
||||
}
|
||||
|
||||
if ($request->has('batch_id')) {
|
||||
$query->where('batch_id', $request->input('batch_id'));
|
||||
}
|
||||
|
||||
if ($request->has('status')) {
|
||||
$query->where('status', $request->input('status'));
|
||||
}
|
||||
|
||||
$perPage = $request->input('per_page', 15);
|
||||
$codes = $query->orderBy('created_at', 'desc')->paginate($perPage);
|
||||
|
||||
$data = $codes->getCollection()->map(function ($code) {
|
||||
return [
|
||||
'id' => $code->id,
|
||||
'template_id' => $code->template_id,
|
||||
'template_name' => $code->template->name ?? '',
|
||||
'code' => $code->code,
|
||||
'batch_id' => $code->batch_id,
|
||||
'status' => $code->status,
|
||||
'status_name' => $code->status_name,
|
||||
'user_id' => $code->user_id,
|
||||
'user_email' => $code->user ? (substr($code->user->email ?? '', 0, 3) . '***@***') : null,
|
||||
'used_at' => $code->used_at,
|
||||
'expires_at' => $code->expires_at,
|
||||
'usage_count' => $code->usage_count,
|
||||
'max_usage' => $code->max_usage,
|
||||
'created_at' => $code->created_at,
|
||||
];
|
||||
})->values();
|
||||
|
||||
return $this->paginate($codes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用/启用兑换码
|
||||
*/
|
||||
public function toggleCode(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'required|integer|exists:v2_gift_card_code,id',
|
||||
'action' => 'required|string|in:disable,enable',
|
||||
]);
|
||||
|
||||
$code = GiftCardCode::find($request->input('id'));
|
||||
if (!$code) {
|
||||
return $this->fail([404, '兑换码不存在']);
|
||||
}
|
||||
|
||||
try {
|
||||
if ($request->input('action') === 'disable') {
|
||||
$code->markAsDisabled();
|
||||
} else {
|
||||
if ($code->status === GiftCardCode::STATUS_DISABLED) {
|
||||
$code->status = GiftCardCode::STATUS_UNUSED;
|
||||
$code->save();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => $request->input('action') === 'disable' ? '已禁用' : '已启用',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail([500, '操作失败']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出兑换码
|
||||
*/
|
||||
public function exportCodes(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'batch_id' => 'required|string|exists:v2_gift_card_code,batch_id',
|
||||
]);
|
||||
|
||||
$codes = GiftCardCode::where('batch_id', $request->input('batch_id'))
|
||||
->orderBy('created_at', 'asc')
|
||||
->get(['code']);
|
||||
|
||||
$content = $codes->pluck('code')->implode("\n");
|
||||
|
||||
return response($content)
|
||||
->header('Content-Type', 'text/plain')
|
||||
->header('Content-Disposition', 'attachment; filename="gift_cards_' . $request->input('batch_id') . '.txt"');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取使用记录
|
||||
*/
|
||||
public function usages(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'template_id' => 'integer|exists:v2_gift_card_template,id',
|
||||
'user_id' => 'integer|exists:v2_user,id',
|
||||
'page' => 'integer|min:1',
|
||||
'per_page' => 'integer|min:1|max:500',
|
||||
]);
|
||||
|
||||
$query = GiftCardUsage::with(['template', 'code', 'user', 'inviteUser']);
|
||||
|
||||
if ($request->has('template_id')) {
|
||||
$query->where('template_id', $request->input('template_id'));
|
||||
}
|
||||
|
||||
if ($request->has('user_id')) {
|
||||
$query->where('user_id', $request->input('user_id'));
|
||||
}
|
||||
|
||||
$perPage = $request->input('per_page', 15);
|
||||
$usages = $query->orderBy('created_at', 'desc')->paginate($perPage);
|
||||
|
||||
$usages->transform(function ($usage) {
|
||||
return [
|
||||
'id' => $usage->id,
|
||||
'code' => $usage->code->code ?? '',
|
||||
'template_name' => $usage->template->name ?? '',
|
||||
'user_email' => $usage->user->email ?? '',
|
||||
'invite_user_email' => $usage->inviteUser ? (substr($usage->inviteUser->email ?? '', 0, 3) . '***@***') : null,
|
||||
'rewards_given' => $usage->rewards_given,
|
||||
'invite_rewards' => $usage->invite_rewards,
|
||||
'multiplier_applied' => $usage->multiplier_applied,
|
||||
'created_at' => $usage->created_at,
|
||||
];
|
||||
})->values();
|
||||
return $this->paginate($usages);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计数据
|
||||
*/
|
||||
public function statistics(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'start_date' => 'date_format:Y-m-d',
|
||||
'end_date' => 'date_format:Y-m-d',
|
||||
]);
|
||||
|
||||
$startDate = $request->input('start_date', date('Y-m-d', strtotime('-30 days')));
|
||||
$endDate = $request->input('end_date', date('Y-m-d'));
|
||||
|
||||
// 总体统计
|
||||
$totalStats = [
|
||||
'templates_count' => GiftCardTemplate::count(),
|
||||
'active_templates_count' => GiftCardTemplate::where('status', 1)->count(),
|
||||
'codes_count' => GiftCardCode::count(),
|
||||
'used_codes_count' => GiftCardCode::where('status', GiftCardCode::STATUS_USED)->count(),
|
||||
'usages_count' => GiftCardUsage::count(),
|
||||
];
|
||||
|
||||
// 每日使用统计
|
||||
$driver = DB::connection()->getDriverName();
|
||||
$dateExpression = "date(created_at, 'unixepoch')"; // Default for SQLite
|
||||
if ($driver === 'mysql') {
|
||||
$dateExpression = 'DATE(FROM_UNIXTIME(created_at))';
|
||||
} elseif ($driver === 'pgsql') {
|
||||
$dateExpression = 'date(to_timestamp(created_at))';
|
||||
}
|
||||
|
||||
$dailyUsages = GiftCardUsage::selectRaw("{$dateExpression} as date, COUNT(*) as count")
|
||||
->whereRaw("{$dateExpression} BETWEEN ? AND ?", [$startDate, $endDate])
|
||||
->groupBy('date')
|
||||
->orderBy('date')
|
||||
->get();
|
||||
|
||||
// 类型统计
|
||||
$typeStats = GiftCardUsage::with('template')
|
||||
->selectRaw('template_id, COUNT(*) as count')
|
||||
->groupBy('template_id')
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'template_name' => $item->template->name ?? '',
|
||||
'type_name' => $item->template->type_name ?? '',
|
||||
'count' => $item->count ?? 0,
|
||||
];
|
||||
});
|
||||
|
||||
return $this->success([
|
||||
'total_stats' => $totalStats,
|
||||
'daily_usages' => $dailyUsages,
|
||||
'type_stats' => $typeStats,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用的礼品卡类型
|
||||
*/
|
||||
public function types()
|
||||
{
|
||||
return $this->success(GiftCardTemplate::getTypeMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单个兑换码
|
||||
*/
|
||||
public function updateCode(Request $request)
|
||||
{
|
||||
$validatedData = $request->validate([
|
||||
'id' => 'required|integer|exists:v2_gift_card_code,id',
|
||||
'expires_at' => 'sometimes|nullable|integer',
|
||||
'max_usage' => 'sometimes|integer|min:1|max:1000',
|
||||
'status' => 'sometimes|integer|in:0,1,2,3',
|
||||
]);
|
||||
|
||||
$code = GiftCardCode::find($validatedData['id']);
|
||||
if (!$code) {
|
||||
return $this->fail([404, '礼品卡不存在']);
|
||||
}
|
||||
|
||||
try {
|
||||
$updateData = collect($validatedData)->except('id')->all();
|
||||
|
||||
if (empty($updateData)) {
|
||||
return $this->success($code);
|
||||
}
|
||||
|
||||
$updateData['updated_at'] = time();
|
||||
$code->update($updateData);
|
||||
|
||||
return $this->success($code->fresh());
|
||||
} catch (\Exception $e) {
|
||||
Log::error('更新礼品卡信息失败', [
|
||||
'admin_id' => $request->user()->id,
|
||||
'code_id' => $code->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return $this->fail([500, '更新失败']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除礼品卡
|
||||
*/
|
||||
public function deleteCode(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'required|integer|exists:v2_gift_card_code,id',
|
||||
]);
|
||||
|
||||
$code = GiftCardCode::find($request->input('id'));
|
||||
if (!$code) {
|
||||
return $this->fail([404, '礼品卡不存在']);
|
||||
}
|
||||
|
||||
// 检查是否已被使用
|
||||
if ($code->status === GiftCardCode::STATUS_USED) {
|
||||
return $this->fail([400, '该礼品卡已被使用,无法删除']);
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查是否有关联的使用记录
|
||||
if ($code->usages()->exists()) {
|
||||
return $this->fail([400, '该礼品卡存在使用记录,无法删除']);
|
||||
}
|
||||
|
||||
$code->delete();
|
||||
return $this->success(['message' => '删除成功']);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('删除礼品卡失败', [
|
||||
'admin_id' => $request->user()->id,
|
||||
'code_id' => $code->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return $this->fail([500, '删除失败']);
|
||||
}
|
||||
}
|
||||
}
|
||||
113
Xboard/app/Http/Controllers/V2/Admin/KnowledgeController.php
Normal file
113
Xboard/app/Http/Controllers/V2/Admin/KnowledgeController.php
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
101
Xboard/app/Http/Controllers/V2/Admin/NoticeController.php
Normal file
101
Xboard/app/Http/Controllers/V2/Admin/NoticeController.php
Normal file
@@ -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, '排序保存失败']);
|
||||
}
|
||||
}
|
||||
}
|
||||
252
Xboard/app/Http/Controllers/V2/Admin/OrderController.php
Normal file
252
Xboard/app/Http/Controllers/V2/Admin/OrderController.php
Normal file
@@ -0,0 +1,252 @@
|
||||
<?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;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class OrderController extends Controller
|
||||
{
|
||||
|
||||
public function detail(Request $request)
|
||||
{
|
||||
$order = Order::with(['user', 'plan', 'commission_log', 'invite_user'])->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((string) $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);
|
||||
|
||||
/** @var \Illuminate\Pagination\LengthAwarePaginator $paginatedResults */
|
||||
$paginatedResults = $orderModel
|
||||
->latest('created_at')
|
||||
->paginate(
|
||||
perPage: $pageSize,
|
||||
page: $current
|
||||
);
|
||||
|
||||
$paginatedResults->getCollection()->transform(function ($order) {
|
||||
$orderArray = $order->toArray();
|
||||
$orderArray['period'] = PlanService::getLegacyPeriod((string) $order->period);
|
||||
return $orderArray;
|
||||
});
|
||||
|
||||
return $this->paginate($paginatedResults);
|
||||
}
|
||||
|
||||
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($field),
|
||||
'notnull' => static fn($q) => $q->whereNotNull($field),
|
||||
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::byEmail($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;
|
||||
$period = $request->input('period');
|
||||
$order->period = PlanService::getPeriodKey((string) $period);
|
||||
$order->trade_no = Helper::guid();
|
||||
$order->total_amount = $request->input('total_amount');
|
||||
|
||||
if (PlanService::getPeriodKey((string) $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);
|
||||
}
|
||||
}
|
||||
133
Xboard/app/Http/Controllers/V2/Admin/PaymentController.php
Normal file
133
Xboard/app/Http/Controllers/V2/Admin/PaymentController.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?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;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PaymentController extends Controller
|
||||
{
|
||||
public function getPaymentMethods()
|
||||
{
|
||||
$methods = [];
|
||||
|
||||
$pluginMethods = PaymentService::getAllPaymentMethodNames();
|
||||
$methods = array_merge($methods, $pluginMethods);
|
||||
|
||||
return $this->success(array_unique($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)
|
||||
{
|
||||
try {
|
||||
$paymentService = new PaymentService($request->input('payment'), $request->input('id'));
|
||||
return $this->success(collect($paymentService->form()));
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail([400, '支付方式不存在或未启用']);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
132
Xboard/app/Http/Controllers/V2/Admin/PlanController.php
Normal file
132
Xboard/app/Http/Controllers/V2/Admin/PlanController.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V2\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\PlanSave;
|
||||
use App\Models\Order;
|
||||
use App\Models\Plan;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PlanController extends Controller
|
||||
{
|
||||
public function fetch(Request $request)
|
||||
{
|
||||
$plans = Plan::orderBy('sort', 'ASC')
|
||||
->with([
|
||||
'group:id,name'
|
||||
])
|
||||
->withCount([
|
||||
'users',
|
||||
'users as active_users_count' => function ($query) {
|
||||
$query->where(function ($q) {
|
||||
$q->where('expired_at', '>', time())
|
||||
->orWhereNull('expired_at');
|
||||
});
|
||||
}
|
||||
])
|
||||
->get();
|
||||
|
||||
return $this->success($plans);
|
||||
}
|
||||
|
||||
public function save(PlanSave $request)
|
||||
{
|
||||
$params = $request->validated();
|
||||
|
||||
if ($request->input('id')) {
|
||||
$plan = Plan::find($request->input('id'));
|
||||
if (!$plan) {
|
||||
return $this->fail([400202, '该订阅不存在']);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
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, '该订阅下存在用户无法删除']);
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
333
Xboard/app/Http/Controllers/V2/Admin/PluginController.php
Normal file
333
Xboard/app/Http/Controllers/V2/Admin/PluginController.php
Normal file
@@ -0,0 +1,333 @@
|
||||
<?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;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
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 types()
|
||||
{
|
||||
return response()->json([
|
||||
'data' => [
|
||||
[
|
||||
'value' => Plugin::TYPE_FEATURE,
|
||||
'label' => '功能',
|
||||
'description' => '提供功能扩展的插件,如Telegram登录、邮件通知等',
|
||||
'icon' => '🔧'
|
||||
],
|
||||
[
|
||||
'value' => Plugin::TYPE_PAYMENT,
|
||||
'label' => '支付方式',
|
||||
'description' => '提供支付接口的插件,如支付宝、微信支付等',
|
||||
'icon' => '💳'
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件列表
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$type = $request->query('type');
|
||||
|
||||
$installedPlugins = Plugin::when($type, function ($query) use ($type) {
|
||||
return $query->byType($type);
|
||||
})
|
||||
->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);
|
||||
$code = $config['code'];
|
||||
$pluginType = $config['type'] ?? Plugin::TYPE_FEATURE;
|
||||
|
||||
// 如果指定了类型,过滤插件
|
||||
if ($type && $pluginType !== $type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$installed = isset($installedPlugins[$code]);
|
||||
$pluginConfig = $installed ? $this->configService->getConfig($code) : ($config['config'] ?? []);
|
||||
$readmeFile = collect(['README.md', 'readme.md'])
|
||||
->map(fn($f) => $directory . '/' . $f)
|
||||
->first(fn($path) => File::exists($path));
|
||||
$readmeContent = $readmeFile ? File::get($readmeFile) : '';
|
||||
$needUpgrade = false;
|
||||
if ($installed) {
|
||||
$installedVersion = $installedPlugins[$code]['version'] ?? null;
|
||||
$localVersion = $config['version'] ?? null;
|
||||
if ($installedVersion && $localVersion && version_compare($localVersion, $installedVersion, '>')) {
|
||||
$needUpgrade = true;
|
||||
}
|
||||
}
|
||||
$plugins[] = [
|
||||
'code' => $config['code'],
|
||||
'name' => $config['name'],
|
||||
'version' => $config['version'],
|
||||
'description' => $config['description'],
|
||||
'author' => $config['author'],
|
||||
'type' => $pluginType,
|
||||
'is_installed' => $installed,
|
||||
'is_enabled' => $installed ? $installedPlugins[$code]['is_enabled'] : false,
|
||||
'is_protected' => in_array($code, Plugin::PROTECTED_PLUGINS),
|
||||
'can_be_deleted' => !in_array($code, Plugin::PROTECTED_PLUGINS),
|
||||
'config' => $pluginConfig,
|
||||
'readme' => $readmeContent,
|
||||
'need_upgrade' => $needUpgrade,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
]);
|
||||
|
||||
$code = $request->input('code');
|
||||
$plugin = Plugin::where('code', $code)->first();
|
||||
if ($plugin && $plugin->is_enabled) {
|
||||
return response()->json([
|
||||
'message' => '请先禁用插件后再卸载'
|
||||
], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->pluginManager->uninstall($code);
|
||||
return response()->json([
|
||||
'message' => '插件卸载成功'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'message' => '插件卸载失败:' . $e->getMessage()
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 升级插件
|
||||
*/
|
||||
public function upgrade(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'code' => 'required|string',
|
||||
]);
|
||||
try {
|
||||
$this->pluginManager->update($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'
|
||||
]);
|
||||
|
||||
$this->pluginManager->disable($request->input('code'));
|
||||
return response()->json([
|
||||
'message' => '插件禁用成功'
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件配置
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传插件
|
||||
*/
|
||||
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 {
|
||||
$this->pluginManager->upload($request->file('file'));
|
||||
return response()->json([
|
||||
'message' => '插件上传成功'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'message' => '插件上传失败:' . $e->getMessage()
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除插件
|
||||
*/
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'code' => 'required|string'
|
||||
]);
|
||||
|
||||
$code = $request->input('code');
|
||||
|
||||
// 检查是否为受保护的插件
|
||||
if (in_array($code, Plugin::PROTECTED_PLUGINS)) {
|
||||
return response()->json([
|
||||
'message' => '该插件为系统默认插件,不允许删除'
|
||||
], 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->pluginManager->delete($code);
|
||||
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();
|
||||
|
||||
// 只在需要时手动加载server_count
|
||||
$serverGroups->each(function ($group) {
|
||||
$group->setAttribute('server_count', $group->server_count);
|
||||
});
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
219
Xboard/app/Http/Controllers/V2/Admin/Server/ManageController.php
Normal file
219
Xboard/app/Http/Controllers/V2/Admin/Server/ManageController.php
Normal file
@@ -0,0 +1,219 @@
|
||||
<?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;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ManageController extends Controller
|
||||
{
|
||||
public function getNodes(Request $request)
|
||||
{
|
||||
$servers = 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',
|
||||
]);
|
||||
|
||||
$server = Server::find($request->id);
|
||||
if (!$server) {
|
||||
return $this->fail([400202, '服务器不存在']);
|
||||
}
|
||||
$server->show = (int) $request->show;
|
||||
if (!$server->save()) {
|
||||
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 batchDelete(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'ids' => 'required|array',
|
||||
'ids.*' => 'integer',
|
||||
]);
|
||||
|
||||
$ids = $request->input('ids');
|
||||
if (empty($ids)) {
|
||||
return $this->fail([400, '请选择要删除的节点']);
|
||||
}
|
||||
|
||||
try {
|
||||
$deleted = Server::whereIn('id', $ids)->delete();
|
||||
if ($deleted === false) {
|
||||
return $this->fail([500, '批量删除失败']);
|
||||
}
|
||||
return $this->success(true);
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
return $this->fail([500, '批量删除失败']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置节点流量
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function resetTraffic(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'required|integer',
|
||||
]);
|
||||
|
||||
$server = Server::find($request->id);
|
||||
if (!$server) {
|
||||
return $this->fail([400202, '服务器不存在']);
|
||||
}
|
||||
|
||||
try {
|
||||
$server->u = 0;
|
||||
$server->d = 0;
|
||||
$server->save();
|
||||
|
||||
Log::info("Server {$server->id} ({$server->name}) traffic reset by admin");
|
||||
return $this->success(true);
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
return $this->fail([500, '重置失败']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量重置节点流量
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function batchResetTraffic(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'ids' => 'required|array',
|
||||
'ids.*' => 'integer',
|
||||
]);
|
||||
|
||||
$ids = $request->input('ids');
|
||||
if (empty($ids)) {
|
||||
return $this->fail([400, '请选择要重置的节点']);
|
||||
}
|
||||
|
||||
try {
|
||||
Server::whereIn('id', $ids)->update([
|
||||
'u' => 0,
|
||||
'd' => 0,
|
||||
]);
|
||||
|
||||
Log::info("Servers " . implode(',', $ids) . " traffic reset by admin");
|
||||
return $this->success(true);
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
return $this->fail([500, '批量重置失败']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制节点
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function copy(Request $request)
|
||||
{
|
||||
$server = Server::find($request->input('id'));
|
||||
if (!$server) {
|
||||
return $this->fail([400202, '服务器不存在']);
|
||||
}
|
||||
$server->show = 0;
|
||||
$server->code = null;
|
||||
Server::create($server->toArray());
|
||||
return $this->success(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?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;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class RouteController extends Controller
|
||||
{
|
||||
public function fetch(Request $request)
|
||||
{
|
||||
$routes = ServerRoute::get();
|
||||
return [
|
||||
'data' => $routes
|
||||
];
|
||||
}
|
||||
|
||||
public function save(Request $request)
|
||||
{
|
||||
$params = $request->validate([
|
||||
'remarks' => 'required',
|
||||
'match' => 'required|array',
|
||||
'action' => 'required|in:block,direct,dns,proxy',
|
||||
'action_value' => 'nullable'
|
||||
], [
|
||||
'remarks.required' => '备注不能为空',
|
||||
'match.required' => '匹配值不能为空',
|
||||
'action.required' => '动作类型不能为空',
|
||||
'action.in' => '动作类型参数有误'
|
||||
]);
|
||||
$params['match'] = array_filter($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
|
||||
];
|
||||
}
|
||||
}
|
||||
508
Xboard/app/Http/Controllers/V2/Admin/StatController.php
Normal file
508
Xboard/app/Http/Controllers/V2/Admin/StatController.php
Normal file
@@ -0,0 +1,508 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V2\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\CommissionLog;
|
||||
use App\Models\Order;
|
||||
use App\Models\Server;
|
||||
use App\Models\Stat;
|
||||
use App\Models\StatServer;
|
||||
use App\Models\StatUser;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\User;
|
||||
use App\Services\StatisticalService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class StatController extends Controller
|
||||
{
|
||||
private $service;
|
||||
public function __construct(StatisticalService $service)
|
||||
{
|
||||
$this->service = $service;
|
||||
}
|
||||
public function getOverride(Request $request)
|
||||
{
|
||||
// 获取在线节点数
|
||||
$onlineNodes = Server::all()->filter(function ($server) {
|
||||
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',
|
||||
]);
|
||||
|
||||
$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) {
|
||||
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()
|
||||
->keyBy('id');
|
||||
|
||||
} else {
|
||||
// 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 = [];
|
||||
$ids = $currentData->pluck('id');
|
||||
$names = $type === 'node'
|
||||
? Server::whereIn('id', $ids)->pluck('name', 'id')
|
||||
: User::whereIn('id', $ids)->pluck('email', 'id');
|
||||
|
||||
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;
|
||||
|
||||
$result[] = [
|
||||
'id' => (string) $data->id,
|
||||
'name' => $names[$data->id] ?? ($type === 'node' ? "Node {$data->id}" : "User {$data->id}"),
|
||||
'value' => $data->value,
|
||||
'previousValue' => $previousValue,
|
||||
'change' => $change,
|
||||
'timestamp' => date('c', $endDate)
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'timestamp' => date('c'),
|
||||
'data' => $result
|
||||
];
|
||||
}
|
||||
}
|
||||
144
Xboard/app/Http/Controllers/V2/Admin/SystemController.php
Normal file
144
Xboard/app/Http/Controllers/V2/Admin/SystemController.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V2\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\AdminAuditLog;
|
||||
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;
|
||||
use App\Helpers\ResponseEnum;
|
||||
|
||||
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 getAuditLog(Request $request)
|
||||
{
|
||||
$current = max(1, (int) $request->input('current', 1));
|
||||
$pageSize = max(10, (int) $request->input('page_size', 10));
|
||||
|
||||
$builder = AdminAuditLog::with('admin:id,email')
|
||||
->orderBy('id', 'DESC')
|
||||
->when($request->input('action'), fn($q, $v) => $q->where('action', $v))
|
||||
->when($request->input('admin_id'), fn($q, $v) => $q->where('admin_id', $v))
|
||||
->when($request->input('keyword'), function ($q, $keyword) {
|
||||
$q->where(function ($q) use ($keyword) {
|
||||
$q->where('uri', 'like', '%' . $keyword . '%')
|
||||
->orWhere('request_data', 'like', '%' . $keyword . '%');
|
||||
});
|
||||
});
|
||||
|
||||
$total = $builder->count();
|
||||
$res = $builder->forPage($current, $pageSize)->get();
|
||||
|
||||
return response(['data' => $res, 'total' => $total]);
|
||||
}
|
||||
|
||||
public function getHorizonFailedJobs(Request $request, JobRepository $jobRepository)
|
||||
{
|
||||
$current = max(1, (int) $request->input('current', 1));
|
||||
$pageSize = max(10, (int) $request->input('page_size', 20));
|
||||
$offset = ($current - 1) * $pageSize;
|
||||
|
||||
$failedJobs = collect($jobRepository->getFailed())
|
||||
->sortByDesc('failed_at')
|
||||
->slice($offset, $pageSize)
|
||||
->values();
|
||||
|
||||
$total = $jobRepository->countFailed();
|
||||
|
||||
return response()->json([
|
||||
'data' => $failedJobs,
|
||||
'total' => $total,
|
||||
'current' => $current,
|
||||
'page_size' => $pageSize,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
150
Xboard/app/Http/Controllers/V2/Admin/ThemeController.php
Normal file
150
Xboard/app/Http/Controllers/V2/Admin/ThemeController.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?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;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
156
Xboard/app/Http/Controllers/V2/Admin/TicketController.php
Normal file
156
Xboard/app/Http/Controllers/V2/Admin/TicketController.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?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, '工单不存在']);
|
||||
}
|
||||
$result = $ticket->toArray();
|
||||
$result['user'] = UserController::transformUserData($ticket->user);
|
||||
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary of fetchTickets
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response
|
||||
*/
|
||||
private function fetchTickets(Request $request)
|
||||
{
|
||||
$ticketModel = Ticket::with('user')
|
||||
->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);
|
||||
$tickets = $ticketModel
|
||||
->latest('updated_at')
|
||||
->paginate(
|
||||
perPage: $request->integer('pageSize', 10),
|
||||
page: $request->integer('current', 1)
|
||||
);
|
||||
|
||||
// 获取items然后映射转换
|
||||
$items = collect($tickets->items())->map(function ($ticket) {
|
||||
$ticketData = $ticket->toArray();
|
||||
$ticketData['user'] = UserController::transformUserData($ticket->user);
|
||||
return $ticketData;
|
||||
})->all();
|
||||
|
||||
return response([
|
||||
'data' => $items,
|
||||
'total' => $tickets->total()
|
||||
]);
|
||||
}
|
||||
|
||||
public function reply(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'required|numeric',
|
||||
'message' => 'required|string'
|
||||
], [
|
||||
'id.required' => '工单ID不能为空',
|
||||
'message.required' => '消息不能为空'
|
||||
]);
|
||||
$ticketService = new TicketService();
|
||||
$ticketService->replyByAdmin(
|
||||
$request->input('id'),
|
||||
$request->input('message'),
|
||||
$request->user()->id
|
||||
);
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
public function close(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'required|numeric'
|
||||
], [
|
||||
'id.required' => '工单ID不能为空'
|
||||
]);
|
||||
try {
|
||||
$ticket = Ticket::findOrFail($request->input('id'));
|
||||
$ticket->status = Ticket::STATUS_CLOSED;
|
||||
$ticket->save();
|
||||
return $this->success(true);
|
||||
} catch (ModelNotFoundException $e) {
|
||||
return $this->fail([400202, '工单不存在']);
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail([500101, '关闭失败']);
|
||||
}
|
||||
}
|
||||
|
||||
public function show($ticketId)
|
||||
{
|
||||
$ticket = Ticket::with([
|
||||
'user',
|
||||
'messages' => function ($query) {
|
||||
$query->with(['user']); // 如果需要用户信息
|
||||
}
|
||||
])->findOrFail($ticketId);
|
||||
|
||||
// 自动包含 is_me 属性
|
||||
return response()->json([
|
||||
'data' => $ticket
|
||||
]);
|
||||
}
|
||||
}
|
||||
235
Xboard/app/Http/Controllers/V2/Admin/TrafficResetController.php
Normal file
235
Xboard/app/Http/Controllers/V2/Admin/TrafficResetController.php
Normal file
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V2\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Models\TrafficResetLog;
|
||||
use App\Services\TrafficResetService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 流量重置管理控制器
|
||||
*/
|
||||
class TrafficResetController extends Controller
|
||||
{
|
||||
private TrafficResetService $trafficResetService;
|
||||
|
||||
public function __construct(TrafficResetService $trafficResetService)
|
||||
{
|
||||
$this->trafficResetService = $trafficResetService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流量重置日志列表
|
||||
*/
|
||||
public function logs(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'user_id' => 'nullable|integer',
|
||||
'user_email' => 'nullable|string',
|
||||
'reset_type' => 'nullable|string|in:' . implode(',', array_keys(TrafficResetLog::getResetTypeNames())),
|
||||
'trigger_source' => 'nullable|string|in:' . implode(',', array_keys(TrafficResetLog::getSourceNames())),
|
||||
'start_date' => 'nullable|date',
|
||||
'end_date' => 'nullable|date|after_or_equal:start_date',
|
||||
'per_page' => 'nullable|integer|min:1|max:10000',
|
||||
'page' => 'nullable|integer|min:1',
|
||||
]);
|
||||
|
||||
$query = TrafficResetLog::with(['user:id,email'])
|
||||
->orderBy('reset_time', 'desc');
|
||||
|
||||
// 筛选条件
|
||||
if ($request->filled('user_id')) {
|
||||
$query->where('user_id', $request->user_id);
|
||||
}
|
||||
|
||||
if ($request->filled('user_email')) {
|
||||
$query->whereHas('user', function ($query) use ($request) {
|
||||
$query->where('email', 'like', '%' . $request->user_email . '%');
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('reset_type')) {
|
||||
$query->where('reset_type', $request->reset_type);
|
||||
}
|
||||
|
||||
if ($request->filled('trigger_source')) {
|
||||
$query->where('trigger_source', $request->trigger_source);
|
||||
}
|
||||
|
||||
if ($request->filled('start_date')) {
|
||||
$query->where('reset_time', '>=', $request->start_date);
|
||||
}
|
||||
|
||||
if ($request->filled('end_date')) {
|
||||
$query->where('reset_time', '<=', $request->end_date . ' 23:59:59');
|
||||
}
|
||||
|
||||
$perPage = $request->get('per_page', 20);
|
||||
$logs = $query->paginate($perPage);
|
||||
|
||||
// 格式化数据
|
||||
$formattedLogs = $logs->getCollection()->map(function (TrafficResetLog $log) {
|
||||
return [
|
||||
'id' => $log->id,
|
||||
'user_id' => $log->user_id,
|
||||
'user_email' => $log->user->email ?? 'N/A',
|
||||
'reset_type' => $log->reset_type,
|
||||
'reset_type_name' => $log->getResetTypeName(),
|
||||
'reset_time' => $log->reset_time,
|
||||
'old_traffic' => [
|
||||
'upload' => $log->old_upload,
|
||||
'download' => $log->old_download,
|
||||
'total' => $log->old_total,
|
||||
'formatted' => $log->formatTraffic($log->old_total),
|
||||
],
|
||||
'new_traffic' => [
|
||||
'upload' => $log->new_upload,
|
||||
'download' => $log->new_download,
|
||||
'total' => $log->new_total,
|
||||
'formatted' => $log->formatTraffic($log->new_total),
|
||||
],
|
||||
'trigger_source' => $log->trigger_source,
|
||||
'trigger_source_name' => $log->getSourceName(),
|
||||
'metadata' => $log->metadata,
|
||||
'created_at' => $log->created_at,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'data' => $formattedLogs->toArray(),
|
||||
'pagination' => [
|
||||
'current_page' => $logs->currentPage(),
|
||||
'last_page' => $logs->lastPage(),
|
||||
'per_page' => $logs->perPage(),
|
||||
'total' => $logs->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流量重置统计信息
|
||||
*/
|
||||
public function stats(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'days' => 'nullable|integer|min:1|max:365',
|
||||
]);
|
||||
|
||||
$days = $request->get('days', 30);
|
||||
$startDate = now()->subDays($days)->startOfDay();
|
||||
|
||||
$stats = [
|
||||
'total_resets' => TrafficResetLog::where('reset_time', '>=', $startDate)->count(),
|
||||
'auto_resets' => TrafficResetLog::where('reset_time', '>=', $startDate)
|
||||
->where('trigger_source', TrafficResetLog::SOURCE_AUTO)
|
||||
->count(),
|
||||
'manual_resets' => TrafficResetLog::where('reset_time', '>=', $startDate)
|
||||
->where('trigger_source', TrafficResetLog::SOURCE_MANUAL)
|
||||
->count(),
|
||||
'cron_resets' => TrafficResetLog::where('reset_time', '>=', $startDate)
|
||||
->where('trigger_source', TrafficResetLog::SOURCE_CRON)
|
||||
->count(),
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
'data' => $stats
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动重置用户流量
|
||||
*/
|
||||
public function resetUser(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'user_id' => 'required|integer|exists:v2_user,id',
|
||||
'reason' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$user = User::find($request->user_id);
|
||||
|
||||
if (!$this->trafficResetService->canReset($user)) {
|
||||
return response()->json([
|
||||
'message' => __('traffic_reset.user_cannot_reset')
|
||||
], 400);
|
||||
}
|
||||
|
||||
$metadata = [];
|
||||
if ($request->filled('reason')) {
|
||||
$metadata['reason'] = $request->reason;
|
||||
$metadata['admin_id'] = auth()->user()?->id;
|
||||
}
|
||||
|
||||
$success = $this->trafficResetService->manualReset($user, $metadata);
|
||||
|
||||
if (!$success) {
|
||||
return response()->json([
|
||||
'message' => __('traffic_reset.reset_failed')
|
||||
], 500);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => __('traffic_reset.reset_success'),
|
||||
'data' => [
|
||||
'user_id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'reset_time' => now(),
|
||||
'next_reset_at' => $user->fresh()->next_reset_at,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取用户重置历史
|
||||
*/
|
||||
public function userHistory(Request $request, int $userId): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'limit' => 'nullable|integer|min:1|max:50',
|
||||
]);
|
||||
|
||||
$user = User::findOrFail($userId);
|
||||
$limit = $request->get('limit', 10);
|
||||
|
||||
$history = $this->trafficResetService->getUserResetHistory($user, $limit);
|
||||
|
||||
/** @var \Illuminate\Database\Eloquent\Collection<int, \App\Models\TrafficResetLog> $history */
|
||||
$data = $history->map(function (TrafficResetLog $log) {
|
||||
return [
|
||||
'id' => $log->id,
|
||||
'reset_type' => $log->reset_type,
|
||||
'reset_type_name' => $log->getResetTypeName(),
|
||||
'reset_time' => $log->reset_time,
|
||||
'old_traffic' => [
|
||||
'upload' => $log->old_upload,
|
||||
'download' => $log->old_download,
|
||||
'total' => $log->old_total,
|
||||
'formatted' => $log->formatTraffic($log->old_total),
|
||||
],
|
||||
'trigger_source' => $log->trigger_source,
|
||||
'trigger_source_name' => $log->getSourceName(),
|
||||
'metadata' => $log->metadata,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
"data" => [
|
||||
'user' => [
|
||||
'id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'reset_count' => $user->reset_count,
|
||||
'last_reset_at' => $user->last_reset_at,
|
||||
'next_reset_at' => $user->next_reset_at,
|
||||
],
|
||||
'history' => $data,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
28
Xboard/app/Http/Controllers/V2/Admin/UpdateController.php
Normal file
28
Xboard/app/Http/Controllers/V2/Admin/UpdateController.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V2\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\UpdateService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UpdateController extends Controller
|
||||
{
|
||||
protected $updateService;
|
||||
|
||||
public function __construct(UpdateService $updateService)
|
||||
{
|
||||
$this->updateService = $updateService;
|
||||
}
|
||||
|
||||
public function checkUpdate()
|
||||
{
|
||||
return $this->success($this->updateService->checkForUpdates());
|
||||
}
|
||||
|
||||
public function executeUpdate()
|
||||
{
|
||||
$result = $this->updateService->executeUpdate();
|
||||
return $result['success'] ? $this->success($result) : $this->fail([500, $result['message']]);
|
||||
}
|
||||
}
|
||||
682
Xboard/app/Http/Controllers/V2/Admin/UserController.php
Normal file
682
Xboard/app/Http/Controllers/V2/Admin/UserController.php
Normal file
@@ -0,0 +1,682 @@
|
||||
<?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\Services\NodeSyncService;
|
||||
use App\Services\UserService;
|
||||
use App\Traits\QueryOperators;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
use QueryOperators;
|
||||
|
||||
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.
|
||||
private function applyFiltersAndSorts(Request $request, Builder|QueryBuilder $builder): void
|
||||
{
|
||||
$this->applyFilters($request, $builder);
|
||||
$this->applySorting($request, $builder);
|
||||
}
|
||||
|
||||
// Apply filters to the query builder.
|
||||
private function applyFilters(Request $request, Builder|QueryBuilder $builder): void
|
||||
{
|
||||
if (!$request->has('filter')) {
|
||||
return;
|
||||
}
|
||||
|
||||
collect($request->input('filter'))->each(function ($filter) use ($builder) {
|
||||
$field = $filter['id'];
|
||||
$value = $filter['value'];
|
||||
$logic = strtolower($filter['logic'] ?? 'and');
|
||||
|
||||
if ($logic === 'or') {
|
||||
$builder->orWhere(function ($query) use ($field, $value) {
|
||||
$this->buildFilterQuery($query, $field, $value);
|
||||
});
|
||||
} else {
|
||||
$builder->where(function ($query) use ($field, $value) {
|
||||
$this->buildFilterQuery($query, $field, $value);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Build one filter query condition.
|
||||
private function buildFilterQuery(Builder|QueryBuilder $query, string $field, mixed $value): void
|
||||
{
|
||||
// 处理关联查询
|
||||
if (str_contains($field, '.')) {
|
||||
if (!method_exists($query, 'whereHas')) {
|
||||
return;
|
||||
}
|
||||
[$relation, $relationField] = explode('.', $field);
|
||||
$query->whereHas($relation, function ($q) use ($relationField, $value) {
|
||||
if (is_array($value)) {
|
||||
$q->whereIn($relationField, $value);
|
||||
} else if (is_string($value) && str_contains($value, ':')) {
|
||||
[$operator, $filterValue] = explode(':', $value, 2);
|
||||
$this->applyQueryCondition($q, $relationField, $operator, $filterValue);
|
||||
} else {
|
||||
$q->where($relationField, 'like', "%{$value}%");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理数组值的 'in' 操作
|
||||
if (is_array($value)) {
|
||||
$query->whereIn($field === 'group_ids' ? 'group_id' : $field, $value);
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理基于运算符的过滤
|
||||
if (!is_string($value) || !str_contains($value, ':')) {
|
||||
$query->where($field, 'like', "%{$value}%");
|
||||
return;
|
||||
}
|
||||
|
||||
[$operator, $filterValue] = explode(':', $value, 2);
|
||||
|
||||
// 转换数字字符串为适当的类型
|
||||
if (is_numeric($filterValue)) {
|
||||
$filterValue = strpos($filterValue, '.') !== false
|
||||
? (float) $filterValue
|
||||
: (int) $filterValue;
|
||||
}
|
||||
|
||||
// 处理计算字段
|
||||
$queryField = match ($field) {
|
||||
'total_used' => DB::raw('(u + d)'),
|
||||
default => $field
|
||||
};
|
||||
|
||||
$this->applyQueryCondition($query, $queryField, $operator, $filterValue);
|
||||
}
|
||||
|
||||
// Apply sorting rules to the query builder.
|
||||
private function applySorting(Request $request, Builder|QueryBuilder $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);
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve bulk operation scope and normalize user_ids.
|
||||
private function resolveScope(Request $request): array
|
||||
{
|
||||
$scope = $request->input('scope');
|
||||
$userIds = $request->input('user_ids');
|
||||
|
||||
$hasSelection = is_array($userIds) && count(array_filter($userIds, static fn($v) => is_numeric($v))) > 0;
|
||||
$hasFilter = $request->has('filter') && !empty($request->input('filter'));
|
||||
|
||||
if (!in_array($scope, ['selected', 'filtered', 'all'], true)) {
|
||||
if ($hasSelection) {
|
||||
$scope = 'selected';
|
||||
} elseif ($hasFilter) {
|
||||
$scope = 'filtered';
|
||||
} else {
|
||||
$scope = 'all';
|
||||
}
|
||||
}
|
||||
|
||||
$normalizedIds = [];
|
||||
if ($scope === 'selected') {
|
||||
$normalizedIds = is_array($userIds) ? $userIds : [];
|
||||
$normalizedIds = array_values(array_unique(array_map(static function ($v) {
|
||||
return is_numeric($v) ? (int) $v : null;
|
||||
}, $normalizedIds)));
|
||||
$normalizedIds = array_values(array_filter($normalizedIds, static fn($v) => is_int($v)));
|
||||
}
|
||||
|
||||
return [
|
||||
'scope' => $scope,
|
||||
'user_ids' => $normalizedIds,
|
||||
];
|
||||
}
|
||||
|
||||
// Fetch paginated user list (filters + sorting).
|
||||
public function fetch(Request $request)
|
||||
{
|
||||
$current = $request->input('current', 1);
|
||||
$pageSize = $request->input('pageSize', 10);
|
||||
|
||||
$userModel = User::query()
|
||||
->with(['plan:id,name', 'invite_user:id,email', 'group:id,name'])
|
||||
->select((new User())->getTable() . '.*')
|
||||
->selectRaw('(u + d) as total_used');
|
||||
|
||||
$this->applyFiltersAndSorts($request, $userModel);
|
||||
|
||||
$users = $userModel->orderBy('id', 'desc')
|
||||
->paginate($pageSize, ['*'], 'page', $current);
|
||||
|
||||
$users->getCollection()->transform(function ($user): array {
|
||||
return self::transformUserData($user);
|
||||
});
|
||||
|
||||
return $this->paginate($users);
|
||||
}
|
||||
|
||||
// Transform user fields for API response.
|
||||
public static function transformUserData(User $user): array
|
||||
{
|
||||
$user = $user->toArray();
|
||||
$user['balance'] = $user['balance'] / 100;
|
||||
$user['commission_balance'] = $user['commission_balance'] / 100;
|
||||
$user['subscribe_url'] = Helper::getSubscribeUrl($user['token']);
|
||||
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 (isset($params['email'])) {
|
||||
if (User::byEmail($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, '订阅计划不存在']);
|
||||
}
|
||||
$params['group_id'] = $plan->group_id;
|
||||
}
|
||||
// 处理邀请用户
|
||||
if ($request->input('invite_user_email') && $inviteUser = User::byEmail($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->removeAllSessions();
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// Export users to CSV.
|
||||
public function dumpCSV(Request $request)
|
||||
{
|
||||
ini_set('memory_limit', '-1');
|
||||
gc_enable(); // 启用垃圾回收
|
||||
|
||||
$scopeInfo = $this->resolveScope($request);
|
||||
$scope = $scopeInfo['scope'];
|
||||
$userIds = $scopeInfo['user_ids'];
|
||||
|
||||
if ($scope === 'selected') {
|
||||
if (empty($userIds)) {
|
||||
return $this->fail([422, 'user_ids不能为空']);
|
||||
}
|
||||
}
|
||||
|
||||
// 优化查询:使用with预加载plan关系,避免N+1问题
|
||||
$query = User::query()
|
||||
->with('plan:id,name')
|
||||
->orderBy('id', 'asc')
|
||||
->select([
|
||||
'email',
|
||||
'balance',
|
||||
'commission_balance',
|
||||
'transfer_enable',
|
||||
'u',
|
||||
'd',
|
||||
'expired_at',
|
||||
'token',
|
||||
'plan_id'
|
||||
]);
|
||||
|
||||
if ($scope === 'selected') {
|
||||
$query->whereIn('id', $userIds);
|
||||
} elseif ($scope === 'filtered') {
|
||||
$this->applyFiltersAndSorts($request, $query);
|
||||
} // all: ignore filter/sort
|
||||
|
||||
$filename = 'users_' . date('Y-m-d_His') . '.csv';
|
||||
|
||||
return response()->streamDownload(function () use ($query) {
|
||||
// 打开输出流
|
||||
$output = fopen('php://output', 'w');
|
||||
|
||||
// 添加BOM标记,确保Excel正确显示中文
|
||||
fprintf($output, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||||
|
||||
// 写入CSV头部
|
||||
fputcsv($output, [
|
||||
'邮箱',
|
||||
'余额',
|
||||
'推广佣金',
|
||||
'总流量',
|
||||
'剩余流量',
|
||||
'套餐到期时间',
|
||||
'订阅计划',
|
||||
'订阅地址'
|
||||
]);
|
||||
|
||||
// 分批处理数据以减少内存使用
|
||||
$query->chunk(500, function ($users) use ($output) {
|
||||
foreach ($users as $user) {
|
||||
try {
|
||||
$row = [
|
||||
$user->email,
|
||||
number_format($user->balance / 100, 2),
|
||||
number_format($user->commission_balance / 100, 2),
|
||||
Helper::trafficConvert($user->transfer_enable),
|
||||
Helper::trafficConvert($user->transfer_enable - ($user->u + $user->d)),
|
||||
$user->expired_at ? date('Y-m-d H:i:s', $user->expired_at) : '长期有效',
|
||||
$user->plan ? $user->plan->name : '无订阅',
|
||||
Helper::getSubscribeUrl($user->token)
|
||||
];
|
||||
fputcsv($output, $row);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('CSV导出错误: ' . $e->getMessage(), [
|
||||
'user_id' => $user->id,
|
||||
'email' => $user->email
|
||||
]);
|
||||
continue; // 继续处理下一条记录
|
||||
}
|
||||
}
|
||||
|
||||
// 清理内存
|
||||
gc_collect_cycles();
|
||||
});
|
||||
|
||||
fclose($output);
|
||||
}, $filename, [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
'Content-Disposition' => 'attachment; filename="' . $filename . '"'
|
||||
]);
|
||||
}
|
||||
|
||||
public function generate(UserGenerate $request)
|
||||
{
|
||||
if ($request->input('email_prefix')) {
|
||||
// If generate_count is specified with email_prefix, generate multiple users with incremented emails
|
||||
if ($request->input('generate_count')) {
|
||||
return $this->multiGenerateWithPrefix($request);
|
||||
}
|
||||
|
||||
// Single user generation with email_prefix
|
||||
$email = $request->input('email_prefix') . '@' . $request->input('email_suffix');
|
||||
|
||||
if (User::byEmail($email)->exists()) {
|
||||
return $this->fail([400201, '邮箱已存在于系统中']);
|
||||
}
|
||||
|
||||
$userService = app(UserService::class);
|
||||
$user = $userService->createUser([
|
||||
'email' => $email,
|
||||
'password' => $request->input('password') ?? $email,
|
||||
'plan_id' => $request->input('plan_id'),
|
||||
'expired_at' => $request->input('expired_at'),
|
||||
]);
|
||||
|
||||
if (!$user->save()) {
|
||||
return $this->fail([500, '生成失败']);
|
||||
}
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
if ($request->input('generate_count')) {
|
||||
return $this->multiGenerate($request);
|
||||
}
|
||||
}
|
||||
|
||||
private function multiGenerate(Request $request)
|
||||
{
|
||||
$userService = app(UserService::class);
|
||||
$usersData = [];
|
||||
|
||||
for ($i = 0; $i < $request->input('generate_count'); $i++) {
|
||||
$email = Helper::randomChar(6) . '@' . $request->input('email_suffix');
|
||||
$usersData[] = [
|
||||
'email' => $email,
|
||||
'password' => $request->input('password') ?? $email,
|
||||
'plan_id' => $request->input('plan_id'),
|
||||
'expired_at' => $request->input('expired_at'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
$users = [];
|
||||
foreach ($usersData as $userData) {
|
||||
$user = $userService->createUser($userData);
|
||||
$user->save();
|
||||
$users[] = $user;
|
||||
}
|
||||
DB::commit();
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return $this->fail([500, '生成失败']);
|
||||
}
|
||||
|
||||
// 判断是否导出 CSV
|
||||
if ($request->input('download_csv')) {
|
||||
$headers = [
|
||||
'Content-Type' => 'text/csv',
|
||||
'Content-Disposition' => 'attachment; filename="users.csv"',
|
||||
];
|
||||
$callback = function () use ($users, $request) {
|
||||
$handle = fopen('php://output', 'w');
|
||||
fputcsv($handle, ['账号', '密码', '过期时间', 'UUID', '创建时间', '订阅地址']);
|
||||
foreach ($users as $user) {
|
||||
$user = $user->refresh();
|
||||
$expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
|
||||
$createDate = date('Y-m-d H:i:s', $user['created_at']);
|
||||
$password = $request->input('password') ?? $user['email'];
|
||||
$subscribeUrl = Helper::getSubscribeUrl($user['token']);
|
||||
fputcsv($handle, [$user['email'], $password, $expireDate, $user['uuid'], $createDate, $subscribeUrl]);
|
||||
}
|
||||
fclose($handle);
|
||||
};
|
||||
return response()->streamDownload($callback, 'users.csv', $headers);
|
||||
}
|
||||
|
||||
// 默认返回 JSON
|
||||
$data = collect($users)->map(function ($user) use ($request) {
|
||||
return [
|
||||
'email' => $user['email'],
|
||||
'password' => $request->input('password') ?? $user['email'],
|
||||
'expired_at' => $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']),
|
||||
'uuid' => $user['uuid'],
|
||||
'created_at' => date('Y-m-d H:i:s', $user['created_at']),
|
||||
'subscribe_url' => Helper::getSubscribeUrl($user['token']),
|
||||
];
|
||||
});
|
||||
return response()->json([
|
||||
'code' => 0,
|
||||
'message' => '批量生成成功',
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
private function multiGenerateWithPrefix(Request $request)
|
||||
{
|
||||
$userService = app(UserService::class);
|
||||
$usersData = [];
|
||||
$emailPrefix = $request->input('email_prefix');
|
||||
$emailSuffix = $request->input('email_suffix');
|
||||
$generateCount = $request->input('generate_count');
|
||||
|
||||
// Check if any of the emails with prefix already exist
|
||||
for ($i = 1; $i <= $generateCount; $i++) {
|
||||
$email = $emailPrefix . '_' . $i . '@' . $emailSuffix;
|
||||
if (User::where('email', $email)->exists()) {
|
||||
return $this->fail([400201, '邮箱 ' . $email . ' 已存在于系统中']);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate user data for batch creation
|
||||
for ($i = 1; $i <= $generateCount; $i++) {
|
||||
$email = $emailPrefix . '_' . $i . '@' . $emailSuffix;
|
||||
$usersData[] = [
|
||||
'email' => $email,
|
||||
'password' => $request->input('password') ?? $email,
|
||||
'plan_id' => $request->input('plan_id'),
|
||||
'expired_at' => $request->input('expired_at'),
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
$users = [];
|
||||
foreach ($usersData as $userData) {
|
||||
$user = $userService->createUser($userData);
|
||||
$user->save();
|
||||
$users[] = $user;
|
||||
}
|
||||
DB::commit();
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return $this->fail([500, '生成失败']);
|
||||
}
|
||||
|
||||
// 判断是否导出 CSV
|
||||
if ($request->input('download_csv')) {
|
||||
$headers = [
|
||||
'Content-Type' => 'text/csv',
|
||||
'Content-Disposition' => 'attachment; filename="users.csv"',
|
||||
];
|
||||
$callback = function () use ($users, $request) {
|
||||
$handle = fopen('php://output', 'w');
|
||||
fputcsv($handle, ['账号', '密码', '过期时间', 'UUID', '创建时间', '订阅地址']);
|
||||
foreach ($users as $user) {
|
||||
$user = $user->refresh();
|
||||
$expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
|
||||
$createDate = date('Y-m-d H:i:s', $user['created_at']);
|
||||
$password = $request->input('password') ?? $user['email'];
|
||||
$subscribeUrl = Helper::getSubscribeUrl($user['token']);
|
||||
fputcsv($handle, [$user['email'], $password, $expireDate, $user['uuid'], $createDate, $subscribeUrl]);
|
||||
}
|
||||
fclose($handle);
|
||||
};
|
||||
return response()->streamDownload($callback, 'users.csv', $headers);
|
||||
}
|
||||
|
||||
// 默认返回 JSON
|
||||
$data = collect($users)->map(function ($user) use ($request) {
|
||||
return [
|
||||
'email' => $user['email'],
|
||||
'password' => $request->input('password') ?? $user['email'],
|
||||
'expired_at' => $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']),
|
||||
'uuid' => $user['uuid'],
|
||||
'created_at' => date('Y-m-d H:i:s', $user['created_at']),
|
||||
'subscribe_url' => Helper::getSubscribeUrl($user['token']),
|
||||
];
|
||||
});
|
||||
return response()->json([
|
||||
'code' => 0,
|
||||
'message' => '批量生成成功',
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
public function sendMail(UserSendMail $request)
|
||||
{
|
||||
ini_set('memory_limit', '-1');
|
||||
$scopeInfo = $this->resolveScope($request);
|
||||
$scope = $scopeInfo['scope'];
|
||||
$userIds = $scopeInfo['user_ids'];
|
||||
|
||||
if ($scope === 'selected') {
|
||||
if (empty($userIds)) {
|
||||
return $this->fail([422, 'user_ids不能为空']);
|
||||
}
|
||||
}
|
||||
|
||||
$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::query()
|
||||
->with('plan:id,name')
|
||||
->orderBy('id', 'desc');
|
||||
|
||||
if ($scope === 'filtered') {
|
||||
// filtered: apply filters/sort
|
||||
$builder->orderBy($sort, $sortType);
|
||||
$this->applyFiltersAndSorts($request, $builder);
|
||||
} elseif ($scope === 'selected') {
|
||||
$builder->whereIn('id', $userIds);
|
||||
} // all: ignore filter/sort
|
||||
|
||||
$subject = $request->input('subject');
|
||||
$content = $request->input('content');
|
||||
$appName = admin_setting('app_name', 'XBoard');
|
||||
$appUrl = admin_setting('app_url');
|
||||
|
||||
$chunkSize = 1000;
|
||||
|
||||
$builder->chunk($chunkSize, function ($users) use ($subject, $content, $appName, $appUrl) {
|
||||
foreach ($users as $user) {
|
||||
$vars = [
|
||||
'app.name' => $appName,
|
||||
'app.url' => $appUrl,
|
||||
'now' => now()->format('Y-m-d H:i:s'),
|
||||
'user.id' => $user->id,
|
||||
'user.email' => $user->email,
|
||||
'user.uuid' => $user->uuid,
|
||||
'user.plan_name' => $user->plan?->name ?? '',
|
||||
'user.expired_at' => $user->expired_at ? date('Y-m-d H:i:s', $user->expired_at) : '',
|
||||
'user.transfer_enable' => (int) ($user->transfer_enable ?? 0),
|
||||
'user.transfer_used' => (int) (($user->u ?? 0) + ($user->d ?? 0)),
|
||||
'user.transfer_left' => (int) (($user->transfer_enable ?? 0) - (($user->u ?? 0) + ($user->d ?? 0))),
|
||||
];
|
||||
|
||||
$templateValue = [
|
||||
'name' => $appName,
|
||||
'url' => $appUrl,
|
||||
'content' => $content,
|
||||
'vars' => $vars,
|
||||
'content_mode' => 'text',
|
||||
];
|
||||
|
||||
dispatch(new SendEmailJob([
|
||||
'email' => $user->email,
|
||||
'subject' => $subject,
|
||||
'template_name' => 'notify',
|
||||
'template_value' => $templateValue
|
||||
], 'send_email_mass'));
|
||||
}
|
||||
});
|
||||
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
public function ban(Request $request)
|
||||
{
|
||||
$scopeInfo = $this->resolveScope($request);
|
||||
$scope = $scopeInfo['scope'];
|
||||
$userIds = $scopeInfo['user_ids'];
|
||||
|
||||
if ($scope === 'selected') {
|
||||
if (empty($userIds)) {
|
||||
return $this->fail([422, 'user_ids不能为空']);
|
||||
}
|
||||
}
|
||||
|
||||
$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::query()->orderBy('id', 'desc');
|
||||
|
||||
if ($scope === 'filtered') {
|
||||
// filtered: keep current semantics
|
||||
$builder->orderBy($sort, $sortType);
|
||||
$this->applyFiltersAndSorts($request, $builder);
|
||||
} elseif ($scope === 'selected') {
|
||||
$builder->whereIn('id', $userIds);
|
||||
} // all: ignore filter/sort
|
||||
|
||||
try {
|
||||
$builder->update([
|
||||
'banned' => 1
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
return $this->fail([500, '处理失败']);
|
||||
}
|
||||
// Full refresh not implemented.
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
// Delete user and related data.
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'required|exists:App\Models\User,id'
|
||||
], [
|
||||
'id.required' => '用户ID不能为空',
|
||||
'id.exists' => '用户不存在'
|
||||
]);
|
||||
$user = User::find($request->input('id'));
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
$user->orders()->delete();
|
||||
$user->codes()->delete();
|
||||
$user->stat()->delete();
|
||||
$user->tickets()->delete();
|
||||
$user->delete();
|
||||
DB::commit();
|
||||
return $this->success(true);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
Log::error($e);
|
||||
return $this->fail([500, '删除失败']);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user