Xboard/app/Http/Controllers/V2/Admin/UserController.php

494 lines
17 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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\Traits\QueryOperators;
use App\Utils\Helper;
use Illuminate\Database\Eloquent\Builder;
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
*
* @param Request $request
* @param Builder $builder
* @return void
*/
private function applyFiltersAndSorts(Request $request, Builder $builder): void
{
$this->applyFilters($request, $builder);
$this->applySorting($request, $builder);
}
/**
* Apply filters to the query builder
*
* @param Request $request
* @param Builder $builder
* @return void
*/
private function applyFilters(Request $request, Builder $builder): void
{
if (!$request->has('filter')) {
return;
}
collect($request->input('filter'))->each(function ($filter) use ($builder) {
$field = $filter['id'];
$value = $filter['value'];
$builder->where(function ($query) use ($field, $value) {
$this->buildFilterQuery($query, $field, $value);
});
});
}
/**
* Build the filter query based on field and value
*
* @param Builder $query
* @param string $field
* @param mixed $value
* @return void
*/
private function buildFilterQuery(Builder $query, string $field, mixed $value): void
{
// 处理关联查询
if (str_contains($field, '.')) {
[$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 to the query builder
*
* @param Request $request
* @param Builder $builder
* @return void
*/
private function applySorting(Request $request, Builder $builder): void
{
if (!$request->has('sort')) {
return;
}
collect($request->input('sort'))->each(function ($sort) use ($builder) {
$field = $sort['id'];
$direction = $sort['desc'] ? 'DESC' : 'ASC';
$builder->orderBy($field, $direction);
});
}
/**
* Fetch paginated user list with filters and sorting
*
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function fetch(Request $request)
{
$current = $request->input('current', 1);
$pageSize = $request->input('pageSize', 10);
$userModel = User::with(['plan:id,name', 'invite_user:id,email', 'group:id,name'])
->select(DB::raw('*, (u+d) as total_used'));
$this->applyFiltersAndSorts($request, $userModel);
$users = $userModel->orderBy('id', 'desc')
->paginate($pageSize, ['*'], 'page', $current);
$users->getCollection()->transform(function ($user) {
return self::transformUserData($user);
});
return response([
'data' => $users->items(),
'total' => $users->total()
]);
}
/**
* Transform user data for response
*
* @param User $user
* @return User
*/
public static function transformUserData(User $user): User
{
$user->subscribe_url = Helper::getSubscribeUrl($user->token);
$user->balance = $user->balance / 100;
$user->commission_balance = $user->commission_balance / 100;
return $user;
}
public function getUserInfoById(Request $request)
{
$request->validate([
'id' => 'required|numeric'
], [
'id.required' => '用户ID不能为空'
]);
$user = User::find($request->input('id'))->load('invite_user');
return $this->success($user);
}
public function update(UserUpdate $request)
{
$params = $request->validated();
$user = User::find($request->input('id'));
if (!$user) {
return $this->fail([400202, '用户不存在']);
}
// 检查邮箱是否被使用
if (User::where('email', $params['email'])->first() && $user->email !== $params['email']) {
return $this->fail([400201, '邮箱已被使用']);
}
// 处理密码
if (isset($params['password'])) {
$params['password'] = password_hash($params['password'], PASSWORD_DEFAULT);
$params['password_algo'] = NULL;
} else {
unset($params['password']);
}
// 处理订阅计划
if (isset($params['plan_id'])) {
$plan = Plan::find($params['plan_id']);
if (!$plan) {
return $this->fail([400202, '订阅计划不存在']);
}
// return json_encode($plan);
$params['group_id'] = $plan->group_id;
}
// 处理邀请用户
if ($request->input('invite_user_email') && $inviteUser = User::where('email', $request->input('invite_user_email'))->first()) {
$params['invite_user_id'] = $inviteUser->id;
} else {
$params['invite_user_id'] = null;
}
if (isset($params['banned']) && (int) $params['banned'] === 1) {
$authService = new AuthService($user);
$authService->removeSession();
}
if (isset($params['balance'])) {
$params['balance'] = $params['balance'] * 100;
}
if (isset($params['commission_balance'])) {
$params['commission_balance'] = $params['commission_balance'] * 100;
}
try {
$user->update($params);
} catch (\Exception $e) {
Log::error($e);
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}
/**
* 导出用户数据为CSV格式
*
* @param Request $request
* @return \Symfony\Component\HttpFoundation\StreamedResponse
*/
public function dumpCSV(Request $request)
{
ini_set('memory_limit', '-1');
gc_enable(); // 启用垃圾回收
// 优化查询使用with预加载plan关系避免N+1问题
$query = User::with('plan:id,name')
->orderBy('id', 'asc')
->select([
'email',
'balance',
'commission_balance',
'transfer_enable',
'u',
'd',
'expired_at',
'token',
'plan_id'
]);
$this->applyFiltersAndSorts($request, $query);
$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 ($request->input('plan_id')) {
$plan = Plan::find($request->input('plan_id'));
if (!$plan) {
return $this->fail([400202, '订阅计划不存在']);
}
}
$user = [
'email' => $request->input('email_prefix') . '@' . $request->input('email_suffix'),
'plan_id' => isset($plan->id) ? $plan->id : NULL,
'group_id' => isset($plan->group_id) ? $plan->group_id : NULL,
'transfer_enable' => isset($plan->transfer_enable) ? $plan->transfer_enable * 1073741824 : 0,
'expired_at' => $request->input('expired_at') ?? NULL,
'uuid' => Helper::guid(true),
'token' => Helper::guid()
];
if (User::where('email', $user['email'])->first()) {
return $this->fail([400201, '邮箱已存在于系统中']);
}
$user['password'] = password_hash($request->input('password') ?? $user['email'], PASSWORD_DEFAULT);
if (!User::create($user)) {
return $this->fail([500, '生成失败']);
}
return $this->success(true);
}
if ($request->input('generate_count')) {
$this->multiGenerate($request);
}
}
private function multiGenerate(Request $request)
{
if ($request->input('plan_id')) {
$plan = Plan::find($request->input('plan_id'));
if (!$plan) {
return $this->fail([400202, '订阅计划不存在']);
}
}
$users = [];
for ($i = 0; $i < $request->input('generate_count'); $i++) {
$user = [
'email' => Helper::randomChar(6) . '@' . $request->input('email_suffix'),
'plan_id' => isset($plan->id) ? $plan->id : NULL,
'group_id' => isset($plan->group_id) ? $plan->group_id : NULL,
'transfer_enable' => isset($plan->transfer_enable) ? $plan->transfer_enable * 1073741824 : 0,
'expired_at' => $request->input('expired_at') ?? NULL,
'uuid' => Helper::guid(true),
'token' => Helper::guid(),
'created_at' => time(),
'updated_at' => time()
];
$user['password'] = password_hash($request->input('password') ?? $user['email'], PASSWORD_DEFAULT);
array_push($users, $user);
}
try {
DB::beginTransaction();
if (!User::insert($users)) {
throw new \Exception();
}
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
Log::error($e);
return $this->fail([500, '生成失败']);
}
$data = "账号,密码,过期时间,UUID,创建时间,订阅地址\r\n";
foreach ($users as $user) {
$expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
$createDate = date('Y-m-d H:i:s', $user['created_at']);
$password = $request->input('password') ?? $user['email'];
$subscribeUrl = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user['token']);
$data .= "{$user['email']},{$password},{$expireDate},{$user['uuid']},{$createDate},{$subscribeUrl}\r\n";
}
echo $data;
}
public function sendMail(UserSendMail $request)
{
ini_set('memory_limit', '-1');
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$builder = User::orderBy($sort, $sortType);
$this->applyFiltersAndSorts($request, $builder);
$users = $builder->get();
foreach ($users as $user) {
SendEmailJob::dispatch(
[
'email' => $user->email,
'subject' => $request->input('subject'),
'template_name' => 'notify',
'template_value' => [
'name' => admin_setting('app_name', 'XBoard'),
'url' => admin_setting('app_url'),
'content' => $request->input('content')
]
],
'send_email_mass'
);
}
return $this->success(true);
}
public function ban(Request $request)
{
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$builder = User::orderBy($sort, $sortType);
$this->applyFilters($request, $builder);
try {
$builder->update([
'banned' => 1
]);
} catch (\Exception $e) {
Log::error($e);
return $this->fail([500, '处理失败']);
}
return $this->success(true);
}
/**
* 删除用户及其关联数据
*
* @param Request $request
* @return JsonResponse
*/
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, '删除失败']);
}
}
}