chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+309
View File
@@ -0,0 +1,309 @@
<?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);
}
}
}