Files
lgp-admin-plus-api/app/Service/AdminService.php
LQ bcf54c2727 初始化
缺陷:主题配色需要优化整体的同风格
2026-08-14 23:21:21 +08:00

438 lines
15 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\Service;
use App\BaseApp\BaseService;
use App\Enum\UserStatusEnum;
use App\Models\MenuModel;
use App\Models\RoleMenuRelationModel;
use App\Models\RoleModel;
use App\Models\AdminModel;
use App\Service\common\RedisService;
use Exception;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class AdminService extends BaseService
{
public function __construct()
{
parent::__construct();
$this->selectField = [
'id',
'open_id',
'avatar',
'nick_name',
// 不查 password / legacy_password列表与详情都会直接回给前端密码哈希不该出网
'phone',
'email',
'code',
'role_id',
'department_id',
'province_id',
'city_id',
'reg_ip',
'last_login_time',
'ip',
'ip_table',
'operation_password',
'desc',
'status',
'created_at',
'updated_at',
'deleted_at',
];
$this->model = AdminModel::class;
$this->queryField = [
'phone' => 'like',
'nick_name' => 'like',
'email' => 'like',
'role_id' => '=',
'department_id' => '=',
'status' => '=',
];
}
/**
* 获取用户列表
* @return array
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function list(): array
{
$this->with = [
'role',
'department',
];
$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();
$v['department_name'] = $v['department']['name'] ?? '';
}
unset($v);
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([]);
// open_id 是 NOT NULL 且无默认值,不显式赋值会插入失败
$params['open_id'] = 'nl_' . bin2hex(random_bytes(15));
// status 列默认值是 1禁用不显式写成正常态新账号一登录就被状态校验挡下
$params['status'] = (int) ($params['status'] ?? UserStatusEnum::NORMAL->value);
$params['password'] = password_hash($params['password'], PASSWORD_DEFAULT);
// 手机号或邮箱任一重复都拒绝且必须排除软删记录deleted_at=0
// 原写法 whereOr('email',...) 是 Laravel 动态 where 的空操作(等于只按 phone 判断且不排软删),
// 这里用闭包 orWhere 正确表达「phone OR email」email 为空时不参与判重,避免误伤空邮箱账号
$email = $params['email'] ?? '';
$existsUser = $this->model::where('deleted_at', 0)
->where(function ($q) use ($params, $email) {
$q->where('phone', $params['phone']);
if ($email !== '') {
$q->orWhere('email', $email);
}
})->exists();
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),
'updated_at' => get_time(),
]);
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),
'updated_at' => get_time(),
]);
if (!$result) {
return $this->utils->errorThrow('重置密码失败');
}
return $result;
}
/**
* 获取用户信息(本人完整字段,供个人中心回填,不做手机号脱敏)
* @return mixed
* @throws Exception
*/
public function myInfo(): mixed
{
$this->with = [
'role'
];
$result = $this->getDetail($this->userId);
$result['created_day_at'] = format_time(strtotime($result['created_at']), 'Y-m-d');
// 兼容前端 Profile 组件字段
$result['realName'] = $result['nick_name'] ?? '';
$result['username'] = $result['phone'] ?? '';
return $result;
}
/**
* 个人中心更新资料(昵称/头像/邮箱/备注/手机)
* 为什么单独接口:不走管理员 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,
];
}
/**
* 获取菜单列表
* @return array
*/
public function menu(): array
{
if ($this->roleId !== 1) {
// 从redis获取菜单列表
$redis = RedisService::getInstance()->init(config('nl.redis.menu_key'))->get($this->roleId);
// $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'
];
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,
// 库里有这几列但之前没往 meta 输出,等于菜单管理里配了也不生效
'badge' => (string) $menu->badge,
'badgeType' => $this->badgeType((int) $menu->badge_type),
'badgeVariants' => $this->badgeVariants((int) $menu->badge_variants),
],
// vben5 路由的 query 是路由级字段而不是 meta且必须是对象
'query' => $this->decodeQuery((string) $menu->query),
];
}
$result = $this->utils->tree($resultMenus);
// 根据sort排序多层
usort($result, function ($a, $b) {
return $a['meta']['order'] - $b['meta']['order'];
});
// 缓存菜单列表到redis
RedisService::getInstance()->init(config('nl.redis.menu_key'))->set($this->roleId, json_encode($result), 3600);
return $result;
}
/**
* 当前账号的权限码
* @return array<int, string>
*/
public function codes(): array
{
return PermissionService::getInstance()->codes($this->roleId);
}
/**
* nl_menu.badge_type0 dot 小红点 / 1 normal 文本
*/
private function badgeType(int $type): string
{
return $type === 1 ? 'normal' : 'dot';
}
/**
* nl_menu.badge_variants0 default 1 destructive 2 primary 3 success 4 warning
*/
private function badgeVariants(int $variants): string
{
return match ($variants) {
1 => 'destructive',
2 => 'primary',
3 => 'success',
4 => 'warning',
default => 'default',
};
}
/**
* 菜单默认查询参数:库里存的是 JSON 字符串,非法值按空对象处理,别让一行坏数据把整棵菜单打挂
* @return array<string, mixed>
*/
private function decodeQuery(string $query): array
{
if (trim($query) === '') {
return [];
}
$decoded = json_decode($query, true);
return is_array($decoded) ? $decoded : [];
}
}