310 lines
10 KiB
PHP
310 lines
10 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||
// +----------------------------------------------------------------------
|
||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||
// +----------------------------------------------------------------------
|
||
// | Author: ywxapp<admin@ywxapp.cn>
|
||
// +----------------------------------------------------------------------
|
||
namespace ywxapp\model;
|
||
|
||
use think\model\concern\SoftDelete;
|
||
|
||
use ywxapp\model\BaseModel;
|
||
/**
|
||
* Backend 类
|
||
*
|
||
* @author ywxapp <admin@ywxapp.cn>
|
||
*/
|
||
class BackendAdmin extends BaseModel
|
||
{
|
||
use SoftDelete;
|
||
|
||
protected function getOptions(): array
|
||
{
|
||
return [
|
||
'strict' => false,
|
||
'name' => 'backend_admin',
|
||
'autoWriteTimestamp' => 'int',
|
||
'createTime' => 'create_at',
|
||
'updateTime' => 'update_at',
|
||
'deleteTime' => 'delete_at',
|
||
'defaultSoftDelete' => 0,
|
||
// 'dateFormat' => 'Y-m-d H:i:s',
|
||
'append' => ['status_text', 'last_login'],
|
||
'hidden' => ['password', 'delete_at'],
|
||
'readonly' => ['id'],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 管理员角色关联
|
||
*/
|
||
public function roles()
|
||
{
|
||
return $this->belongsToMany(BackendRole::class, BackendRoleAccess::class, 'role_id', 'admin_id');
|
||
}
|
||
|
||
// 密码字段自动加密
|
||
|
||
public function setPasswordAttr($value)
|
||
{
|
||
// 使用PHP内置的password_hash,无需salt字段
|
||
if (! password_get_info($value)['algo']) {
|
||
// 如果不是已哈希的密码,则进行哈希处理
|
||
return password_hash($value, PASSWORD_DEFAULT);
|
||
}
|
||
return $value;
|
||
}
|
||
|
||
// 验证密码
|
||
|
||
public function checkPassword($password)
|
||
{
|
||
return password_verify($password, $this->password);
|
||
}
|
||
|
||
// 获取状态文本
|
||
|
||
public function getStatusTextAttr()
|
||
{
|
||
$statusMap = [0 => '禁用', 1 => '正常', 2 => '锁定'];
|
||
return $statusMap[$this->status] ?? '未知';
|
||
}
|
||
|
||
// 获取最后登录描述
|
||
|
||
public function getLastLoginAttr()
|
||
{
|
||
if (empty($this->login_time)) {
|
||
return '从未登录';
|
||
}
|
||
|
||
$diff = time() - strtotime($this->login_time);
|
||
|
||
if ($diff < 60) {
|
||
return '刚刚';
|
||
} elseif ($diff < 3600) {
|
||
return floor($diff / 60) . '分钟前';
|
||
} elseif ($diff < 86400) {
|
||
return floor($diff / 3600) . '小时前';
|
||
} else {
|
||
return date('Y-m-d', strtotime($this->login_time));
|
||
}
|
||
}
|
||
|
||
// 检查账户是否被锁定
|
||
|
||
public function isLocked()
|
||
{
|
||
if ($this->status == 0) {
|
||
return true; // 已禁用
|
||
}
|
||
|
||
if ($this->fail_count >= 5 && ! empty($this->lock_time)) {
|
||
$lockUntil = strtotime($this->lock_time) + 1800; // 锁定30分钟
|
||
return time() < $lockUntil;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// 重置密码
|
||
|
||
public function resetPassword($newPassword)
|
||
{
|
||
$this->password = $newPassword; // 会触发setPasswordAttr自动加密
|
||
$this->need_reset = 0; // 标记无需重置
|
||
return $this->save();
|
||
}
|
||
|
||
// 记录登录失败(累计失败次数,超过阈值锁定)
|
||
|
||
public function recordLoginFail($ip)
|
||
{
|
||
$this->fail_count += 1;
|
||
if ($this->fail_count >= 5) {
|
||
$this->lock_time = date('Y-m-d H:i:s');
|
||
}
|
||
$this->save();
|
||
}
|
||
|
||
// 记录登录成功(重置失败计数与锁定)
|
||
|
||
public function recordLoginSuccess()
|
||
{
|
||
$this->login_time = time();
|
||
$this->fail_count = 0;
|
||
$this->lock_time = null;
|
||
$this->save();
|
||
}
|
||
|
||
// public function getAvatarAttr($value, $data)
|
||
// {
|
||
// return '/static/common/images/avatar.jpg';
|
||
// }
|
||
|
||
/**
|
||
* 获取用户有权访问的菜单树
|
||
*/
|
||
public function getAccessibleMenus()
|
||
{
|
||
$roleIds = $this->roles->column('id');
|
||
$roleNames = $this->roles->column('name');
|
||
|
||
// 超级管理员:直接可见全部后台菜单(与 getPermissionNames 的 * 语义保持一致)
|
||
$superRoleId = config('ywxapp.superAdmin', 1);
|
||
if (in_array('superadmin', $roleNames, true) || in_array($superRoleId, $roleIds, true)) {
|
||
$flatMenus = BackendPower::field('id,title,name,pid,sort,route,icon,type,addon')
|
||
->where('type', '<', '3')
|
||
->order('sort', 'asc')
|
||
->select()
|
||
->toArray();
|
||
foreach ($flatMenus as &$menu) {
|
||
// 依据权限表 addon 字段判定:空=核心后台菜单(拼 /admin/ 前缀),非空=插件菜单(完整路径直出)。
|
||
$menu['route'] = $this->buildMenuUrl($menu['route'], $menu['addon'] ?? null);
|
||
}
|
||
return BackendPower::buildTree($flatMenus);
|
||
}
|
||
|
||
if (empty($roleIds)) {
|
||
return [];
|
||
}
|
||
$permissions = BackendRolePower::where('role_id', 'in', $roleIds)
|
||
->distinct(true)
|
||
->column('power_id');
|
||
if (empty($permissions)) {
|
||
return [];
|
||
}
|
||
$flatMenus = BackendPower::field('id,title,name,pid,sort,route,icon,type,addon')
|
||
->whereIn('id', array_values($permissions))
|
||
->where('type', '<', '3') // 只获取菜单权限
|
||
->order('sort', 'asc')
|
||
->select()
|
||
->toArray();
|
||
foreach ($flatMenus as &$menu) {
|
||
// 依据权限表 addon 字段判定:空=核心后台菜单(直接拼 URL),非空=插件菜单(走 url 反转)。
|
||
$menu['route'] = $this->buildMenuUrl($menu['route'], $menu['addon'] ?? null);
|
||
}
|
||
return BackendPower::buildTree($flatMenus);
|
||
}
|
||
|
||
/**
|
||
* 获取用户所有权限标识(字符串数组)
|
||
*/
|
||
public function getAllPermissions(): array
|
||
{
|
||
$roleIds = $this->roles->column('id');
|
||
if (empty($roleIds)) {
|
||
return [];
|
||
}
|
||
return BackendRolePower::where('role_id', 'in', $roleIds)
|
||
->distinct(true)
|
||
->column('power_id');
|
||
}
|
||
|
||
/**
|
||
* 获取用户的所有权限(通过角色)
|
||
*/
|
||
public function permissions()
|
||
{
|
||
return $this->belongsToMany(BackendPower::class, BackendRolePower::class, 'power_id', 'role_id')->via('roles'); // 通过 roles 关联自动关联权限
|
||
}
|
||
|
||
/**
|
||
* 获取权限标识数组(name)
|
||
* 通过 角色→权限 关联可靠地取出当前管理员拥有的权限 key
|
||
*/
|
||
public function getPermissionNames()
|
||
{
|
||
$roleIds = $this->roles->column('id');
|
||
if (empty($roleIds)) {
|
||
return [];
|
||
}
|
||
// 拥有超级管理员角色:权限恒为 *(全部权限),校验/展示均以 * 表示
|
||
$superRoleId = config('ywxapp.superAdmin', 1);
|
||
$roleNames = $this->roles->column('name');
|
||
if (in_array('superadmin', $roleNames, true) || in_array($superRoleId, $roleIds, true)) {
|
||
return ['*'];
|
||
}
|
||
$powerIds = BackendRolePower::where('role_id', 'in', $roleIds)
|
||
->distinct(true)
|
||
->column('power_id');
|
||
if (empty($powerIds)) {
|
||
return [];
|
||
}
|
||
return BackendPower::whereIn('id', $powerIds)->column('name');
|
||
}
|
||
|
||
/**
|
||
* 检查是否有某权限
|
||
*/
|
||
public function can($permissionName)
|
||
{
|
||
$names = $this->getPermissionNames();
|
||
// 权限集合含 * 表示拥有全部权限
|
||
return in_array('*', $names, true) || in_array($permissionName, $names);
|
||
}
|
||
|
||
/**
|
||
* 菜单 URL 生成
|
||
*
|
||
* 依据权限表 addon 字段判定归属:
|
||
* - addon 为空:核心后台应用内菜单,直接按 app_map 反查 backend 应用前缀拼接
|
||
* `/{prefix}/{route}.html`,不调用 url() 反转,彻底规避插件路由抢注与
|
||
* IP/域名访问差异导致的生成异常。
|
||
* - addon 非空:插件后台菜单,走 url() 反转(全限定 `addon/backend/...`,
|
||
* 不会被插件路由同名规则抢注)。
|
||
* - 外部链接(http://、https://、//)原样返回;空 route 返回 #。
|
||
*
|
||
* @param string|null $route 菜单路由(如 links/index、framework/index、haonav/backend/links)
|
||
* @param string|null $addon 权限表 addon 字段(核心菜单为 null/空,插件菜单为插件名)
|
||
*/
|
||
protected function buildMenuUrl(?string $route, ?string $addon = null): string
|
||
{
|
||
if (! $route) {
|
||
return '#';
|
||
}
|
||
if (preg_match('#^(https?://|//)#i', $route)) {
|
||
return $route;
|
||
}
|
||
|
||
$route = ltrim($route, '/');
|
||
|
||
// 插件菜单:route 字段已是完整可访问路径(如 /appmall/backend/developer/index,
|
||
// 与插件 menu.json 约定一致),直接拼后缀输出即可。
|
||
// 切勿用 url() 反转:url() 会将该字符串当成 MVC 控制器地址解析,导致被误映射到
|
||
// 核心后台 /admin/ 前缀(如 /admin/developer/index.html),而非插件真实路径。
|
||
if (! empty($addon)) {
|
||
$suffix = config('route.url_html_suffix');
|
||
$u = '/' . $route;
|
||
if ($suffix && $suffix !== '' && ! preg_match('/\.' . preg_quote(ltrim($suffix, '.'), '/') . '$/i', $u)) {
|
||
$u .= '.' . ltrim($suffix, '.');
|
||
}
|
||
return $u;
|
||
}
|
||
|
||
// 核心后台菜单:按 app_map 反查 backend 应用前缀,直接拼接,避免 url() 反转歧义
|
||
$appMap = (array) config('app.app_map');
|
||
$appPrefix = array_search('backend', $appMap, true);
|
||
if (! $appPrefix) {
|
||
$appPrefix = 'admin';
|
||
}
|
||
|
||
return '/' . $appPrefix . '/' . $route . '.html';
|
||
}
|
||
|
||
|
||
/**
|
||
* 表结构自愈:建表 + 关键列兜底(install.sql 为事实源)。
|
||
* 查询/写入前调用,避免老库缺表导致 1146。
|
||
*/
|
||
public static function ensureSchema(): void
|
||
{
|
||
$prefix = BaseModel::currentPrefix();
|
||
foreach (['backend_admin', 'backend_log', 'backend_power', 'backend_role', 'backend_role_access', 'backend_role_power'] as $t) {
|
||
BaseModel::ensureTableFromInstall($prefix, $t);
|
||
}
|
||
}
|
||
}
|