Files
chatroom/app/Http/Controllers/UserController.php

370 lines
14 KiB
PHP
Raw Normal View History

<?php
/**
* 文件功能:用户中心与管理控制器
* 接管原版 USERinfo.ASP, USERSET.ASP, chpasswd.asp, KILLUSER.ASP, LOCKIP.ASP
*
* 权限等级通过 sysparam 表动态配置:
* level_kick - 踢人所需等级
* level_mute - 禁言所需等级
* level_ban - 封号所需等级
* level_banip - 封IP所需等级
*
* @author ChatRoom Laravel
*
* @version 1.1.0
*/
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;
use App\Models\Sysparam;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Redis;
class UserController extends Controller
{
/**
* 查看其他用户资料片 (对应 USERinfo.ASP)
*/
public function show(string $username): JsonResponse
{
$targetUser = User::where('username', $username)->firstOrFail();
$operator = Auth::user();
// 基础公开信息
$activePosition = $targetUser->activePosition?->load('position.department')->position;
$data = [
'username' => $targetUser->username,
'sex' => match ((int) $targetUser->sex) {
1 => '男', 2 => '女', default => ''
},
'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'),
// 在职职务(供名片弹窗任命/撤销判断)
'position_name' => $activePosition?->name ?? '',
'position_icon' => $activePosition?->icon ?? '',
'department_name' => $activePosition?->department?->name ?? '',
];
// 只有等级不低于对方,或者自己看自己时,才能看到详细的财富、经验资产
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['bank_jjb'] = $targetUser->bank_jjb ?? 0;
$data['meili'] = $targetUser->meili ?? 0;
}
// 仅当自己看自己时,附加邀请相关信息,用于展示专属邀请链接
if ($operator && $operator->id === $targetUser->id) {
$data['id'] = $targetUser->id;
$data['invitees_count'] = $targetUser->invitees()->count();
}
// 职务履历所有任职记录按任命时间倒序positions() 关系已含 with
$data['position_history'] = $targetUser->positions
->map(fn ($up) => [
'position_icon' => $up->position?->icon ?? '',
'position_name' => $up->position?->name ?? '',
'department_name' => $up->position?->department?->name ?? '',
'appointed_at' => $up->appointed_at?->format('Y-m-d'),
'revoked_at' => $up->revoked_at?->format('Y-m-d'),
'is_active' => $up->is_active,
'duration_days' => $up->duration_days,
])
->values()
->all();
// 拥有封禁IPlevel_banip或踢人以上权限的管理可以查看IP和归属地
$levelBanIp = (int) Sysparam::getValue('level_banip', '15');
if ($operator && $operator->user_level >= $levelBanIp) {
$data['first_ip'] = $targetUser->first_ip;
// last_ip 目前定义为『上次登录IP』取数据库 previous_ip
$data['last_ip'] = $targetUser->previous_ip;
// login_ip 目前定义为『本次登录IP』取数据库 last_ip
$data['login_ip'] = $targetUser->last_ip;
// 解析归属地:使用 ip2region 离线库,直接返回原生中文(省|市|ISP
$ipToLookup = $targetUser->last_ip;
if ($ipToLookup) {
try {
// 不传路径,使用 zoujingli/ip2region 包自带的内置数据库
$ip2r = new \Ip2Region;
$info = $ip2r->getIpInfo($ipToLookup);
if ($info) {
$country = $info['country'] ?? '';
$province = $info['province'] ?? '';
$city = $info['city'] ?? '';
// 过滤掉占位符 "0"
$province = ($province === '0') ? '' : $province;
$city = ($city === '0') ? '' : $city;
if ($country === '中国') {
$data['location'] = trim($province.($province !== $city ? ' '.$city : ''));
} else {
$data['location'] = $country ?: '未知区域';
}
if (empty($data['location'])) {
$data['location'] = '未知区域';
}
} else {
$data['location'] = '未知区域';
}
} catch (\Exception $e) {
$data['location'] = '解析失败';
}
} else {
$data['location'] = '暂无记录';
}
}
return response()->json([
'status' => 'success',
'data' => $data,
]);
}
/**
* 修改个人资料 (对应 USERSET.ASP)
*/
public function updateProfile(UpdateProfileRequest $request): JsonResponse
{
$user = Auth::user();
$data = $request->validated();
// 当用户试图更新邮箱,并且新邮箱不等于当前旧邮箱时启动验证码拦截
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);
}
if (isset($data['headface']) && $data['headface'] !== $user->headface) {
$user->deleteCustomAvatar();
}
$user->update($data);
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' => '密码已成功修改。下次请使用新密码登录。']);
}
/**
* 通用权限校验:检查操作者是否有权操作目标用户
*
* @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 配置
*/
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);
}
$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 配置
* 禁言信息存入 RedisTTL 到期自动解除
*/
public function mute(Request $request, string $username): JsonResponse
{
$operator = Auth::user();
$roomId = $request->input('room_id');
$duration = (int) $request->input('duration', 5);
if (! $roomId) {
return response()->json(['status' => 'error', 'message' => '缺少房间参数。'], 422);
}
$result = $this->checkPermission($operator, $username, $roomId, 'level_mute', '禁言');
if ($result instanceof JsonResponse) {
return $result;
}
// 写入 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);
}
$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}] 的账号。"));
return response()->json(['status' => 'success', 'message' => "用户 {$username} 已被封号。"]);
}
/**
* 封IP记录IP到黑名单并踢出
* 所需等级由 sysparam level_banip 配置
*/
public function banIp(Request $request, string $username): JsonResponse
{
$operator = Auth::user();
$roomId = $request->input('room_id');
if (! $roomId) {
return response()->json(['status' => 'error', 'message' => '缺少房间参数。'], 422);
}
$result = $this->checkPermission($operator, $username, $roomId, 'level_banip', '封IP');
if ($result instanceof JsonResponse) {
return $result;
}
$targetIp = $result['target']->last_ip;
if ($targetIp) {
// 将IP加入 Redis 黑名单(永久)
Redis::sadd('banned_ips', $targetIp);
}
// 同时封号
$result['target']->user_level = -1;
$result['target']->save();
// 踢出聊天室
broadcast(new UserKicked($roomId, $username, "管理员 [{$operator->username}] 已封禁用户 [{$username}] 的IP地址。"));
$ipInfo = $targetIp ? "IP: {$targetIp}" : '未记录IP';
return response()->json(['status' => 'success', 'message' => "用户 {$username} 已被封号并封IP{$ipInfo}"]);
}
}