2025-05-12 00:19:35 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace App\Service;
|
|
|
|
|
|
|
|
|
|
|
|
use App\BaseApp\BaseService;
|
|
|
|
|
|
use App\Enum\UserStatusEnum;
|
2025-05-12 09:04:46 +08:00
|
|
|
|
use App\Models\MenuModel;
|
|
|
|
|
|
use App\Models\RoleMenuRelationModel;
|
2025-05-12 00:19:35 +08:00
|
|
|
|
use App\Models\RoleModel;
|
2025-05-12 09:04:46 +08:00
|
|
|
|
use App\Models\AdminModel;
|
|
|
|
|
|
use App\Service\common\RedisService;
|
2025-05-12 00:19:35 +08:00
|
|
|
|
use Exception;
|
|
|
|
|
|
use Psr\Container\ContainerExceptionInterface;
|
|
|
|
|
|
use Psr\Container\NotFoundExceptionInterface;
|
|
|
|
|
|
|
2025-05-12 09:04:46 +08:00
|
|
|
|
class AdminService extends BaseService
|
2025-05-12 00:19:35 +08:00
|
|
|
|
{
|
|
|
|
|
|
public function __construct()
|
|
|
|
|
|
{
|
|
|
|
|
|
parent::__construct();
|
2025-05-12 16:13:46 +08:00
|
|
|
|
$this->selectField = [
|
|
|
|
|
|
'id',
|
|
|
|
|
|
'open_id',
|
|
|
|
|
|
'avatar',
|
|
|
|
|
|
'nick_name',
|
|
|
|
|
|
'password',
|
|
|
|
|
|
'phone',
|
|
|
|
|
|
'email',
|
|
|
|
|
|
'code',
|
|
|
|
|
|
'role_id',
|
|
|
|
|
|
'province_id',
|
|
|
|
|
|
'city_id',
|
|
|
|
|
|
'reg_ip',
|
|
|
|
|
|
'last_login_time',
|
|
|
|
|
|
'ip',
|
|
|
|
|
|
'ip_table',
|
|
|
|
|
|
'operation_password',
|
|
|
|
|
|
'desc',
|
|
|
|
|
|
'status',
|
|
|
|
|
|
'created_at',
|
|
|
|
|
|
'updated_at',
|
|
|
|
|
|
'deleted_at',
|
|
|
|
|
|
];
|
2025-05-12 09:04:46 +08:00
|
|
|
|
$this->model = AdminModel::class;
|
2025-05-12 00:19:35 +08:00
|
|
|
|
$this->queryField = [
|
2025-05-12 09:04:46 +08:00
|
|
|
|
'phone' => 'like',
|
2025-05-12 00:19:35 +08:00
|
|
|
|
'nick_name' => 'like',
|
|
|
|
|
|
'email' => 'like',
|
|
|
|
|
|
'role_id' => '=',
|
|
|
|
|
|
'status' => '=',
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取用户列表
|
|
|
|
|
|
* @return array
|
|
|
|
|
|
* @throws ContainerExceptionInterface
|
|
|
|
|
|
* @throws NotFoundExceptionInterface
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function list(): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$this->with = [
|
2025-05-12 13:12:57 +08:00
|
|
|
|
'role'
|
2025-05-12 00:19:35 +08:00
|
|
|
|
];
|
|
|
|
|
|
$result = $this->getPageList();
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($result['items'] as &$v) {
|
|
|
|
|
|
$v['ip_table'] = json_decode($v['ip_table'], true);
|
|
|
|
|
|
$v['status_text'] = UserStatusEnum::from($v['status'])->description();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function detail($id)
|
|
|
|
|
|
{
|
|
|
|
|
|
$result = $this->getDetail($id);
|
|
|
|
|
|
$result['ip_table'] = json_decode($result['ip_table'], true);
|
|
|
|
|
|
$result['status_text'] = UserStatusEnum::from($result['status'])->description();
|
|
|
|
|
|
return $result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function option()
|
|
|
|
|
|
{
|
|
|
|
|
|
$this->optionField = ['id', 'nick_name'];
|
|
|
|
|
|
return $this->getOption();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建用户
|
|
|
|
|
|
* @param $params
|
|
|
|
|
|
* @return mixed
|
|
|
|
|
|
* @throws Exception
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function create($params): mixed
|
|
|
|
|
|
{
|
|
|
|
|
|
$params['ip_table'] = json_encode([]);
|
|
|
|
|
|
$params['password'] = password_hash($params['password'], PASSWORD_DEFAULT);
|
2025-05-12 09:04:46 +08:00
|
|
|
|
$existsUser = $this->model::where('phone', $params['phone'])->whereOr('email', $params['email'])->exists();
|
2025-05-12 00:19:35 +08:00
|
|
|
|
if ($existsUser) {
|
|
|
|
|
|
$this->utils->errorThrow('账号或邮箱已存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $this->insert($params);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 编辑用户
|
|
|
|
|
|
* @param $id
|
|
|
|
|
|
* @param $params
|
|
|
|
|
|
* @return mixed
|
|
|
|
|
|
* @throws Exception
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function update($id, $params): mixed
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->save($id, $params);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 删除用户
|
|
|
|
|
|
* @param $ids
|
|
|
|
|
|
* @return mixed|true
|
|
|
|
|
|
* @throws Exception
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function delete($ids): mixed
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->del($ids);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 修改密码
|
|
|
|
|
|
* @param $password
|
|
|
|
|
|
* @param $newPassword
|
|
|
|
|
|
* @return mixed
|
|
|
|
|
|
* @throws Exception
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function changePassword($password, $newPassword): mixed
|
|
|
|
|
|
{
|
|
|
|
|
|
$info = $this->model::find($this->userId);
|
|
|
|
|
|
if (empty($info)) {
|
|
|
|
|
|
return $this->utils->notFound('数据不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!password_verify($password, $info['password'])) {
|
|
|
|
|
|
return $this->utils->errorThrow('密码错误');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (password_verify($newPassword, $info['password'])) {
|
|
|
|
|
|
return $this->utils->errorThrow('新密码不能与旧密码相同');
|
|
|
|
|
|
}
|
|
|
|
|
|
$result = $this->model::where('id', $this->userId)->update([
|
|
|
|
|
|
'password' => password_hash($newPassword, PASSWORD_DEFAULT),
|
2026-08-10 18:32:06 +08:00
|
|
|
|
'updated_at' => get_time(),
|
2025-05-12 00:19:35 +08:00
|
|
|
|
]);
|
|
|
|
|
|
if (!$result) {
|
|
|
|
|
|
return $this->utils->errorThrow('重置密码失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
return $result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 重置密码
|
|
|
|
|
|
* @param $id
|
|
|
|
|
|
* @param $password
|
|
|
|
|
|
* @param $newPassword
|
|
|
|
|
|
* @return mixed
|
|
|
|
|
|
* @throws Exception
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function resetPassword($id, $password, $newPassword): mixed
|
|
|
|
|
|
{
|
|
|
|
|
|
if ($password !== $newPassword) {
|
|
|
|
|
|
return $this->utils->errorThrow('两次密码不一致');
|
|
|
|
|
|
}
|
|
|
|
|
|
$info = $this->model::find($id);
|
|
|
|
|
|
if (empty($info)) {
|
|
|
|
|
|
return $this->utils->notFound('数据不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (password_verify($newPassword, $info['password'])) {
|
|
|
|
|
|
return $this->utils->errorThrow('新密码不能与旧密码相同');
|
|
|
|
|
|
}
|
|
|
|
|
|
$result = $this->model::where('id', $id)->update([
|
|
|
|
|
|
'password' => password_hash($newPassword, PASSWORD_DEFAULT),
|
2026-08-10 18:32:06 +08:00
|
|
|
|
'updated_at' => get_time(),
|
2025-05-12 00:19:35 +08:00
|
|
|
|
]);
|
|
|
|
|
|
if (!$result) {
|
|
|
|
|
|
return $this->utils->errorThrow('重置密码失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
return $result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-08-10 18:32:06 +08:00
|
|
|
|
* 获取用户信息(本人完整字段,供个人中心回填,不做手机号脱敏)
|
2025-05-12 00:19:35 +08:00
|
|
|
|
* @return mixed
|
|
|
|
|
|
* @throws Exception
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function myInfo(): mixed
|
|
|
|
|
|
{
|
|
|
|
|
|
$this->with = [
|
2025-05-12 13:12:57 +08:00
|
|
|
|
'role'
|
2025-05-12 00:19:35 +08:00
|
|
|
|
];
|
2026-08-10 18:32:06 +08:00
|
|
|
|
$result = $this->getDetail($this->userId);
|
2025-05-12 00:19:35 +08:00
|
|
|
|
$result['created_day_at'] = format_time(strtotime($result['created_at']), 'Y-m-d');
|
2026-08-10 18:32:06 +08:00
|
|
|
|
// 兼容前端 Profile 组件字段
|
|
|
|
|
|
$result['realName'] = $result['nick_name'] ?? '';
|
|
|
|
|
|
$result['username'] = $result['phone'] ?? '';
|
2025-05-12 00:19:35 +08:00
|
|
|
|
return $result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 18:32:06 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 个人中心更新资料(昵称/头像/邮箱/备注/手机)
|
|
|
|
|
|
* 为什么单独接口:不走管理员 CRUD,避免越权改角色等字段
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function updateProfile(array $params): mixed
|
|
|
|
|
|
{
|
|
|
|
|
|
$allow = ['nick_name', 'avatar', 'email', 'desc', 'phone'];
|
|
|
|
|
|
$data = [];
|
|
|
|
|
|
foreach ($allow as $field) {
|
|
|
|
|
|
if (array_key_exists($field, $params)) {
|
|
|
|
|
|
$data[$field] = is_string($params[$field]) ? trim($params[$field]) : $params[$field];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isset($data['nick_name']) && $data['nick_name'] === '') {
|
|
|
|
|
|
$this->utils->errorThrow('昵称不能为空');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isset($data['phone']) && $data['phone'] !== '') {
|
|
|
|
|
|
$exists = $this->model::where('phone', $data['phone'])
|
|
|
|
|
|
->where('id', '<>', $this->userId)
|
|
|
|
|
|
->where('deleted_at', 0)
|
|
|
|
|
|
->exists();
|
|
|
|
|
|
if ($exists) {
|
|
|
|
|
|
$this->utils->errorThrow('手机号已被占用');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isset($data['email']) && $data['email'] !== '') {
|
|
|
|
|
|
$exists = $this->model::where('email', $data['email'])
|
|
|
|
|
|
->where('id', '<>', $this->userId)
|
|
|
|
|
|
->where('deleted_at', 0)
|
|
|
|
|
|
->exists();
|
|
|
|
|
|
if ($exists) {
|
|
|
|
|
|
$this->utils->errorThrow('邮箱已被占用');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (empty($data)) {
|
|
|
|
|
|
return $this->myInfo();
|
|
|
|
|
|
}
|
|
|
|
|
|
$data['updated_at'] = get_time();
|
|
|
|
|
|
$this->model::where('id', $this->userId)->update($data);
|
|
|
|
|
|
return $this->myInfo();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 当前用户登录日志分页
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function loginLogList(): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$page = max(1, (int) request()->get('page', 1));
|
|
|
|
|
|
$size = max(1, min(100, (int) request()->get('pageSize', request()->get('size', 10))));
|
|
|
|
|
|
$query = \App\Models\LoginLogModel::where('user_id', $this->userId)->orderByDesc('id');
|
|
|
|
|
|
$total = (clone $query)->count();
|
|
|
|
|
|
$items = $query->forPage($page, $size)->get()->toArray();
|
|
|
|
|
|
return [
|
|
|
|
|
|
'page' => $page,
|
|
|
|
|
|
'size' => $size,
|
|
|
|
|
|
'page_count' => (int) ceil($total / max($size, 1)),
|
|
|
|
|
|
'total' => $total,
|
|
|
|
|
|
'items' => $items,
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 当前用户操作日志分页(附带接口操作名)
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function opLogList(): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$page = max(1, (int) request()->get('page', 1));
|
|
|
|
|
|
$size = max(1, min(100, (int) request()->get('pageSize', request()->get('size', 10))));
|
|
|
|
|
|
$query = \App\Models\ApiOpLogModel::where('user_id', $this->userId)->orderByDesc('id');
|
|
|
|
|
|
$total = (clone $query)->count();
|
|
|
|
|
|
$items = $query->forPage($page, $size)->get()->toArray();
|
|
|
|
|
|
$urls = array_values(array_unique(array_column($items, 'url')));
|
|
|
|
|
|
$endpointMap = [];
|
|
|
|
|
|
if (!empty($urls)) {
|
|
|
|
|
|
$endpointMap = \App\Models\ApiEndpointModel::whereIn('url', $urls)
|
|
|
|
|
|
->where('deleted_at', 0)
|
|
|
|
|
|
->get(['url', 'name', 'description'])
|
|
|
|
|
|
->keyBy('url')
|
|
|
|
|
|
->toArray();
|
|
|
|
|
|
}
|
|
|
|
|
|
foreach ($items as &$item) {
|
|
|
|
|
|
$ep = $endpointMap[$item['url'] ?? ''] ?? null;
|
|
|
|
|
|
$item['name'] = $ep['name'] ?? '';
|
|
|
|
|
|
$item['description'] = $ep['description'] ?? '';
|
|
|
|
|
|
$item['method_text'] = match ((int) ($item['method'] ?? 0)) {
|
|
|
|
|
|
1 => 'GET',
|
|
|
|
|
|
2 => 'POST',
|
|
|
|
|
|
default => 'ANY',
|
|
|
|
|
|
};
|
|
|
|
|
|
$item['type_text'] = ((int) ($item['type'] ?? 0) === 0) ? '成功' : '失败';
|
|
|
|
|
|
if (is_string($item['param'] ?? null)) {
|
|
|
|
|
|
$item['param'] = json_decode($item['param'], true) ?: [];
|
|
|
|
|
|
}
|
|
|
|
|
|
if (is_string($item['result'] ?? null)) {
|
|
|
|
|
|
$item['result'] = json_decode($item['result'], true) ?: [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
unset($item);
|
|
|
|
|
|
return [
|
|
|
|
|
|
'page' => $page,
|
|
|
|
|
|
'size' => $size,
|
|
|
|
|
|
'page_count' => (int) ceil($total / max($size, 1)),
|
|
|
|
|
|
'total' => $total,
|
|
|
|
|
|
'items' => $items,
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-12 00:19:35 +08:00
|
|
|
|
/**
|
2025-05-12 09:04:46 +08:00
|
|
|
|
* 获取菜单列表
|
|
|
|
|
|
* @return array
|
2025-05-12 00:19:35 +08:00
|
|
|
|
*/
|
2025-05-12 09:04:46 +08:00
|
|
|
|
public function menu(): array
|
2025-05-12 00:19:35 +08:00
|
|
|
|
{
|
2025-05-12 09:04:46 +08:00
|
|
|
|
if ($this->roleId !== 1) {
|
|
|
|
|
|
// 从redis获取菜单列表
|
2025-05-12 16:13:46 +08:00
|
|
|
|
$redis = RedisService::getInstance()->init(config('nl.redis.menu_key'))->get($this->roleId);
|
2025-05-12 09:04:46 +08:00
|
|
|
|
// $redis = null;
|
|
|
|
|
|
if (!empty($redis)) {
|
|
|
|
|
|
return json_decode($redis, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果没有数据则从数据库获取菜单列表
|
|
|
|
|
|
$selectField = [
|
|
|
|
|
|
'id', 'pid', 'title', 'icon', 'name', 'affix_tab', 'path', 'component', 'redirect', 'keep_alive', 'hide_in_menu', 'badge', 'badge_type', 'badge_variants', 'iframe_src', 'sort', 'query', 'created_at'
|
2025-05-12 00:19:35 +08:00
|
|
|
|
];
|
2025-05-12 09:04:46 +08:00
|
|
|
|
if ($this->roleId === 1) {
|
|
|
|
|
|
$menuList = MenuModel::where('deleted_at', 0)->orderBy('sort', 'asc')->get($selectField);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$menuIds = RoleMenuRelationModel::where('role_id', $this->roleId)->pluck('menu_id');
|
|
|
|
|
|
$menuList = MenuModel::where('deleted_at', 0)->whereIn('id', $menuIds)->orderBy('sort', 'asc')->get($selectField);
|
|
|
|
|
|
}
|
|
|
|
|
|
$resultMenus = [];
|
|
|
|
|
|
foreach ($menuList as $menu) {
|
|
|
|
|
|
$resultMenus[] = [
|
|
|
|
|
|
'id' => $menu->id,
|
|
|
|
|
|
'name' => $menu->name,
|
|
|
|
|
|
'path' => $menu->path,
|
|
|
|
|
|
'component' => $menu->component,
|
|
|
|
|
|
'redirect' => $menu->redirect,
|
|
|
|
|
|
'pid' => $menu->pid,
|
|
|
|
|
|
'sort' => $menu->sort,
|
|
|
|
|
|
'meta' => [
|
|
|
|
|
|
'title' => $menu->title,
|
|
|
|
|
|
'icon' => $menu->icon,
|
|
|
|
|
|
'keepAlive' => !$menu->keep_alive,
|
|
|
|
|
|
'hideInMenu' => (bool)$menu->hide_in_menu,
|
|
|
|
|
|
'affixTab' => !$menu->affix_tab,
|
|
|
|
|
|
'order' => $menu->sort,
|
|
|
|
|
|
'iframeSrc' => $menu->iframe_src,
|
|
|
|
|
|
]
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
$result = $this->utils->tree($resultMenus);
|
|
|
|
|
|
// 根据sort排序(多层)
|
|
|
|
|
|
usort($result, function ($a, $b) {
|
|
|
|
|
|
return $a['meta']['order'] - $b['meta']['order'];
|
|
|
|
|
|
});
|
|
|
|
|
|
// 缓存菜单列表到redis
|
2025-05-12 16:13:46 +08:00
|
|
|
|
RedisService::getInstance()->init(config('nl.redis.menu_key'))->set($this->roleId, json_encode($result), 3600);
|
2025-05-12 09:04:46 +08:00
|
|
|
|
return $result;
|
2025-05-12 00:19:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|