2026-02-26 13:35:38 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 文件功能:用户中心与管理控制器
|
2026-02-26 21:10:34 +08:00
|
|
|
|
* 接管原版 USERinfo.ASP, USERSET.ASP, chpasswd.asp, KILLUSER.ASP, LOCKIP.ASP
|
|
|
|
|
|
*
|
|
|
|
|
|
* 权限等级通过 sysparam 表动态配置:
|
|
|
|
|
|
* level_kick - 踢人所需等级
|
|
|
|
|
|
* level_mute - 禁言所需等级
|
|
|
|
|
|
* level_ban - 封号所需等级
|
|
|
|
|
|
* level_banip - 封IP所需等级
|
2026-02-26 13:35:38 +08:00
|
|
|
|
*
|
|
|
|
|
|
* @author ChatRoom Laravel
|
|
|
|
|
|
*
|
2026-02-26 21:10:34 +08:00
|
|
|
|
* @version 1.1.0
|
2026-02-26 13:35:38 +08:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
|
|
|
|
|
|
|
|
use App\Events\UserKicked;
|
|
|
|
|
|
use App\Events\UserMuted;
|
|
|
|
|
|
use App\Http\Requests\ChangePasswordRequest;
|
|
|
|
|
|
use App\Http\Requests\UpdateProfileRequest;
|
|
|
|
|
|
use App\Models\Room;
|
2026-02-26 21:10:34 +08:00
|
|
|
|
use App\Models\Sysparam;
|
2026-02-26 13:35:38 +08:00
|
|
|
|
use App\Models\User;
|
|
|
|
|
|
use Illuminate\Http\JsonResponse;
|
|
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
|
use Illuminate\Support\Facades\Auth;
|
|
|
|
|
|
use Illuminate\Support\Facades\Hash;
|
2026-02-26 21:10:34 +08:00
|
|
|
|
use Illuminate\Support\Facades\Redis;
|
2026-02-26 13:35:38 +08:00
|
|
|
|
|
2026-02-27 11:29:48 +08:00
|
|
|
|
use Stevebauman\Location\Facades\Location;
|
|
|
|
|
|
|
2026-02-26 13:35:38 +08:00
|
|
|
|
class UserController extends Controller
|
|
|
|
|
|
{
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 查看其他用户资料片 (对应 USERinfo.ASP)
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function show(string $username): JsonResponse
|
|
|
|
|
|
{
|
2026-02-27 11:29:48 +08:00
|
|
|
|
$targetUser = User::where('username', $username)->firstOrFail();
|
|
|
|
|
|
$operator = Auth::user();
|
|
|
|
|
|
|
|
|
|
|
|
// 基础公开信息
|
|
|
|
|
|
$data = [
|
|
|
|
|
|
'username' => $targetUser->username,
|
|
|
|
|
|
'sex' => $targetUser->sex,
|
|
|
|
|
|
'headface' => $targetUser->headface,
|
|
|
|
|
|
'usersf' => $targetUser->usersf,
|
|
|
|
|
|
'user_level' => $targetUser->user_level,
|
|
|
|
|
|
'qianming' => $targetUser->qianming,
|
|
|
|
|
|
'sign' => $targetUser->sign ?? '这个人很懒,什么都没留下。',
|
|
|
|
|
|
'created_at' => $targetUser->created_at->format('Y-m-d'),
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// 只有等级不低于对方,或者自己看自己时,才能看到详细的财富、经验资产
|
|
|
|
|
|
if ($operator && ($operator->user_level >= $targetUser->user_level || $operator->id === $targetUser->id)) {
|
|
|
|
|
|
$data['exp_num'] = $targetUser->exp_num ?? 0;
|
|
|
|
|
|
$data['jjb'] = $targetUser->jjb ?? 0;
|
|
|
|
|
|
$data['meili'] = $targetUser->meili ?? 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 拥有封禁IP(level_banip)或踢人以上权限的管理,可以查看IP和归属地
|
|
|
|
|
|
$levelBanIp = (int) Sysparam::getValue('level_banip', '15');
|
|
|
|
|
|
if ($operator && $operator->user_level >= $levelBanIp) {
|
|
|
|
|
|
$data['last_ip'] = $targetUser->last_ip;
|
|
|
|
|
|
$data['login_ip'] = $targetUser->login_ip; // 假设表中存在 login_ip 记录本次IP,若无则使用 last_ip 退化
|
|
|
|
|
|
|
2026-02-27 11:44:28 +08:00
|
|
|
|
// 解析归属地 (防崩溃处理:检查服务提供者是否已安装)
|
2026-02-27 12:08:38 +08:00
|
|
|
|
// GeoLite2 本地 .mmdb 仅返回英文,这里使用内置翻译表转为中文
|
|
|
|
|
|
$locationMap = [
|
|
|
|
|
|
'China' => '中国', 'Beijing' => '北京', 'Shanghai' => '上海',
|
|
|
|
|
|
'Tianjin' => '天津', 'Chongqing' => '重庆', 'Hebei' => '河北',
|
|
|
|
|
|
'Shanxi' => '山西', 'Liaoning' => '辽宁', 'Jilin' => '吉林',
|
|
|
|
|
|
'Heilongjiang' => '黑龙江', 'Jiangsu' => '江苏', 'Zhejiang' => '浙江',
|
|
|
|
|
|
'Anhui' => '安徽', 'Fujian' => '福建', 'Jiangxi' => '江西',
|
|
|
|
|
|
'Shandong' => '山东', 'Henan' => '河南', 'Hubei' => '湖北',
|
|
|
|
|
|
'Hunan' => '湖南', 'Guangdong' => '广东', 'Hainan' => '海南',
|
|
|
|
|
|
'Sichuan' => '四川', 'Guizhou' => '贵州', 'Yunnan' => '云南',
|
|
|
|
|
|
'Shaanxi' => '陕西', 'Gansu' => '甘肃', 'Qinghai' => '青海',
|
|
|
|
|
|
'Taiwan' => '台湾', 'Inner Mongolia' => '内蒙古', 'Guangxi' => '广西',
|
|
|
|
|
|
'Tibet' => '西藏', 'Ningxia' => '宁夏', 'Xinjiang' => '新疆',
|
|
|
|
|
|
'Hong Kong' => '香港', 'Macau' => '澳门',
|
|
|
|
|
|
'United States' => '美国', 'Japan' => '日本', 'South Korea' => '韩国',
|
|
|
|
|
|
'United Kingdom' => '英国', 'France' => '法国', 'Germany' => '德国',
|
|
|
|
|
|
'Russia' => '俄罗斯', 'Canada' => '加拿大', 'Australia' => '澳大利亚',
|
|
|
|
|
|
'Singapore' => '新加坡',
|
|
|
|
|
|
];
|
|
|
|
|
|
$tr = fn(string $name): string => $locationMap[$name] ?? $name;
|
|
|
|
|
|
|
2026-02-27 11:29:48 +08:00
|
|
|
|
$ipToLookup = $targetUser->login_ip ?: $targetUser->last_ip;
|
|
|
|
|
|
if ($ipToLookup) {
|
2026-02-27 11:44:28 +08:00
|
|
|
|
if (class_exists('\Stevebauman\Location\Facades\Location')) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
$position = Location::get($ipToLookup);
|
|
|
|
|
|
if ($position) {
|
2026-02-27 12:08:38 +08:00
|
|
|
|
$isChinaOrLocal = in_array($position->countryCode ?? '', ['CN', 'TW', 'HK', 'MO'])
|
|
|
|
|
|
|| $position->countryName === 'Local';
|
|
|
|
|
|
if ($isChinaOrLocal) {
|
|
|
|
|
|
$region = $tr($position->regionName ?? '');
|
|
|
|
|
|
$city = $tr($position->cityName ?? '');
|
|
|
|
|
|
$data['location'] = trim($region . ($region !== $city ? ' ' . $city : ''));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$data['location'] = $tr($position->countryName ?? '未知区域');
|
|
|
|
|
|
}
|
2026-02-27 11:56:29 +08:00
|
|
|
|
if (empty($data['location'])) {
|
|
|
|
|
|
$data['location'] = '未知区域';
|
|
|
|
|
|
}
|
2026-02-27 11:44:28 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
$data['location'] = '未知区域';
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
|
$data['location'] = '解析失败';
|
|
|
|
|
|
}
|
2026-02-27 11:29:48 +08:00
|
|
|
|
} else {
|
2026-02-27 11:56:29 +08:00
|
|
|
|
$data['location'] = '未安装IP库';
|
2026-02-27 11:29:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$data['location'] = '暂无记录';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-26 13:35:38 +08:00
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
2026-02-27 00:05:31 +08:00
|
|
|
|
'status' => 'success',
|
2026-02-27 11:29:48 +08:00
|
|
|
|
'data' => $data,
|
2026-02-26 13:35:38 +08:00
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 修改个人资料 (对应 USERSET.ASP)
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function updateProfile(UpdateProfileRequest $request): JsonResponse
|
|
|
|
|
|
{
|
|
|
|
|
|
$user = Auth::user();
|
2026-02-26 22:57:30 +08:00
|
|
|
|
$data = $request->validated();
|
2026-02-27 10:02:33 +08:00
|
|
|
|
|
|
|
|
|
|
// 当用户试图更新邮箱,并且新邮箱不等于当前旧邮箱时启动验证码拦截
|
|
|
|
|
|
if (isset($data['email']) && $data['email'] !== $user->email) {
|
|
|
|
|
|
// 首先判断系统开关是否开启,没开启直接禁止修改邮箱
|
|
|
|
|
|
if (\App\Models\SysParam::where('alias', 'smtp_enabled')->value('body') !== '1') {
|
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => '系统未开启邮件服务,当前禁止绑定/修改邮箱。'], 403);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$emailCode = $request->input('email_code');
|
|
|
|
|
|
if (empty($emailCode)) {
|
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => '新邮箱需要验证码,请先获取并填写验证码。'], 422);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取缓存的验证码
|
|
|
|
|
|
$codeKey = 'email_verify_code_' . $user->id . '_' . $data['email'];
|
|
|
|
|
|
$cachedCode = \Illuminate\Support\Facades\Cache::get($codeKey);
|
|
|
|
|
|
|
|
|
|
|
|
if (!$cachedCode || $cachedCode != $emailCode) {
|
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => '验证码不正确或已过期(有效期5分钟),请重新获取。'], 422);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证成功后,立即核销该验证码防止二次利用
|
|
|
|
|
|
\Illuminate\Support\Facades\Cache::forget($codeKey);
|
|
|
|
|
|
}
|
2026-02-26 22:57:30 +08:00
|
|
|
|
|
|
|
|
|
|
$user->update($data);
|
2026-02-26 13:35:38 +08:00
|
|
|
|
|
|
|
|
|
|
return response()->json(['status' => 'success', 'message' => '资料更新成功。']);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 修改密码 (对应 chpasswd.asp)
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function changePassword(ChangePasswordRequest $request): JsonResponse
|
|
|
|
|
|
{
|
|
|
|
|
|
$user = Auth::user();
|
|
|
|
|
|
$oldPasswordInput = $request->input('old_password');
|
|
|
|
|
|
|
|
|
|
|
|
// 双模式密码校验逻辑(沿用 AuthController 中策略)
|
|
|
|
|
|
$isOldPasswordCorrect = false;
|
|
|
|
|
|
|
|
|
|
|
|
// 优先验证 Bcrypt
|
|
|
|
|
|
if (Hash::check($oldPasswordInput, $user->password)) {
|
|
|
|
|
|
$isOldPasswordCorrect = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 降级验证旧版 MD5
|
|
|
|
|
|
elseif (md5($oldPasswordInput) === $user->password) {
|
|
|
|
|
|
$isOldPasswordCorrect = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (! $isOldPasswordCorrect) {
|
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => '当前密码输入不正确。'], 422);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证通过,覆盖为新的 Bcrypt 加密串
|
|
|
|
|
|
$user->password = Hash::make($request->input('new_password'));
|
|
|
|
|
|
$user->save();
|
|
|
|
|
|
|
|
|
|
|
|
return response()->json(['status' => 'success', 'message' => '密码已成功修改。下次请使用新密码登录。']);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-26 21:10:34 +08:00
|
|
|
|
* 通用权限校验:检查操作者是否有权操作目标用户
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param object $operator 操作者
|
|
|
|
|
|
* @param string $targetUsername 目标用户名
|
|
|
|
|
|
* @param int $roomId 房间ID
|
|
|
|
|
|
* @param string $levelKey sysparam中的等级键名(如 level_kick)
|
|
|
|
|
|
* @param string $actionName 操作名称(用于错误提示)
|
|
|
|
|
|
* @return array{room: Room, target: User}|JsonResponse
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function checkPermission(object $operator, string $targetUsername, int $roomId, string $levelKey, string $actionName): array|JsonResponse
|
|
|
|
|
|
{
|
|
|
|
|
|
$room = Room::findOrFail($roomId);
|
|
|
|
|
|
$requiredLevel = (int) Sysparam::getValue($levelKey, '15');
|
|
|
|
|
|
|
|
|
|
|
|
// 鉴权:操作者要是房间房主或达到所需等级
|
|
|
|
|
|
if ($room->master !== $operator->username && $operator->user_level < $requiredLevel) {
|
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => "权限不足(需要{$requiredLevel}级),无法执行{$actionName}操作。"], 403);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$targetUser = User::where('username', $targetUsername)->first();
|
|
|
|
|
|
if (! $targetUser) {
|
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => '目标用户不存在。'], 404);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 防误伤:不能操作等级 >= 自己的人
|
|
|
|
|
|
if ($targetUser->user_level >= $operator->user_level) {
|
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => "权限不足,无法对同级或高级用户执行{$actionName}。"], 403);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return ['room' => $room, 'target' => $targetUser];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 踢出房间 (对应 KILLUSER.ASP)
|
|
|
|
|
|
* 所需等级由 sysparam level_kick 配置
|
2026-02-26 13:35:38 +08:00
|
|
|
|
*/
|
|
|
|
|
|
public function kick(Request $request, string $username): JsonResponse
|
|
|
|
|
|
{
|
|
|
|
|
|
$operator = Auth::user();
|
|
|
|
|
|
$roomId = $request->input('room_id');
|
|
|
|
|
|
|
|
|
|
|
|
if (! $roomId) {
|
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => '缺少房间参数。'], 422);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 21:10:34 +08:00
|
|
|
|
$result = $this->checkPermission($operator, $username, $roomId, 'level_kick', '踢出');
|
|
|
|
|
|
if ($result instanceof JsonResponse) {
|
|
|
|
|
|
return $result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 广播踢出事件
|
|
|
|
|
|
broadcast(new UserKicked($roomId, $result['target']->username, "管理员 [{$operator->username}] 将 [{$result['target']->username}] 踢出了聊天室。"));
|
|
|
|
|
|
|
|
|
|
|
|
return response()->json(['status' => 'success', 'message' => "已成功将 {$result['target']->username} 踢出房间。"]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 禁言 (对应原版限制功能)
|
|
|
|
|
|
* 所需等级由 sysparam level_mute 配置
|
|
|
|
|
|
* 禁言信息存入 Redis,TTL 到期自动解除
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function mute(Request $request, string $username): JsonResponse
|
|
|
|
|
|
{
|
|
|
|
|
|
$operator = Auth::user();
|
|
|
|
|
|
$roomId = $request->input('room_id');
|
|
|
|
|
|
$duration = (int) $request->input('duration', 5);
|
2026-02-26 13:35:38 +08:00
|
|
|
|
|
2026-02-26 21:10:34 +08:00
|
|
|
|
if (! $roomId) {
|
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => '缺少房间参数。'], 422);
|
2026-02-26 13:35:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 21:10:34 +08:00
|
|
|
|
$result = $this->checkPermission($operator, $username, $roomId, 'level_mute', '禁言');
|
|
|
|
|
|
if ($result instanceof JsonResponse) {
|
|
|
|
|
|
return $result;
|
2026-02-26 13:35:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 21:10:34 +08:00
|
|
|
|
// 写入 Redis 禁言标记,TTL = 禁言分钟数 * 60
|
|
|
|
|
|
Redis::setex("mute:{$roomId}:{$username}", $duration * 60, json_encode([
|
|
|
|
|
|
'operator' => $operator->username,
|
|
|
|
|
|
'reason' => '管理员禁言',
|
|
|
|
|
|
'until' => now()->addMinutes($duration)->toDateTimeString(),
|
|
|
|
|
|
]));
|
|
|
|
|
|
|
|
|
|
|
|
// 广播禁言事件
|
|
|
|
|
|
broadcast(new UserMuted($roomId, $username, $duration));
|
|
|
|
|
|
|
|
|
|
|
|
return response()->json(['status' => 'success', 'message' => "已对 {$username} 实施禁言 {$duration} 分钟。"]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 封号(禁止登录)
|
|
|
|
|
|
* 所需等级由 sysparam level_ban 配置
|
|
|
|
|
|
* 将用户等级设为 -1 表示封禁
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function ban(Request $request, string $username): JsonResponse
|
|
|
|
|
|
{
|
|
|
|
|
|
$operator = Auth::user();
|
|
|
|
|
|
$roomId = $request->input('room_id');
|
|
|
|
|
|
|
|
|
|
|
|
if (! $roomId) {
|
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => '缺少房间参数。'], 422);
|
2026-02-26 13:35:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 21:10:34 +08:00
|
|
|
|
$result = $this->checkPermission($operator, $username, $roomId, 'level_ban', '封号');
|
|
|
|
|
|
if ($result instanceof JsonResponse) {
|
|
|
|
|
|
return $result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 封号:设置等级为 -1
|
|
|
|
|
|
$result['target']->user_level = -1;
|
|
|
|
|
|
$result['target']->save();
|
|
|
|
|
|
|
|
|
|
|
|
// 踢出聊天室
|
|
|
|
|
|
broadcast(new UserKicked($roomId, $username, "管理员 [{$operator->username}] 已封禁用户 [{$username}] 的账号。"));
|
2026-02-26 13:35:38 +08:00
|
|
|
|
|
2026-02-26 21:10:34 +08:00
|
|
|
|
return response()->json(['status' => 'success', 'message' => "用户 {$username} 已被封号。"]);
|
2026-02-26 13:35:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-26 21:10:34 +08:00
|
|
|
|
* 封IP(记录IP到黑名单并踢出)
|
|
|
|
|
|
* 所需等级由 sysparam level_banip 配置
|
2026-02-26 13:35:38 +08:00
|
|
|
|
*/
|
2026-02-26 21:10:34 +08:00
|
|
|
|
public function banIp(Request $request, string $username): JsonResponse
|
2026-02-26 13:35:38 +08:00
|
|
|
|
{
|
|
|
|
|
|
$operator = Auth::user();
|
|
|
|
|
|
$roomId = $request->input('room_id');
|
|
|
|
|
|
|
|
|
|
|
|
if (! $roomId) {
|
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => '缺少房间参数。'], 422);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 21:10:34 +08:00
|
|
|
|
$result = $this->checkPermission($operator, $username, $roomId, 'level_banip', '封IP');
|
|
|
|
|
|
if ($result instanceof JsonResponse) {
|
|
|
|
|
|
return $result;
|
|
|
|
|
|
}
|
2026-02-26 13:35:38 +08:00
|
|
|
|
|
2026-02-26 21:10:34 +08:00
|
|
|
|
$targetIp = $result['target']->last_ip;
|
|
|
|
|
|
|
|
|
|
|
|
if ($targetIp) {
|
|
|
|
|
|
// 将IP加入 Redis 黑名单(永久)
|
|
|
|
|
|
Redis::sadd('banned_ips', $targetIp);
|
2026-02-26 13:35:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 21:10:34 +08:00
|
|
|
|
// 同时封号
|
|
|
|
|
|
$result['target']->user_level = -1;
|
|
|
|
|
|
$result['target']->save();
|
2026-02-26 13:35:38 +08:00
|
|
|
|
|
2026-02-26 21:10:34 +08:00
|
|
|
|
// 踢出聊天室
|
|
|
|
|
|
broadcast(new UserKicked($roomId, $username, "管理员 [{$operator->username}] 已封禁用户 [{$username}] 的IP地址。"));
|
|
|
|
|
|
|
|
|
|
|
|
$ipInfo = $targetIp ? "(IP: {$targetIp})" : '(未记录IP)';
|
2026-02-26 13:35:38 +08:00
|
|
|
|
|
2026-02-26 21:10:34 +08:00
|
|
|
|
return response()->json(['status' => 'success', 'message' => "用户 {$username} 已被封号并封IP{$ipInfo}。"]);
|
2026-02-26 13:35:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|