项目初始化
This commit is contained in:
117
app/common/controller/Api.php
Normal file
117
app/common/controller/Api.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use Throwable;
|
||||
use think\App;
|
||||
use think\Response;
|
||||
use think\facade\Db;
|
||||
use app\BaseController;
|
||||
use think\db\exception\PDOException;
|
||||
use think\exception\HttpResponseException;
|
||||
|
||||
/**
|
||||
* API控制器基类
|
||||
*/
|
||||
class Api extends BaseController
|
||||
{
|
||||
/**
|
||||
* 默认响应输出类型,支持json/xml/jsonp
|
||||
* @var string
|
||||
*/
|
||||
protected string $responseType = 'json';
|
||||
|
||||
/**
|
||||
* 应用站点系统设置
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $useSystemSettings = true;
|
||||
|
||||
public function __construct(App $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制器初始化方法
|
||||
* @access protected
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function initialize(): void
|
||||
{
|
||||
// 系统站点配置
|
||||
if ($this->useSystemSettings) {
|
||||
// 检查数据库连接
|
||||
try {
|
||||
Db::execute("SELECT 1");
|
||||
} catch (PDOException $e) {
|
||||
$this->error(mb_convert_encoding($e->getMessage(), 'UTF-8', 'UTF-8,GBK,GB2312,BIG5'));
|
||||
}
|
||||
|
||||
ip_check(); // ip检查
|
||||
set_timezone(); // 时区设定
|
||||
}
|
||||
|
||||
parent::initialize();
|
||||
|
||||
// 加载控制器语言包
|
||||
$langSet = $this->app->lang->getLangSet();
|
||||
$this->app->lang->load([
|
||||
app_path() . 'lang' . DIRECTORY_SEPARATOR . $langSet . DIRECTORY_SEPARATOR . (str_replace('/', DIRECTORY_SEPARATOR, $this->app->request->controllerPath)) . '.php'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作成功
|
||||
* @param string $msg 提示消息
|
||||
* @param mixed $data 返回数据
|
||||
* @param int $code 错误码
|
||||
* @param string|null $type 输出类型
|
||||
* @param array $header 发送的 header 信息
|
||||
* @param array $options Response 输出参数
|
||||
*/
|
||||
protected function success(string $msg = '', mixed $data = null, int $code = 1, ?string $type = null, array $header = [], array $options = []): void
|
||||
{
|
||||
$this->result($msg, $data, $code, $type, $header, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作失败
|
||||
* @param string $msg 提示消息
|
||||
* @param mixed $data 返回数据
|
||||
* @param int $code 错误码
|
||||
* @param string|null $type 输出类型
|
||||
* @param array $header 发送的 header 信息
|
||||
* @param array $options Response 输出参数
|
||||
*/
|
||||
protected function error(string $msg = '', mixed $data = null, int $code = 0, ?string $type = null, array $header = [], array $options = []): void
|
||||
{
|
||||
$this->result($msg, $data, $code, $type, $header, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 API 数据
|
||||
* @param string $msg 提示消息
|
||||
* @param mixed $data 返回数据
|
||||
* @param int $code 错误码
|
||||
* @param string|null $type 输出类型
|
||||
* @param array $header 发送的 header 信息
|
||||
* @param array $options Response 输出参数
|
||||
*/
|
||||
public function result(string $msg, mixed $data = null, int $code = 0, ?string $type = null, array $header = [], array $options = [])
|
||||
{
|
||||
$result = [
|
||||
'code' => $code,
|
||||
'msg' => $msg,
|
||||
'time' => $this->request->server('REQUEST_TIME'),
|
||||
'data' => $data,
|
||||
];
|
||||
|
||||
$type = $type ?: $this->responseType;
|
||||
$code = $header['statusCode'] ?? 200;
|
||||
|
||||
$response = Response::create($result, $type, $code)->header($header)->options($options);
|
||||
throw new HttpResponseException($response);
|
||||
}
|
||||
|
||||
}
|
||||
383
app/common/controller/Backend.php
Normal file
383
app/common/controller/Backend.php
Normal file
@@ -0,0 +1,383 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use Throwable;
|
||||
use think\Model;
|
||||
use think\facade\Event;
|
||||
use app\admin\library\Auth;
|
||||
use app\common\library\token\TokenExpirationException;
|
||||
|
||||
class Backend extends Api
|
||||
{
|
||||
/**
|
||||
* 无需登录的方法,访问本控制器的此方法,无需管理员登录
|
||||
* @var array
|
||||
*/
|
||||
protected array $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* 无需鉴权的方法
|
||||
* @var array
|
||||
*/
|
||||
protected array $noNeedPermission = [];
|
||||
|
||||
/**
|
||||
* 新增/编辑时,对前端发送的字段进行排除(忽略不入库)
|
||||
* @var array|string
|
||||
*/
|
||||
protected array|string $preExcludeFields = [];
|
||||
|
||||
/**
|
||||
* 权限类实例
|
||||
* @var Auth
|
||||
*/
|
||||
protected Auth $auth;
|
||||
|
||||
/**
|
||||
* 模型类实例
|
||||
* @var object
|
||||
* @phpstan-var Model
|
||||
*/
|
||||
protected object $model;
|
||||
|
||||
/**
|
||||
* 权重字段
|
||||
* @var string
|
||||
*/
|
||||
protected string $weighField = 'weigh';
|
||||
|
||||
/**
|
||||
* 默认排序
|
||||
* @var string|array id,desc 或 ['id' => 'desc']
|
||||
*/
|
||||
protected string|array $defaultSortField = [];
|
||||
|
||||
/**
|
||||
* 有序保证
|
||||
* 查询数据时总是需要指定 ORDER BY 子句,否则 MySQL 不保证排序,即先查到哪行就输出哪行且不保证多次查询中的输出顺序
|
||||
* 将以下配置作为数据有序保证(用于无排序字段时、默认排序字段相同时继续保持数据有序),不设置将自动使用 pk 字段
|
||||
* @var string|array id,desc 或 ['id' => 'desc'](有更方便的格式,此处为了保持和 $defaultSortField 属性的配置格式一致)
|
||||
*/
|
||||
protected string|array $orderGuarantee = [];
|
||||
|
||||
/**
|
||||
* 快速搜索字段
|
||||
* @var string|array
|
||||
*/
|
||||
protected string|array $quickSearchField = 'id';
|
||||
|
||||
/**
|
||||
* 是否开启模型验证
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $modelValidate = true;
|
||||
|
||||
/**
|
||||
* 是否开启模型场景验证
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $modelSceneValidate = false;
|
||||
|
||||
/**
|
||||
* 关联查询方法名,方法应定义在模型中
|
||||
* @var array
|
||||
*/
|
||||
protected array $withJoinTable = [];
|
||||
|
||||
/**
|
||||
* 关联查询JOIN方式
|
||||
* @var string
|
||||
*/
|
||||
protected string $withJoinType = 'LEFT';
|
||||
|
||||
/**
|
||||
* 开启数据限制
|
||||
* false=关闭
|
||||
* personal=仅限个人
|
||||
* allAuth=拥有某管理员所有的权限时
|
||||
* allAuthAndOthers=拥有某管理员所有的权限并且还有其他权限时
|
||||
* parent=上级分组中的管理员可查
|
||||
* 指定分组中的管理员可查,比如 $dataLimit = 2;
|
||||
* 启用请确保数据表内存在 admin_id 字段,可以查询/编辑数据的管理员为admin_id对应的管理员+数据限制所表示的管理员们
|
||||
* @var bool|string|int
|
||||
*/
|
||||
protected bool|string|int $dataLimit = false;
|
||||
|
||||
/**
|
||||
* 数据限制字段
|
||||
* @var string
|
||||
*/
|
||||
protected string $dataLimitField = 'admin_id';
|
||||
|
||||
/**
|
||||
* 数据限制开启时自动填充字段值为当前管理员id
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $dataLimitFieldAutoFill = true;
|
||||
|
||||
/**
|
||||
* 查看请求返回的主表字段控制
|
||||
* @var string|array
|
||||
*/
|
||||
protected string|array $indexField = ['*'];
|
||||
|
||||
/**
|
||||
* 引入traits
|
||||
* traits内实现了index、add、edit等方法
|
||||
*/
|
||||
use \app\admin\library\traits\Backend;
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function initialize(): void
|
||||
{
|
||||
parent::initialize();
|
||||
|
||||
$needLogin = !action_in_arr($this->noNeedLogin);
|
||||
|
||||
try {
|
||||
|
||||
// 初始化管理员鉴权实例
|
||||
$this->auth = Auth::instance();
|
||||
$token = get_auth_token();
|
||||
if ($token) $this->auth->init($token);
|
||||
|
||||
} catch (TokenExpirationException) {
|
||||
if ($needLogin) {
|
||||
$this->error(__('Token expiration'), [], 409);
|
||||
}
|
||||
}
|
||||
|
||||
if ($needLogin) {
|
||||
if (!$this->auth->isLogin()) {
|
||||
$this->error(__('Please login first'), [
|
||||
'type' => $this->auth::NEED_LOGIN
|
||||
], $this->auth::LOGIN_RESPONSE_CODE);
|
||||
}
|
||||
if (!action_in_arr($this->noNeedPermission)) {
|
||||
$routePath = ($this->app->request->controllerPath ?? '') . '/' . $this->request->action(true);
|
||||
if (!$this->auth->check($routePath)) {
|
||||
$this->error(__('You have no permission'), [], 401);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 管理员验权和登录标签位
|
||||
Event::trigger('backendInit', $this->auth);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询参数构建器
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function queryBuilder(): array
|
||||
{
|
||||
if (empty($this->model)) {
|
||||
return [];
|
||||
}
|
||||
$pk = $this->model->getPk();
|
||||
$quickSearch = $this->request->get("quickSearch/s", '');
|
||||
$limit = $this->request->get("limit/d", 10);
|
||||
$search = $this->request->get("search/a", []);
|
||||
$initKey = $this->request->get("initKey/s", $pk);
|
||||
$initValue = $this->request->get("initValue", '');
|
||||
$initOperator = $this->request->get("initOperator/s", 'in');
|
||||
|
||||
$where = [];
|
||||
$modelTable = strtolower($this->model->getTable());
|
||||
$alias[$modelTable] = parse_name(basename(str_replace('\\', '/', get_class($this->model))));
|
||||
$mainTableAlias = $alias[$modelTable] . '.';
|
||||
|
||||
// 快速搜索
|
||||
if ($quickSearch) {
|
||||
$quickSearchArr = is_array($this->quickSearchField) ? $this->quickSearchField : explode(',', $this->quickSearchField);
|
||||
foreach ($quickSearchArr as $k => $v) {
|
||||
$quickSearchArr[$k] = str_contains($v, '.') ? $v : $mainTableAlias . $v;
|
||||
}
|
||||
$where[] = [implode("|", $quickSearchArr), "LIKE", '%' . str_replace('%', '\%', $quickSearch) . '%'];
|
||||
}
|
||||
if ($initValue) {
|
||||
$where[] = [$initKey, $initOperator, $initValue];
|
||||
$limit = 999999;
|
||||
}
|
||||
|
||||
// 通用搜索组装
|
||||
foreach ($search as $field) {
|
||||
if (!is_array($field) || !isset($field['operator']) || !isset($field['field']) || !isset($field['val'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$field['operator'] = $this->getOperatorByAlias($field['operator']);
|
||||
|
||||
// 查询关联表字段,转换表别名(驼峰转小写下划线)
|
||||
if (str_contains($field['field'], '.')) {
|
||||
$fieldNameParts = explode('.', $field['field']);
|
||||
$fieldNamePartsLastKey = array_key_last($fieldNameParts);
|
||||
|
||||
// 忽略最后一个元素(字段名)
|
||||
foreach ($fieldNameParts as $fieldNamePartsKey => $fieldNamePart) {
|
||||
if ($fieldNamePartsKey !== $fieldNamePartsLastKey) {
|
||||
$fieldNameParts[$fieldNamePartsKey] = parse_name($fieldNamePart);
|
||||
}
|
||||
}
|
||||
|
||||
$fieldName = implode('.', $fieldNameParts);
|
||||
} else {
|
||||
$fieldName = $mainTableAlias . $field['field'];
|
||||
}
|
||||
|
||||
// 日期时间
|
||||
if (isset($field['render']) && $field['render'] == 'datetime') {
|
||||
if ($field['operator'] == 'RANGE') {
|
||||
$datetimeArr = explode(',', $field['val']);
|
||||
if (!isset($datetimeArr[1])) {
|
||||
continue;
|
||||
}
|
||||
$datetimeArr = array_filter(array_map("strtotime", $datetimeArr));
|
||||
$where[] = [$fieldName, str_replace('RANGE', 'BETWEEN', $field['operator']), $datetimeArr];
|
||||
continue;
|
||||
}
|
||||
$where[] = [$fieldName, '=', strtotime($field['val'])];
|
||||
continue;
|
||||
}
|
||||
|
||||
// 范围查询
|
||||
if ($field['operator'] == 'RANGE' || $field['operator'] == 'NOT RANGE') {
|
||||
$arr = explode(',', $field['val']);
|
||||
// 重新确定操作符
|
||||
if (!isset($arr[0]) || $arr[0] === '') {
|
||||
$operator = $field['operator'] == 'RANGE' ? '<=' : '>';
|
||||
$arr = $arr[1];
|
||||
} elseif (!isset($arr[1]) || $arr[1] === '') {
|
||||
$operator = $field['operator'] == 'RANGE' ? '>=' : '<';
|
||||
$arr = $arr[0];
|
||||
} else {
|
||||
$operator = str_replace('RANGE', 'BETWEEN', $field['operator']);
|
||||
}
|
||||
$where[] = [$fieldName, $operator, $arr];
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ($field['operator']) {
|
||||
case '=':
|
||||
case '<>':
|
||||
$where[] = [$fieldName, $field['operator'], (string)$field['val']];
|
||||
break;
|
||||
case 'LIKE':
|
||||
case 'NOT LIKE':
|
||||
$where[] = [$fieldName, $field['operator'], '%' . str_replace('%', '\%', $field['val']) . '%'];
|
||||
break;
|
||||
case '>':
|
||||
case '>=':
|
||||
case '<':
|
||||
case '<=':
|
||||
$where[] = [$fieldName, $field['operator'], intval($field['val'])];
|
||||
break;
|
||||
case 'FIND_IN_SET':
|
||||
if (is_array($field['val'])) {
|
||||
foreach ($field['val'] as $val) {
|
||||
$where[] = [$fieldName, 'find in set', $val];
|
||||
}
|
||||
} else {
|
||||
$where[] = [$fieldName, 'find in set', $field['val']];
|
||||
}
|
||||
break;
|
||||
case 'IN':
|
||||
case 'NOT IN':
|
||||
$where[] = [$fieldName, $field['operator'], is_array($field['val']) ? $field['val'] : explode(',', $field['val'])];
|
||||
break;
|
||||
case 'NULL':
|
||||
case 'NOT NULL':
|
||||
$where[] = [$fieldName, strtolower($field['operator']), ''];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 数据权限
|
||||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||||
if ($dataLimitAdminIds) {
|
||||
$where[] = [$mainTableAlias . $this->dataLimitField, 'in', $dataLimitAdminIds];
|
||||
}
|
||||
|
||||
return [$where, $alias, $limit, $this->queryOrderBuilder()];
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询的排序参数构建器
|
||||
*/
|
||||
public function queryOrderBuilder()
|
||||
{
|
||||
$pk = $this->model->getPk();
|
||||
$order = $this->request->get("order/s") ?: $this->defaultSortField;
|
||||
|
||||
if ($order && is_string($order)) {
|
||||
$order = explode(',', $order);
|
||||
$order = [$order[0] => $order[1] ?? 'asc'];
|
||||
}
|
||||
if (!$this->orderGuarantee) {
|
||||
$this->orderGuarantee = [$pk => 'desc'];
|
||||
} elseif (is_string($this->orderGuarantee)) {
|
||||
$this->orderGuarantee = explode(',', $this->orderGuarantee);
|
||||
$this->orderGuarantee = [$this->orderGuarantee[0] => $this->orderGuarantee[1] ?? 'asc'];
|
||||
}
|
||||
$orderGuaranteeKey = array_key_first($this->orderGuarantee);
|
||||
if (!array_key_exists($orderGuaranteeKey, $order)) {
|
||||
$order[$orderGuaranteeKey] = $this->orderGuarantee[$orderGuaranteeKey];
|
||||
}
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据权限控制-获取有权限访问的管理员Ids
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function getDataLimitAdminIds(): array
|
||||
{
|
||||
if (!$this->dataLimit || $this->auth->isSuperAdmin()) {
|
||||
return [];
|
||||
}
|
||||
$adminIds = [];
|
||||
if ($this->dataLimit == 'parent') {
|
||||
// 取得当前管理员的下级分组们
|
||||
$parentGroups = $this->auth->getAdminChildGroups();
|
||||
if ($parentGroups) {
|
||||
// 取得分组内的所有管理员
|
||||
$adminIds = $this->auth->getGroupAdmins($parentGroups);
|
||||
}
|
||||
} elseif (is_numeric($this->dataLimit) && $this->dataLimit > 0) {
|
||||
// 在组内,可查看所有,不在组内,可查看自己的
|
||||
$adminIds = $this->auth->getGroupAdmins([$this->dataLimit]);
|
||||
return in_array($this->auth->id, $adminIds) ? [] : [$this->auth->id];
|
||||
} elseif ($this->dataLimit == 'allAuth' || $this->dataLimit == 'allAuthAndOthers') {
|
||||
// 取得拥有他所有权限的分组
|
||||
$allAuthGroups = $this->auth->getAllAuthGroups($this->dataLimit);
|
||||
// 取得分组内的所有管理员
|
||||
$adminIds = $this->auth->getGroupAdmins($allAuthGroups);
|
||||
}
|
||||
$adminIds[] = $this->auth->id;
|
||||
return array_unique($adminIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从别名获取原始的逻辑运算符
|
||||
* @param string $operator 逻辑运算符别名
|
||||
* @return string 原始的逻辑运算符,无别名则原样返回
|
||||
*/
|
||||
protected function getOperatorByAlias(string $operator): string
|
||||
{
|
||||
$alias = [
|
||||
'ne' => '<>',
|
||||
'eq' => '=',
|
||||
'gt' => '>',
|
||||
'egt' => '>=',
|
||||
'lt' => '<',
|
||||
'elt' => '<=',
|
||||
];
|
||||
|
||||
return $alias[$operator] ?? $operator;
|
||||
}
|
||||
}
|
||||
73
app/common/controller/Frontend.php
Normal file
73
app/common/controller/Frontend.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use Throwable;
|
||||
use think\facade\Event;
|
||||
use app\common\library\Auth;
|
||||
use think\exception\HttpResponseException;
|
||||
use app\common\library\token\TokenExpirationException;
|
||||
|
||||
class Frontend extends Api
|
||||
{
|
||||
/**
|
||||
* 无需登录的方法
|
||||
* 访问本控制器的此方法,无需会员登录
|
||||
* @var array
|
||||
*/
|
||||
protected array $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* 无需鉴权的方法
|
||||
* @var array
|
||||
*/
|
||||
protected array $noNeedPermission = [];
|
||||
|
||||
/**
|
||||
* 权限类实例
|
||||
* @var Auth
|
||||
*/
|
||||
protected Auth $auth;
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
* @throws Throwable
|
||||
* @throws HttpResponseException
|
||||
*/
|
||||
public function initialize(): void
|
||||
{
|
||||
parent::initialize();
|
||||
|
||||
$needLogin = !action_in_arr($this->noNeedLogin);
|
||||
|
||||
try {
|
||||
|
||||
// 初始化会员鉴权实例
|
||||
$this->auth = Auth::instance();
|
||||
$token = get_auth_token(['ba', 'user', 'token']);
|
||||
if ($token) $this->auth->init($token);
|
||||
|
||||
} catch (TokenExpirationException) {
|
||||
if ($needLogin) {
|
||||
$this->error(__('Token expiration'), [], 409);
|
||||
}
|
||||
}
|
||||
|
||||
if ($needLogin) {
|
||||
if (!$this->auth->isLogin()) {
|
||||
$this->error(__('Please login first'), [
|
||||
'type' => $this->auth::NEED_LOGIN
|
||||
], $this->auth::LOGIN_RESPONSE_CODE);
|
||||
}
|
||||
if (!action_in_arr($this->noNeedPermission)) {
|
||||
$routePath = ($this->app->request->controllerPath ?? '') . '/' . $this->request->action(true);
|
||||
if (!$this->auth->check($routePath)) {
|
||||
$this->error(__('You have no permission'), [], 401);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 会员验权和登录标签位
|
||||
Event::trigger('frontendInit', $this->auth);
|
||||
}
|
||||
}
|
||||
139
app/common/event/Security.php
Normal file
139
app/common/event/Security.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\event;
|
||||
|
||||
use Throwable;
|
||||
use think\Request;
|
||||
use ba\TableManager;
|
||||
use think\facade\Db;
|
||||
use think\facade\Log;
|
||||
use app\admin\library\Auth;
|
||||
use app\admin\model\SensitiveDataLog;
|
||||
use app\admin\model\DataRecycle;
|
||||
use app\admin\model\DataRecycleLog;
|
||||
use app\admin\model\SensitiveData;
|
||||
|
||||
class Security
|
||||
{
|
||||
protected array $listenAction = ['edit', 'del'];
|
||||
|
||||
public function handle(Request $request): bool
|
||||
{
|
||||
$action = $request->action(true);
|
||||
if (!in_array($action, $this->listenAction) || (!$request->isPost() && !$request->isDelete())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($action == 'del') {
|
||||
$dataIds = $request->param('ids');
|
||||
try {
|
||||
$recycle = DataRecycle::where('status', 1)
|
||||
->where('controller_as', $request->controllerPath)
|
||||
->find();
|
||||
if (!$recycle) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$recycleData = Db::connect(TableManager::getConnection($recycle['connection']))
|
||||
->name($recycle['data_table'])
|
||||
->whereIn($recycle['primary_key'], $dataIds)
|
||||
->select()
|
||||
->toArray();
|
||||
$recycleDataArr = [];
|
||||
$auth = Auth::instance();
|
||||
$adminId = $auth->isLogin() ? $auth->id : 0;
|
||||
foreach ($recycleData as $recycleDatum) {
|
||||
$recycleDataArr[] = [
|
||||
'admin_id' => $adminId,
|
||||
'recycle_id' => $recycle['id'],
|
||||
'data' => json_encode($recycleDatum, JSON_UNESCAPED_UNICODE),
|
||||
'connection' => $recycle['connection'],
|
||||
'data_table' => $recycle['data_table'],
|
||||
'primary_key' => $recycle['primary_key'],
|
||||
'ip' => $request->ip(),
|
||||
'useragent' => substr($request->server('HTTP_USER_AGENT'), 0, 255),
|
||||
];
|
||||
}
|
||||
if (!$recycleDataArr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// saveAll 方法自带事务
|
||||
$dataRecycleLogModel = new DataRecycleLog();
|
||||
if (!$dataRecycleLogModel->saveAll($recycleDataArr)) {
|
||||
Log::record('[ DataSecurity ] Failed to recycle data:' . var_export($recycleDataArr, true), 'warning');
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
Log::record('[ DataSecurity ]' . $e->getMessage(), 'warning');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
$sensitiveData = SensitiveData::where('status', 1)
|
||||
->where('controller_as', $request->controllerPath)
|
||||
->find();
|
||||
if (!$sensitiveData) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$sensitiveData = $sensitiveData->toArray();
|
||||
$dataId = $request->param($sensitiveData['primary_key']);
|
||||
$editData = Db::connect(TableManager::getConnection($sensitiveData['connection']))
|
||||
->name($sensitiveData['data_table'])
|
||||
->field(array_keys($sensitiveData['data_fields']))
|
||||
->where($sensitiveData['primary_key'], $dataId)
|
||||
->find();
|
||||
if (!$editData) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$auth = Auth::instance();
|
||||
$adminId = $auth->isLogin() ? $auth->id : 0;
|
||||
$newData = $request->post();
|
||||
foreach ($sensitiveData['data_fields'] as $field => $title) {
|
||||
if (isset($editData[$field]) && isset($newData[$field]) && $editData[$field] != $newData[$field]) {
|
||||
|
||||
/*
|
||||
* 其他跳过规则可添加至此处
|
||||
* 1. 如果字段名中包含 password,修改值为空则忽略,修改值不为空,则密码记录为 ******
|
||||
*/
|
||||
if (stripos('password', $field) !== false) {
|
||||
if (!$newData[$field]) {
|
||||
continue;
|
||||
} else {
|
||||
$newData[$field] = "******";
|
||||
}
|
||||
}
|
||||
|
||||
$sensitiveDataLog[] = [
|
||||
'admin_id' => $adminId,
|
||||
'sensitive_id' => $sensitiveData['id'],
|
||||
'connection' => $sensitiveData['connection'],
|
||||
'data_table' => $sensitiveData['data_table'],
|
||||
'primary_key' => $sensitiveData['primary_key'],
|
||||
'data_field' => $field,
|
||||
'data_comment' => $title,
|
||||
'id_value' => $dataId,
|
||||
'before' => $editData[$field],
|
||||
'after' => $newData[$field],
|
||||
'ip' => $request->ip(),
|
||||
'useragent' => substr($request->server('HTTP_USER_AGENT'), 0, 255),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($sensitiveDataLog) || !$sensitiveDataLog) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$sensitiveDataLogModel = new SensitiveDataLog();
|
||||
if (!$sensitiveDataLogModel->saveAll($sensitiveDataLog)) {
|
||||
Log::record('[ DataSecurity ] Sensitive data recording failed:' . var_export($sensitiveDataLog, true), 'warning');
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
Log::record('[ DataSecurity ]' . $e->getMessage(), 'warning');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
24
app/common/facade/Token.php
Normal file
24
app/common/facade/Token.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\facade;
|
||||
|
||||
use think\Facade;
|
||||
use app\common\library\token\Driver;
|
||||
|
||||
/**
|
||||
* Token 门面类
|
||||
* @see Driver
|
||||
* @method array get(string $token) static 获取 token 的数据
|
||||
* @method bool set(string $token, string $type, int $userId, int $expire = null) static 设置 token
|
||||
* @method bool check(string $token, string $type, int $userId) static 检查token是否有效
|
||||
* @method bool delete(string $token) static 删除一个token
|
||||
* @method bool clear(string $type, int $userId) static 清理一个用户的所有token
|
||||
* @method void tokenExpirationCheck(array $token) static 检查一个token是否过期,过期则抛出token过期异常
|
||||
*/
|
||||
class Token extends Facade
|
||||
{
|
||||
protected static function getFacadeClass(): string
|
||||
{
|
||||
return 'app\common\library\Token';
|
||||
}
|
||||
}
|
||||
562
app/common/library/Auth.php
Normal file
562
app/common/library/Auth.php
Normal file
@@ -0,0 +1,562 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use Throwable;
|
||||
use ba\Random;
|
||||
use think\facade\Db;
|
||||
use think\facade\Event;
|
||||
use think\facade\Config;
|
||||
use app\common\model\User;
|
||||
use think\facade\Validate;
|
||||
use app\common\facade\Token;
|
||||
|
||||
/**
|
||||
* 公共权限类(会员权限类)
|
||||
* @property int $id 会员ID
|
||||
* @property string $username 会员用户名
|
||||
* @property string $nickname 会员昵称
|
||||
* @property string $email 会员邮箱
|
||||
* @property string $mobile 会员手机号
|
||||
* @property string $password 密码密文
|
||||
* @property string $salt 密码盐
|
||||
*/
|
||||
class Auth extends \ba\Auth
|
||||
{
|
||||
/**
|
||||
* 需要登录时/无需登录时的响应状态代码
|
||||
*/
|
||||
public const LOGIN_RESPONSE_CODE = 303;
|
||||
|
||||
/**
|
||||
* 需要登录标记 - 前台应清理 token、记录当前路由 path、跳转到登录页
|
||||
*/
|
||||
public const NEED_LOGIN = 'need login';
|
||||
|
||||
/**
|
||||
* 已经登录标记 - 前台应跳转到基础路由
|
||||
*/
|
||||
public const LOGGED_IN = 'logged in';
|
||||
|
||||
/**
|
||||
* token 入库 type
|
||||
*/
|
||||
public const TOKEN_TYPE = 'user';
|
||||
|
||||
/**
|
||||
* 是否登录
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $loginEd = false;
|
||||
|
||||
/**
|
||||
* 错误消息
|
||||
* @var string
|
||||
*/
|
||||
protected string $error = '';
|
||||
|
||||
/**
|
||||
* Model实例
|
||||
* @var ?User
|
||||
*/
|
||||
protected ?User $model = null;
|
||||
|
||||
/**
|
||||
* 令牌
|
||||
* @var string
|
||||
*/
|
||||
protected string $token = '';
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
* @var string
|
||||
*/
|
||||
protected string $refreshToken = '';
|
||||
|
||||
/**
|
||||
* 令牌默认有效期
|
||||
* 可在 config/buildadmin.php 内修改默认值
|
||||
* @var int
|
||||
*/
|
||||
protected int $keepTime = 86400;
|
||||
|
||||
/**
|
||||
* 刷新令牌有效期
|
||||
* @var int
|
||||
*/
|
||||
protected int $refreshTokenKeepTime = 2592000;
|
||||
|
||||
/**
|
||||
* 允许输出的字段
|
||||
* @var array
|
||||
*/
|
||||
protected array $allowFields = ['id', 'username', 'nickname', 'email', 'mobile', 'avatar', 'gender', 'birthday', 'money', 'score', 'join_time', 'motto', 'last_login_time', 'last_login_ip'];
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
parent::__construct(array_merge([
|
||||
'auth_group' => 'user_group', // 用户组数据表名
|
||||
'auth_group_access' => '', // 用户-用户组关系表(关系字段)
|
||||
'auth_rule' => 'user_rule', // 权限规则表
|
||||
], $config));
|
||||
|
||||
$this->setKeepTime((int)Config::get('buildadmin.user_token_keep_time'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 魔术方法-会员信息字段
|
||||
* @param $name
|
||||
* @return mixed 字段信息
|
||||
*/
|
||||
public function __get($name): mixed
|
||||
{
|
||||
return $this->model?->$name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
* @access public
|
||||
* @param array $options 传递给 /ba/Auth 的参数
|
||||
* @return Auth
|
||||
*/
|
||||
public static function instance(array $options = []): Auth
|
||||
{
|
||||
$request = request();
|
||||
if (!isset($request->userAuth)) {
|
||||
$request->userAuth = new static($options);
|
||||
}
|
||||
return $request->userAuth;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据Token初始化会员登录态
|
||||
* @param $token
|
||||
* @return bool
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function init($token): bool
|
||||
{
|
||||
$tokenData = Token::get($token);
|
||||
if ($tokenData) {
|
||||
|
||||
/**
|
||||
* 过期检查,过期则抛出 @see TokenExpirationException
|
||||
*/
|
||||
Token::tokenExpirationCheck($tokenData);
|
||||
|
||||
$userId = intval($tokenData['user_id']);
|
||||
if ($tokenData['type'] == self::TOKEN_TYPE && $userId > 0) {
|
||||
$this->model = User::where('id', $userId)->find();
|
||||
if (!$this->model) {
|
||||
$this->setError('Account not exist');
|
||||
return false;
|
||||
}
|
||||
if ($this->model->status != 'enable') {
|
||||
$this->setError('Account disabled');
|
||||
return false;
|
||||
}
|
||||
$this->token = $token;
|
||||
$this->loginSuccessful();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->setError('Token login failed');
|
||||
$this->reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员注册,可使用关键词参数方式调用:$auth->register('u18888888888', email: 'test@qq.com')
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @param string $mobile
|
||||
* @param string $email
|
||||
* @param int $group 会员分组 ID 号
|
||||
* @param array $extend 扩展数据,如 ['status' => 'disable']
|
||||
* @return bool
|
||||
*/
|
||||
public function register(string $username, string $password = '', string $mobile = '', string $email = '', int $group = 1, array $extend = []): bool
|
||||
{
|
||||
$validate = Validate::rule([
|
||||
'email|' . __('Email') => 'email|unique:user',
|
||||
'mobile|' . __('Mobile') => 'mobile|unique:user',
|
||||
'username|' . __('Username') => 'require|regex:^[a-zA-Z][a-zA-Z0-9_]{2,15}$|unique:user',
|
||||
'password|' . __('Password') => 'regex:^(?!.*[&<>"\'\n\r]).{6,32}$',
|
||||
]);
|
||||
$params = [
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'mobile' => $mobile,
|
||||
'email' => $email,
|
||||
];
|
||||
if (!$validate->check($params)) {
|
||||
$this->setError($validate->getError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 用户昵称
|
||||
$nickname = preg_replace_callback('/1[3-9]\d{9}/', function ($matches) {
|
||||
// 对 username 中出现的所有手机号进行脱敏处理
|
||||
$mobile = $matches[0];
|
||||
return substr($mobile, 0, 3) . '****' . substr($mobile, 7);
|
||||
}, $username);
|
||||
|
||||
$ip = request()->ip();
|
||||
$time = time();
|
||||
$data = [
|
||||
'group_id' => $group,
|
||||
'nickname' => $nickname,
|
||||
'join_ip' => $ip,
|
||||
'join_time' => $time,
|
||||
'last_login_ip' => $ip,
|
||||
'last_login_time' => $time,
|
||||
'status' => 'enable', // 状态:enable=启用,disable=禁用,使用 string 存储可以自定义其他状态
|
||||
];
|
||||
$data = array_merge($params, $data);
|
||||
$data = array_merge($data, $extend);
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$this->model = User::create($data);
|
||||
$this->token = Random::uuid();
|
||||
Token::set($this->token, self::TOKEN_TYPE, $this->model->id, $this->keepTime);
|
||||
Db::commit();
|
||||
|
||||
if ($password) {
|
||||
$this->model->resetPassword($this->model->id, $password);
|
||||
}
|
||||
|
||||
Event::trigger('userRegisterSuccess', $this->model);
|
||||
} catch (Throwable $e) {
|
||||
$this->setError($e->getMessage());
|
||||
Db::rollback();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员登录
|
||||
* @param string $username 用户名
|
||||
* @param string $password 密码
|
||||
* @param bool $keep 是否保持登录
|
||||
* @return bool
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function login(string $username, string $password, bool $keep): bool
|
||||
{
|
||||
// 判断账户类型
|
||||
$accountType = false;
|
||||
$validate = Validate::rule([
|
||||
'mobile' => 'mobile',
|
||||
'email' => 'email',
|
||||
'username' => 'regex:^[a-zA-Z][a-zA-Z0-9_]{2,15}$',
|
||||
]);
|
||||
if ($validate->check(['mobile' => $username])) $accountType = 'mobile';
|
||||
if ($validate->check(['email' => $username])) $accountType = 'email';
|
||||
if ($validate->check(['username' => $username])) $accountType = 'username';
|
||||
if (!$accountType) {
|
||||
$this->setError('Account not exist');
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->model = User::where($accountType, $username)->find();
|
||||
if (!$this->model) {
|
||||
$this->setError('Account not exist');
|
||||
return false;
|
||||
}
|
||||
if ($this->model->status == 'disable') {
|
||||
$this->setError('Account disabled');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 登录失败重试检查
|
||||
$userLoginRetry = Config::get('buildadmin.user_login_retry');
|
||||
if ($userLoginRetry && $this->model->last_login_time) {
|
||||
// 重置失败次数
|
||||
if ($this->model->login_failure > 0 && time() - $this->model->last_login_time >= 86400) {
|
||||
$this->model->login_failure = 0;
|
||||
$this->model->save();
|
||||
|
||||
// 重获模型实例,避免单实例多次更新
|
||||
$this->model = User::where($accountType, $username)->find();
|
||||
}
|
||||
|
||||
if ($this->model->login_failure >= $userLoginRetry) {
|
||||
$this->setError('Please try again after 1 day');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 密码检查
|
||||
if (!verify_password($password, $this->model->password, ['salt' => $this->model->salt])) {
|
||||
$this->loginFailed();
|
||||
$this->setError('Password is incorrect');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 清理 token
|
||||
if (Config::get('buildadmin.user_sso')) {
|
||||
Token::clear(self::TOKEN_TYPE, $this->model->id);
|
||||
Token::clear(self::TOKEN_TYPE . '-refresh', $this->model->id);
|
||||
}
|
||||
|
||||
if ($keep) {
|
||||
$this->setRefreshToken($this->refreshTokenKeepTime);
|
||||
}
|
||||
$this->loginSuccessful();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接登录会员账号
|
||||
* @param int $userId 用户ID
|
||||
* @return bool
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function direct(int $userId): bool
|
||||
{
|
||||
$this->model = User::find($userId);
|
||||
if (!$this->model) return false;
|
||||
if (Config::get('buildadmin.user_sso')) {
|
||||
Token::clear(self::TOKEN_TYPE, $this->model->id);
|
||||
Token::clear(self::TOKEN_TYPE . '-refresh', $this->model->id);
|
||||
}
|
||||
return $this->loginSuccessful();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查旧密码是否正确
|
||||
* @param $password
|
||||
* @return bool
|
||||
* @deprecated 请使用 verify_password 公共函数代替
|
||||
*/
|
||||
public function checkPassword($password): bool
|
||||
{
|
||||
return verify_password($password, $this->model->password, ['salt' => $this->model->salt]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录成功
|
||||
* @return bool
|
||||
*/
|
||||
public function loginSuccessful(): bool
|
||||
{
|
||||
if (!$this->model) {
|
||||
return false;
|
||||
}
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
$this->model->login_failure = 0;
|
||||
$this->model->last_login_time = time();
|
||||
$this->model->last_login_ip = request()->ip();
|
||||
$this->model->save();
|
||||
$this->loginEd = true;
|
||||
|
||||
if (!$this->token) {
|
||||
$this->token = Random::uuid();
|
||||
Token::set($this->token, self::TOKEN_TYPE, $this->model->id, $this->keepTime);
|
||||
}
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
$this->setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录失败
|
||||
* @return bool
|
||||
*/
|
||||
public function loginFailed(): bool
|
||||
{
|
||||
if (!$this->model) return false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
$this->model->login_failure++;
|
||||
$this->model->last_login_time = time();
|
||||
$this->model->last_login_ip = request()->ip();
|
||||
$this->model->save();
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
$this->setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
return $this->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
* @return bool
|
||||
*/
|
||||
public function logout(): bool
|
||||
{
|
||||
if (!$this->loginEd) {
|
||||
$this->setError('You are not logged in');
|
||||
return false;
|
||||
}
|
||||
return $this->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否登录
|
||||
* @return bool
|
||||
*/
|
||||
public function isLogin(): bool
|
||||
{
|
||||
return $this->loginEd;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员模型
|
||||
* @return User
|
||||
*/
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员Token
|
||||
* @return string
|
||||
*/
|
||||
public function getToken(): string
|
||||
{
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置刷新Token
|
||||
* @param int $keepTime
|
||||
* @return void
|
||||
*/
|
||||
public function setRefreshToken(int $keepTime = 0): void
|
||||
{
|
||||
$this->refreshToken = Random::uuid();
|
||||
Token::set($this->refreshToken, self::TOKEN_TYPE . '-refresh', $this->model->id, $keepTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员刷新Token
|
||||
* @return string
|
||||
*/
|
||||
public function getRefreshToken(): string
|
||||
{
|
||||
return $this->refreshToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员信息 - 只输出允许输出的字段
|
||||
* @return array
|
||||
*/
|
||||
public function getUserInfo(): array
|
||||
{
|
||||
if (!$this->model) return [];
|
||||
$info = $this->model->toArray();
|
||||
$info = array_intersect_key($info, array_flip($this->getAllowFields()));
|
||||
$info['token'] = $this->getToken();
|
||||
$info['refresh_token'] = $this->getRefreshToken();
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取允许输出字段
|
||||
* @return array
|
||||
*/
|
||||
public function getAllowFields(): array
|
||||
{
|
||||
return $this->allowFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置允许输出字段
|
||||
* @param $fields
|
||||
* @return void
|
||||
*/
|
||||
public function setAllowFields($fields): void
|
||||
{
|
||||
$this->allowFields = $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置Token有效期
|
||||
* @param int $keepTime
|
||||
* @return void
|
||||
*/
|
||||
public function setKeepTime(int $keepTime = 0): void
|
||||
{
|
||||
$this->keepTime = $keepTime;
|
||||
}
|
||||
|
||||
public function check(string $name, int $uid = 0, string $relation = 'or', string $mode = 'url'): bool
|
||||
{
|
||||
return parent::check($name, $uid ?: $this->id, $relation, $mode);
|
||||
}
|
||||
|
||||
public function getRuleList(int $uid = 0): array
|
||||
{
|
||||
return parent::getRuleList($uid ?: $this->id);
|
||||
}
|
||||
|
||||
public function getRuleIds(int $uid = 0): array
|
||||
{
|
||||
return parent::getRuleIds($uid ?: $this->id);
|
||||
}
|
||||
|
||||
public function getMenus(int $uid = 0): array
|
||||
{
|
||||
return parent::getMenus($uid ?: $this->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是拥有所有权限的会员
|
||||
* @return bool
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function isSuperUser(): bool
|
||||
{
|
||||
return in_array('*', $this->getRuleIds());
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置错误消息
|
||||
* @param string $error
|
||||
* @return Auth
|
||||
*/
|
||||
public function setError(string $error): Auth
|
||||
{
|
||||
$this->error = $error;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误消息
|
||||
* @return string
|
||||
*/
|
||||
public function getError(): string
|
||||
{
|
||||
return $this->error ? __($this->error) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 属性重置(注销、登录失败、重新初始化等将单例数据销毁)
|
||||
*/
|
||||
protected function reset(bool $deleteToken = true): bool
|
||||
{
|
||||
if ($deleteToken && $this->token) {
|
||||
Token::delete($this->token);
|
||||
}
|
||||
|
||||
$this->token = '';
|
||||
$this->loginEd = false;
|
||||
$this->model = null;
|
||||
$this->refreshToken = '';
|
||||
$this->setError('');
|
||||
$this->setKeepTime((int)Config::get('buildadmin.user_token_keep_time'));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
71
app/common/library/Email.php
Normal file
71
app/common/library/Email.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use Throwable;
|
||||
use think\facade\Lang;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
|
||||
/**
|
||||
* 邮件类
|
||||
* 继承PHPMailer并初始化好了站点系统配置中的邮件配置信息
|
||||
*/
|
||||
class Email extends PHPMailer
|
||||
{
|
||||
/**
|
||||
* 是否已在管理后台配置好邮件服务
|
||||
* @var bool
|
||||
*/
|
||||
public bool $configured = false;
|
||||
|
||||
/**
|
||||
* 默认配置
|
||||
* @var array
|
||||
*/
|
||||
public array $options = [
|
||||
'charset' => 'utf-8', //编码格式
|
||||
'debug' => true, //调式模式
|
||||
'lang' => 'zh_cn',
|
||||
];
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param array $options
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$this->options = array_merge($this->options, $options);
|
||||
|
||||
parent::__construct($this->options['debug']);
|
||||
$langSet = Lang::getLangSet();
|
||||
if ($langSet == 'zh-cn' || !$langSet) $langSet = 'zh_cn';
|
||||
$this->options['lang'] = $this->options['lang'] ?: $langSet;
|
||||
|
||||
$this->setLanguage($this->options['lang'], root_path() . 'vendor' . DIRECTORY_SEPARATOR . 'phpmailer' . DIRECTORY_SEPARATOR . 'phpmailer' . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR);
|
||||
$this->CharSet = $this->options['charset'];
|
||||
|
||||
$sysMailConfig = get_sys_config('', 'mail');
|
||||
$this->configured = true;
|
||||
foreach ($sysMailConfig as $item) {
|
||||
if (!$item) {
|
||||
$this->configured = false;
|
||||
}
|
||||
}
|
||||
if ($this->configured) {
|
||||
$this->Host = $sysMailConfig['smtp_server'];
|
||||
$this->SMTPAuth = true;
|
||||
$this->Username = $sysMailConfig['smtp_user'];
|
||||
$this->Password = $sysMailConfig['smtp_pass'];
|
||||
$this->SMTPSecure = $sysMailConfig['smtp_verification'] == 'SSL' ? self::ENCRYPTION_SMTPS : self::ENCRYPTION_STARTTLS;
|
||||
$this->Port = $sysMailConfig['smtp_port'];
|
||||
|
||||
$this->setFrom($sysMailConfig['smtp_sender_mail'], $sysMailConfig['smtp_user']);
|
||||
}
|
||||
}
|
||||
|
||||
public function setSubject($subject): void
|
||||
{
|
||||
$this->Subject = "=?utf-8?B?" . base64_encode($subject) . "?=";
|
||||
}
|
||||
}
|
||||
156
app/common/library/Menu.php
Normal file
156
app/common/library/Menu.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use Throwable;
|
||||
use app\admin\model\AdminRule;
|
||||
use app\admin\model\UserRule;
|
||||
|
||||
/**
|
||||
* 菜单规则管理类
|
||||
*/
|
||||
class Menu
|
||||
{
|
||||
/**
|
||||
* @param array $menu
|
||||
* @param int|string $parent 父级规则name或id
|
||||
* @param string $mode 添加模式(规则重复时):cover=覆盖旧菜单,rename=重命名新菜单,ignore=忽略
|
||||
* @param string $position 位置:backend=后台,frontend=前台
|
||||
* @return void
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function create(array $menu, int|string $parent = 0, string $mode = 'cover', string $position = 'backend'): void
|
||||
{
|
||||
$pid = 0;
|
||||
$model = $position == 'backend' ? new AdminRule() : new UserRule();
|
||||
$parentRule = $model->where((is_numeric($parent) ? 'id' : 'name'), $parent)->find();
|
||||
if ($parentRule) {
|
||||
$pid = $parentRule['id'];
|
||||
}
|
||||
foreach ($menu as $item) {
|
||||
if (!self::requiredAttrCheck($item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 属性
|
||||
$item['status'] = 1;
|
||||
if (!isset($item['pid'])) {
|
||||
$item['pid'] = $pid;
|
||||
}
|
||||
|
||||
$sameOldMenu = $model->where('name', $item['name'])->find();
|
||||
if ($sameOldMenu) {
|
||||
// 存在相同名称的菜单规则
|
||||
if ($mode == 'cover') {
|
||||
$sameOldMenu->save($item);
|
||||
} elseif ($mode == 'rename') {
|
||||
$count = $model->where('name', $item['name'])->count();
|
||||
$item['name'] = $item['name'] . '-CONFLICT-' . $count;
|
||||
$item['path'] = $item['path'] . '-CONFLICT-' . $count;
|
||||
$item['title'] = $item['title'] . '-CONFLICT-' . $count;
|
||||
$sameOldMenu = $model->create($item);
|
||||
} elseif ($mode == 'ignore') {
|
||||
// 忽略同名菜单时,当前 pid 下没有同名菜单,则创建同名新菜单,以保证所有新增菜单的上下级结构
|
||||
$sameOldMenu = $model
|
||||
->where('name', $item['name'])
|
||||
->where('pid', $item['pid'])
|
||||
->find();
|
||||
|
||||
if (!$sameOldMenu) {
|
||||
$sameOldMenu = $model->create($item);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$sameOldMenu = $model->create($item);
|
||||
}
|
||||
if (!empty($item['children'])) {
|
||||
self::create($item['children'], $sameOldMenu['id'], $mode, $position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删菜单
|
||||
* @param string|int $id 规则name或id
|
||||
* @param bool $recursion 是否递归删除子级菜单、是否删除自身,是否删除上级空菜单
|
||||
* @param string $position 位置:backend=后台,frontend=前台
|
||||
* @return bool
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function delete(string|int $id, bool $recursion = false, string $position = 'backend'): bool
|
||||
{
|
||||
if (!$id) {
|
||||
return true;
|
||||
}
|
||||
$model = $position == 'backend' ? new AdminRule() : new UserRule();
|
||||
$menuRule = $model->where((is_numeric($id) ? 'id' : 'name'), $id)->find();
|
||||
if (!$menuRule) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$children = $model->where('pid', $menuRule['id'])->select()->toArray();
|
||||
if ($recursion && $children) {
|
||||
foreach ($children as $child) {
|
||||
self::delete($child['id'], true, $position);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$children || $recursion) {
|
||||
$menuRule->delete();
|
||||
self::delete($menuRule->pid, false, $position);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用菜单
|
||||
* @param string|int $id 规则name或id
|
||||
* @param string $position 位置:backend=后台,frontend=前台
|
||||
* @return bool
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function enable(string|int $id, string $position = 'backend'): bool
|
||||
{
|
||||
$model = $position == 'backend' ? new AdminRule() : new UserRule();
|
||||
$menuRule = $model->where((is_numeric($id) ? 'id' : 'name'), $id)->find();
|
||||
if (!$menuRule) {
|
||||
return false;
|
||||
}
|
||||
$menuRule->status = 1;
|
||||
$menuRule->save();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用菜单
|
||||
* @param string|int $id 规则name或id
|
||||
* @param string $position 位置:backend=后台,frontend=前台
|
||||
* @return bool
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function disable(string|int $id, string $position = 'backend'): bool
|
||||
{
|
||||
$model = $position == 'backend' ? new AdminRule() : new UserRule();
|
||||
$menuRule = $model->where((is_numeric($id) ? 'id' : 'name'), $id)->find();
|
||||
if (!$menuRule) {
|
||||
return false;
|
||||
}
|
||||
$menuRule->status = 0;
|
||||
$menuRule->save();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function requiredAttrCheck($menu): bool
|
||||
{
|
||||
$attrs = ['type', 'title', 'name'];
|
||||
foreach ($attrs as $attr) {
|
||||
if (!array_key_exists($attr, $menu)) {
|
||||
return false;
|
||||
}
|
||||
if (!$menu[$attr]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
87
app/common/library/SnowFlake.php
Normal file
87
app/common/library/SnowFlake.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
/**
|
||||
* 雪花ID生成类
|
||||
*/
|
||||
class SnowFlake
|
||||
{
|
||||
/**
|
||||
* 起始时间戳
|
||||
* @var int
|
||||
*/
|
||||
private const EPOCH = 1672502400000;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private const max41bit = 1099511627775;
|
||||
|
||||
/**
|
||||
* 机器节点 10bit
|
||||
* @var int
|
||||
*/
|
||||
protected static int $machineId = 1;
|
||||
|
||||
/**
|
||||
* 序列号
|
||||
* @var int
|
||||
*/
|
||||
protected static int $count = 0;
|
||||
|
||||
/**
|
||||
* 最后一次生成ID的时间偏移量
|
||||
* @var int
|
||||
*/
|
||||
protected static int $last = 0;
|
||||
|
||||
/**
|
||||
* 设置机器节点
|
||||
* @param int $mId 机器节点id
|
||||
* @return void
|
||||
*/
|
||||
public static function setMachineId(int $mId): void
|
||||
{
|
||||
self::$machineId = $mId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成雪花ID
|
||||
* @return float|int
|
||||
*/
|
||||
public static function generateParticle(): float|int
|
||||
{
|
||||
// 当前时间 42bit
|
||||
$time = (int)floor(microtime(true) * 1000);
|
||||
|
||||
// 时间偏移量
|
||||
$time -= self::EPOCH;
|
||||
|
||||
// 起始时间戳加上时间偏移量并转为二进制
|
||||
$base = decbin(self::max41bit + $time);
|
||||
|
||||
// 追加节点机器id
|
||||
if (!is_null(self::$machineId)) {
|
||||
$machineId = str_pad(decbin(self::$machineId), 10, "0", STR_PAD_LEFT);
|
||||
$base .= $machineId;
|
||||
}
|
||||
|
||||
// 序列号
|
||||
if ($time == self::$last) {
|
||||
self::$count++;
|
||||
} else {
|
||||
self::$count = 0;
|
||||
}
|
||||
|
||||
// 追加序列号部分
|
||||
$sequence = str_pad(decbin(self::$count), 12, "0", STR_PAD_LEFT);
|
||||
$base .= $sequence;
|
||||
|
||||
// 保存生成ID的时间偏移量
|
||||
self::$last = $time;
|
||||
|
||||
// 返回64bit二进制数的十进制标识
|
||||
return bindec($base);
|
||||
}
|
||||
}
|
||||
232
app/common/library/Token.php
Normal file
232
app/common/library/Token.php
Normal file
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use think\helper\Arr;
|
||||
use think\helper\Str;
|
||||
use think\facade\Config;
|
||||
use InvalidArgumentException;
|
||||
use app\common\library\token\TokenExpirationException;
|
||||
|
||||
/**
|
||||
* Token 管理类
|
||||
*/
|
||||
class Token
|
||||
{
|
||||
/**
|
||||
* Token 实例
|
||||
* @var array
|
||||
* @uses Token 数组项
|
||||
*/
|
||||
public array $instance = [];
|
||||
|
||||
/**
|
||||
* token驱动类句柄
|
||||
* @var ?object
|
||||
*/
|
||||
public ?object $handler = null;
|
||||
|
||||
/**
|
||||
* 驱动类命名空间
|
||||
* @var string
|
||||
*/
|
||||
protected string $namespace = '\\app\\common\\library\\token\\driver\\';
|
||||
|
||||
/**
|
||||
* 获取驱动句柄
|
||||
* @param string|null $name
|
||||
* @return object
|
||||
*/
|
||||
public function getDriver(?string $name = null): object
|
||||
{
|
||||
if (!is_null($this->handler)) {
|
||||
return $this->handler;
|
||||
}
|
||||
$name = $name ?: $this->getDefaultDriver();
|
||||
|
||||
if (is_null($name)) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Unable to resolve NULL driver for [%s].',
|
||||
static::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->createDriver($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建驱动句柄
|
||||
* @param string $name
|
||||
* @return object
|
||||
*/
|
||||
protected function createDriver(string $name): object
|
||||
{
|
||||
$type = $this->resolveType($name);
|
||||
|
||||
$method = 'create' . Str::studly($type) . 'Driver';
|
||||
|
||||
$params = $this->resolveParams($name);
|
||||
|
||||
if (method_exists($this, $method)) {
|
||||
return $this->$method(...$params);
|
||||
}
|
||||
|
||||
$class = $this->resolveClass($type);
|
||||
|
||||
if (isset($this->instance[$type])) {
|
||||
return $this->instance[$type];
|
||||
}
|
||||
|
||||
return new $class(...$params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认驱动
|
||||
* @return string
|
||||
*/
|
||||
protected function getDefaultDriver(): string
|
||||
{
|
||||
return $this->getConfig('default');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取驱动配置
|
||||
* @param string|null $name 要获取的配置项,不传递获取完整token配置
|
||||
* @param mixed $default
|
||||
* @return array|string
|
||||
*/
|
||||
protected function getConfig(?string $name = null, $default = null): array|string
|
||||
{
|
||||
if (!is_null($name)) {
|
||||
return Config::get('buildadmin.token.' . $name, $default);
|
||||
}
|
||||
|
||||
return Config::get('buildadmin.token');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取驱动配置参数
|
||||
* @param $name
|
||||
* @return array
|
||||
*/
|
||||
protected function resolveParams($name): array
|
||||
{
|
||||
$config = $this->getStoreConfig($name);
|
||||
return [$config];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取驱动类
|
||||
* @param string $type
|
||||
* @return string
|
||||
*/
|
||||
protected function resolveClass(string $type): string
|
||||
{
|
||||
if ($this->namespace || str_contains($type, '\\')) {
|
||||
$class = str_contains($type, '\\') ? $type : $this->namespace . Str::studly($type);
|
||||
|
||||
if (class_exists($class)) {
|
||||
return $class;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException("Driver [$type] not supported.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取驱动配置
|
||||
* @param string $store
|
||||
* @param string|null $name
|
||||
* @param mixed $default
|
||||
* @return array|string
|
||||
*/
|
||||
protected function getStoreConfig(string $store, ?string $name = null, $default = null): array|string
|
||||
{
|
||||
if ($config = $this->getConfig("stores.$store")) {
|
||||
return Arr::get($config, $name, $default);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException("Store [$store] not found.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取驱动类型
|
||||
* @param string $name
|
||||
* @return string
|
||||
*/
|
||||
protected function resolveType(string $name): string
|
||||
{
|
||||
return $this->getStoreConfig($name, 'type', 'Mysql');
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置token
|
||||
* @param string $token
|
||||
* @param string $type
|
||||
* @param int $user_id
|
||||
* @param int|null $expire
|
||||
* @return bool
|
||||
*/
|
||||
public function set(string $token, string $type, int $user_id, ?int $expire = null): bool
|
||||
{
|
||||
return $this->getDriver()->set($token, $type, $user_id, $expire);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取token
|
||||
* @param string $token
|
||||
* @param bool $expirationException
|
||||
* @return array
|
||||
*/
|
||||
public function get(string $token, bool $expirationException = true): array
|
||||
{
|
||||
return $this->getDriver()->get($token, $expirationException);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查token
|
||||
* @param string $token
|
||||
* @param string $type
|
||||
* @param int $user_id
|
||||
* @param bool $expirationException
|
||||
* @return bool
|
||||
*/
|
||||
public function check(string $token, string $type, int $user_id, bool $expirationException = true): bool
|
||||
{
|
||||
return $this->getDriver()->check($token, $type, $user_id, $expirationException);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除token
|
||||
* @param string $token
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(string $token): bool
|
||||
{
|
||||
return $this->getDriver()->delete($token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理指定用户token
|
||||
* @param string $type
|
||||
* @param int $user_id
|
||||
* @return bool
|
||||
*/
|
||||
public function clear(string $type, int $user_id): bool
|
||||
{
|
||||
return $this->getDriver()->clear($type, $user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Token过期检查
|
||||
* @throws TokenExpirationException
|
||||
*/
|
||||
public function tokenExpirationCheck(array $token): void
|
||||
{
|
||||
if (isset($token['expire_time']) && $token['expire_time'] <= time()) {
|
||||
throw new TokenExpirationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
341
app/common/library/Upload.php
Normal file
341
app/common/library/Upload.php
Normal file
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use Throwable;
|
||||
use ba\Random;
|
||||
use ba\Filesystem;
|
||||
use think\Exception;
|
||||
use think\helper\Str;
|
||||
use think\facade\Config;
|
||||
use think\facade\Validate;
|
||||
use think\file\UploadedFile;
|
||||
use InvalidArgumentException;
|
||||
use think\validate\ValidateRule;
|
||||
use app\common\model\Attachment;
|
||||
use app\common\library\upload\Driver;
|
||||
|
||||
/**
|
||||
* 上传
|
||||
*/
|
||||
class Upload
|
||||
{
|
||||
/**
|
||||
* 上传配置
|
||||
*/
|
||||
protected array $config = [];
|
||||
|
||||
/**
|
||||
* 被上传文件
|
||||
*/
|
||||
protected ?UploadedFile $file = null;
|
||||
|
||||
/**
|
||||
* 是否是图片
|
||||
*/
|
||||
protected bool $isImage = false;
|
||||
|
||||
/**
|
||||
* 文件信息
|
||||
*/
|
||||
protected array $fileInfo;
|
||||
|
||||
/**
|
||||
* 上传驱动
|
||||
*/
|
||||
protected array $driver = [
|
||||
'name' => 'local', // 默认驱动:local=本地
|
||||
'handler' => [], // 驱动句柄
|
||||
'namespace' => '\\app\\common\\library\\upload\\driver\\', // 驱动类的命名空间
|
||||
];
|
||||
|
||||
/**
|
||||
* 存储子目录
|
||||
*/
|
||||
protected string $topic = 'default';
|
||||
|
||||
/**
|
||||
* 构造方法
|
||||
* @param ?UploadedFile $file 上传的文件
|
||||
* @param array $config 配置
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function __construct(?UploadedFile $file = null, array $config = [])
|
||||
{
|
||||
$upload = Config::get('upload');
|
||||
$this->config = array_merge($upload, $config);
|
||||
|
||||
if ($file) {
|
||||
$this->setFile($file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置上传文件
|
||||
* @param ?UploadedFile $file
|
||||
* @return Upload
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function setFile(?UploadedFile $file): Upload
|
||||
{
|
||||
if (empty($file)) {
|
||||
throw new Exception(__('No files were uploaded'));
|
||||
}
|
||||
|
||||
$suffix = strtolower($file->extension());
|
||||
$suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
|
||||
$fileInfo['suffix'] = $suffix;
|
||||
$fileInfo['type'] = $file->getMime();
|
||||
$fileInfo['size'] = $file->getSize();
|
||||
$fileInfo['name'] = $file->getOriginalName();
|
||||
$fileInfo['sha1'] = $file->sha1();
|
||||
|
||||
$this->file = $file;
|
||||
$this->fileInfo = $fileInfo;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置上传驱动
|
||||
*/
|
||||
public function setDriver(string $driver): Upload
|
||||
{
|
||||
$this->driver['name'] = $driver;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上传驱动句柄
|
||||
* @param ?string $driver 驱动名称
|
||||
* @param bool $noDriveException 找不到驱动是否抛出异常
|
||||
* @return bool|Driver
|
||||
*/
|
||||
public function getDriver(?string $driver = null, bool $noDriveException = true): bool|Driver
|
||||
{
|
||||
if (is_null($driver)) {
|
||||
$driver = $this->driver['name'];
|
||||
}
|
||||
if (!isset($this->driver['handler'][$driver])) {
|
||||
$class = $this->resolveDriverClass($driver);
|
||||
if ($class) {
|
||||
$this->driver['handler'][$driver] = new $class();
|
||||
} elseif ($noDriveException) {
|
||||
throw new InvalidArgumentException(__('Driver %s not supported', [$driver]));
|
||||
}
|
||||
}
|
||||
return $this->driver['handler'][$driver] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取驱动类
|
||||
*/
|
||||
protected function resolveDriverClass(string $driver): bool|string
|
||||
{
|
||||
if ($this->driver['namespace'] || str_contains($driver, '\\')) {
|
||||
$class = str_contains($driver, '\\') ? $driver : $this->driver['namespace'] . Str::studly($driver);
|
||||
if (class_exists($class)) {
|
||||
return $class;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置存储子目录
|
||||
*/
|
||||
public function setTopic(string $topic): Upload
|
||||
{
|
||||
$this->topic = $topic;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是图片并设置好相关属性
|
||||
* @return bool
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function checkIsImage(): bool
|
||||
{
|
||||
if (in_array($this->fileInfo['type'], ['image/gif', 'image/jpg', 'image/jpeg', 'image/bmp', 'image/png', 'image/webp']) || in_array($this->fileInfo['suffix'], ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'webp'])) {
|
||||
$imgInfo = getimagesize($this->file->getPathname());
|
||||
if (!$imgInfo || !isset($imgInfo[0]) || !isset($imgInfo[1])) {
|
||||
throw new Exception(__('The uploaded image file is not a valid image'));
|
||||
}
|
||||
$this->fileInfo['width'] = $imgInfo[0];
|
||||
$this->fileInfo['height'] = $imgInfo[1];
|
||||
$this->isImage = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传的文件是否为图片
|
||||
* @return bool
|
||||
*/
|
||||
public function isImage(): bool
|
||||
{
|
||||
return $this->isImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件后缀
|
||||
* @return string
|
||||
*/
|
||||
public function getSuffix(): string
|
||||
{
|
||||
return $this->fileInfo['suffix'] ?: 'file';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件保存路径和名称
|
||||
* @param ?string $saveName
|
||||
* @param ?string $filename
|
||||
* @param ?string $sha1
|
||||
* @return string
|
||||
*/
|
||||
public function getSaveName(?string $saveName = null, ?string $filename = null, ?string $sha1 = null): string
|
||||
{
|
||||
if ($filename) {
|
||||
$suffix = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
$suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
|
||||
} else {
|
||||
$suffix = $this->fileInfo['suffix'];
|
||||
}
|
||||
$filename = $filename ?: $this->fileInfo['name'];
|
||||
$sha1 = $sha1 ?: $this->fileInfo['sha1'];
|
||||
$replaceArr = [
|
||||
'{topic}' => $this->topic,
|
||||
'{year}' => date("Y"),
|
||||
'{mon}' => date("m"),
|
||||
'{day}' => date("d"),
|
||||
'{hour}' => date("H"),
|
||||
'{min}' => date("i"),
|
||||
'{sec}' => date("s"),
|
||||
'{random}' => Random::build(),
|
||||
'{random32}' => Random::build('alnum', 32),
|
||||
'{fileName}' => $this->getFileNameSubstr($filename, $suffix),
|
||||
'{suffix}' => $suffix,
|
||||
'{.suffix}' => $suffix ? '.' . $suffix : '',
|
||||
'{fileSha1}' => $sha1,
|
||||
];
|
||||
$saveName = $saveName ?: $this->config['save_name'];
|
||||
return Filesystem::fsFit(str_replace(array_keys($replaceArr), array_values($replaceArr), $saveName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证文件是否符合上传配置要求
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function validates(): void
|
||||
{
|
||||
if (empty($this->file)) {
|
||||
throw new Exception(__('No files have been uploaded or the file size exceeds the upload limit of the server'));
|
||||
}
|
||||
|
||||
$size = Filesystem::fileUnitToByte($this->config['max_size']);
|
||||
$mime = $this->checkConfig($this->config['allowed_mime_types']);
|
||||
$suffix = $this->checkConfig($this->config['allowed_suffixes']);
|
||||
|
||||
// 文件大小
|
||||
$fileValidateRule = ValidateRule::fileSize($size, __('The uploaded file is too large (%sMiB), Maximum file size:%sMiB', [
|
||||
round($this->fileInfo['size'] / pow(1024, 2), 2),
|
||||
round($size / pow(1024, 2), 2)
|
||||
]));
|
||||
|
||||
// 文件后缀
|
||||
if ($suffix) {
|
||||
$fileValidateRule->fileExt($suffix, __('The uploaded file format is not allowed'));
|
||||
}
|
||||
// 文件 MIME 类型
|
||||
if ($mime) {
|
||||
$fileValidateRule->fileMime($mime, __('The uploaded file format is not allowed'));
|
||||
}
|
||||
|
||||
// 图片文件利用tp内置规则做一些额外检查
|
||||
if ($this->checkIsImage()) {
|
||||
$fileValidateRule->image("{$this->fileInfo['width']},{$this->fileInfo['height']}", __('The uploaded image file is not a valid image'));
|
||||
}
|
||||
|
||||
Validate::failException()
|
||||
->rule([
|
||||
'file' => $fileValidateRule,
|
||||
'topic' => ValidateRule::is('alphaDash', __('Topic format error')),
|
||||
'driver' => ValidateRule::is('alphaDash', __('Driver %s not supported', [$this->driver['name']])),
|
||||
])
|
||||
->check([
|
||||
'file' => $this->file,
|
||||
'topic' => $this->topic,
|
||||
'driver' => $this->driver['name'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param ?string $saveName
|
||||
* @param int $adminId
|
||||
* @param int $userId
|
||||
* @return array
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function upload(?string $saveName = null, int $adminId = 0, int $userId = 0): array
|
||||
{
|
||||
$this->validates();
|
||||
|
||||
$driver = $this->getDriver();
|
||||
$saveName = $saveName ?: $this->getSaveName();
|
||||
$params = [
|
||||
'topic' => $this->topic,
|
||||
'admin_id' => $adminId,
|
||||
'user_id' => $userId,
|
||||
'url' => $driver->url($saveName, false),
|
||||
'width' => $this->fileInfo['width'] ?? 0,
|
||||
'height' => $this->fileInfo['height'] ?? 0,
|
||||
'name' => $this->getFileNameSubstr($this->fileInfo['name'], $this->fileInfo['suffix'], 100) . ".{$this->fileInfo['suffix']}",
|
||||
'size' => $this->fileInfo['size'],
|
||||
'mimetype' => $this->fileInfo['type'],
|
||||
'storage' => $this->driver['name'],
|
||||
'sha1' => $this->fileInfo['sha1']
|
||||
];
|
||||
|
||||
// 附件数据入库 - 不依赖模型新增前事件,确保入库前文件已经移动完成
|
||||
$attachment = Attachment::where('sha1', $params['sha1'])
|
||||
->where('topic', $params['topic'])
|
||||
->where('storage', $params['storage'])
|
||||
->find();
|
||||
if ($attachment && $driver->exists($attachment->url)) {
|
||||
$attachment->quote++;
|
||||
$attachment->last_upload_time = time();
|
||||
} else {
|
||||
$driver->save($this->file, $saveName);
|
||||
$attachment = new Attachment();
|
||||
$attachment->data(array_filter($params));
|
||||
}
|
||||
$attachment->save();
|
||||
return $attachment->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件名称字符串的子串
|
||||
*/
|
||||
public function getFileNameSubstr(string $fileName, string $suffix, int $length = 15): string
|
||||
{
|
||||
// 对 $fileName 中不利于传输的字符串进行过滤
|
||||
$pattern = "/[\s:@#?&\/=',+]+/u";
|
||||
$fileName = str_replace(".$suffix", '', $fileName);
|
||||
$fileName = preg_replace($pattern, '', $fileName);
|
||||
return mb_substr(htmlspecialchars(strip_tags($fileName)), 0, $length);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查配置项,将 string 类型的配置转换为 array,并且将所有字母转换为小写
|
||||
*/
|
||||
protected function checkConfig($configItem): array
|
||||
{
|
||||
if (is_array($configItem)) {
|
||||
return array_map('strtolower', $configItem);
|
||||
} else {
|
||||
return explode(',', strtolower($configItem));
|
||||
}
|
||||
}
|
||||
}
|
||||
92
app/common/library/token/Driver.php
Normal file
92
app/common/library/token/Driver.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\library\token;
|
||||
|
||||
use think\facade\Config;
|
||||
|
||||
/**
|
||||
* Token 驱动抽象类
|
||||
*/
|
||||
abstract class Driver
|
||||
{
|
||||
/**
|
||||
* 具体驱动的句柄 Mysql|Redis
|
||||
* @var object
|
||||
*/
|
||||
protected object $handler;
|
||||
|
||||
/**
|
||||
* @var array 配置数据
|
||||
*/
|
||||
protected array $options = [];
|
||||
|
||||
/**
|
||||
* 设置 token
|
||||
* @param string $token Token
|
||||
* @param string $type Type
|
||||
* @param int $userId 用户ID
|
||||
* @param ?int $expire 过期时间
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function set(string $token, string $type, int $userId, ?int $expire = null): bool;
|
||||
|
||||
/**
|
||||
* 获取 token 的数据
|
||||
* @param string $token Token
|
||||
* @return array
|
||||
*/
|
||||
abstract public function get(string $token): array;
|
||||
|
||||
/**
|
||||
* 检查token是否有效
|
||||
* @param string $token
|
||||
* @param string $type
|
||||
* @param int $userId
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function check(string $token, string $type, int $userId): bool;
|
||||
|
||||
/**
|
||||
* 删除一个token
|
||||
* @param string $token
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function delete(string $token): bool;
|
||||
|
||||
/**
|
||||
* 清理一个用户的所有token
|
||||
* @param string $type
|
||||
* @param int $userId
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function clear(string $type, int $userId): bool;
|
||||
|
||||
/**
|
||||
* 返回句柄对象
|
||||
* @access public
|
||||
* @return object|null
|
||||
*/
|
||||
public function handler(): ?object
|
||||
{
|
||||
return $this->handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $token
|
||||
* @return string
|
||||
*/
|
||||
protected function getEncryptedToken(string $token): string
|
||||
{
|
||||
$config = Config::get('buildadmin.token');
|
||||
return hash_hmac($config['algo'], $token, $config['key']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $expireTime
|
||||
* @return int
|
||||
*/
|
||||
protected function getExpiredIn(int $expireTime): int
|
||||
{
|
||||
return $expireTime ? max(0, $expireTime - time()) : 365 * 86400;
|
||||
}
|
||||
}
|
||||
16
app/common/library/token/TokenExpirationException.php
Normal file
16
app/common/library/token/TokenExpirationException.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\library\token;
|
||||
|
||||
use think\Exception;
|
||||
|
||||
/**
|
||||
* Token过期异常
|
||||
*/
|
||||
class TokenExpirationException extends Exception
|
||||
{
|
||||
public function __construct(protected $message = '', protected $code = 409, protected $data = [])
|
||||
{
|
||||
parent::__construct($message, $code);
|
||||
}
|
||||
}
|
||||
109
app/common/library/token/driver/Mysql.php
Normal file
109
app/common/library/token/driver/Mysql.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\library\token\driver;
|
||||
|
||||
use Throwable;
|
||||
use think\facade\Db;
|
||||
use think\facade\Cache;
|
||||
use app\common\library\token\Driver;
|
||||
|
||||
/**
|
||||
* @see Driver
|
||||
*/
|
||||
class Mysql extends Driver
|
||||
{
|
||||
/**
|
||||
* 默认配置
|
||||
* @var array
|
||||
*/
|
||||
protected array $options = [];
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @access public
|
||||
* @param array $options 参数
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
if (!empty($options)) {
|
||||
$this->options = array_merge($this->options, $options);
|
||||
}
|
||||
|
||||
if ($this->options['name']) {
|
||||
$this->handler = Db::connect($this->options['name'])->name($this->options['table']);
|
||||
} else {
|
||||
$this->handler = Db::name($this->options['table']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function set(string $token, string $type, int $userId, ?int $expire = null): bool
|
||||
{
|
||||
if (is_null($expire)) {
|
||||
$expire = $this->options['expire'];
|
||||
}
|
||||
$expireTime = $expire !== 0 ? time() + $expire : 0;
|
||||
$token = $this->getEncryptedToken($token);
|
||||
$this->handler->insert([
|
||||
'token' => $token,
|
||||
'type' => $type,
|
||||
'user_id' => $userId,
|
||||
'create_time' => time(),
|
||||
'expire_time' => $expireTime,
|
||||
]);
|
||||
|
||||
// 每隔48小时清理一次过期Token
|
||||
$time = time();
|
||||
$lastCacheCleanupTime = Cache::get('last_cache_cleanup_time');
|
||||
if (!$lastCacheCleanupTime || $lastCacheCleanupTime < $time - 172800) {
|
||||
Cache::set('last_cache_cleanup_time', $time);
|
||||
$this->handler->where('expire_time', '<', time())->where('expire_time', '>', 0)->delete();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function get(string $token): array
|
||||
{
|
||||
$data = $this->handler->where('token', $this->getEncryptedToken($token))->find();
|
||||
if (!$data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$data['token'] = $token; // 返回未加密的token给客户端使用
|
||||
$data['expires_in'] = $this->getExpiredIn($data['expire_time'] ?? 0); // 返回剩余有效时间
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function check(string $token, string $type, int $userId): bool
|
||||
{
|
||||
$data = $this->get($token);
|
||||
if (!$data || ($data['expire_time'] && $data['expire_time'] <= time())) return false;
|
||||
return $data['type'] == $type && $data['user_id'] == $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function delete(string $token): bool
|
||||
{
|
||||
$this->handler->where('token', $this->getEncryptedToken($token))->delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function clear(string $type, int $userId): bool
|
||||
{
|
||||
$this->handler->where('type', $type)->where('user_id', $userId)->delete();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
146
app/common/library/token/driver/Redis.php
Normal file
146
app/common/library/token/driver/Redis.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\library\token\driver;
|
||||
|
||||
use Throwable;
|
||||
use BadFunctionCallException;
|
||||
use app\common\library\token\Driver;
|
||||
|
||||
/**
|
||||
* @see Driver
|
||||
*/
|
||||
class Redis extends Driver
|
||||
{
|
||||
/**
|
||||
* 默认配置
|
||||
* @var array
|
||||
*/
|
||||
protected array $options = [];
|
||||
|
||||
/**
|
||||
* Token 过期后缓存继续保留的时间(s)
|
||||
*/
|
||||
protected int $expiredHold = 60 * 60 * 24 * 2;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @access public
|
||||
* @param array $options 参数
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
if (!extension_loaded('redis')) {
|
||||
throw new BadFunctionCallException('未安装redis扩展');
|
||||
}
|
||||
if (!empty($options)) {
|
||||
$this->options = array_merge($this->options, $options);
|
||||
}
|
||||
$this->handler = new \Redis();
|
||||
if ($this->options['persistent']) {
|
||||
$this->handler->pconnect($this->options['host'], $this->options['port'], $this->options['timeout'], 'persistent_id_' . $this->options['select']);
|
||||
} else {
|
||||
$this->handler->connect($this->options['host'], $this->options['port'], $this->options['timeout']);
|
||||
}
|
||||
|
||||
if ('' != $this->options['password']) {
|
||||
$this->handler->auth($this->options['password']);
|
||||
}
|
||||
|
||||
if (false !== $this->options['select']) {
|
||||
$this->handler->select($this->options['select']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function set(string $token, string $type, int $userId, ?int $expire = null): bool
|
||||
{
|
||||
if (is_null($expire)) {
|
||||
$expire = $this->options['expire'];
|
||||
}
|
||||
$expireTime = $expire !== 0 ? time() + $expire : 0;
|
||||
$token = $this->getEncryptedToken($token);
|
||||
$tokenInfo = [
|
||||
'token' => $token,
|
||||
'type' => $type,
|
||||
'user_id' => $userId,
|
||||
'create_time' => time(),
|
||||
'expire_time' => $expireTime,
|
||||
];
|
||||
$tokenInfo = json_encode($tokenInfo, JSON_UNESCAPED_UNICODE);
|
||||
if ($expire) {
|
||||
$expire += $this->expiredHold;
|
||||
$result = $this->handler->setex($token, $expire, $tokenInfo);
|
||||
} else {
|
||||
$result = $this->handler->set($token, $tokenInfo);
|
||||
}
|
||||
$this->handler->sAdd($this->getUserKey($type, $userId), $token);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function get(string $token): array
|
||||
{
|
||||
$key = $this->getEncryptedToken($token);
|
||||
$data = $this->handler->get($key);
|
||||
if (is_null($data) || false === $data) {
|
||||
return [];
|
||||
}
|
||||
$data = json_decode($data, true);
|
||||
|
||||
$data['token'] = $token; // 返回未加密的token给客户端使用
|
||||
$data['expires_in'] = $this->getExpiredIn($data['expire_time'] ?? 0); // 过期时间
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function check(string $token, string $type, int $userId): bool
|
||||
{
|
||||
$data = $this->get($token);
|
||||
if (!$data || ($data['expire_time'] && $data['expire_time'] <= time())) return false;
|
||||
return $data['type'] == $type && $data['user_id'] == $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function delete(string $token): bool
|
||||
{
|
||||
$data = $this->get($token);
|
||||
if ($data) {
|
||||
$key = $this->getEncryptedToken($token);
|
||||
$this->handler->del($key);
|
||||
$this->handler->sRem($this->getUserKey($data['type'], $data['user_id']), $key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function clear(string $type, int $userId): bool
|
||||
{
|
||||
$userKey = $this->getUserKey($type, $userId);
|
||||
$keys = $this->handler->sMembers($userKey);
|
||||
$this->handler->del($userKey);
|
||||
$this->handler->del($keys);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员的key
|
||||
* @param $type
|
||||
* @param $userId
|
||||
* @return string
|
||||
*/
|
||||
protected function getUserKey($type, $userId): string
|
||||
{
|
||||
return $this->options['prefix'] . $type . '-' . $userId;
|
||||
}
|
||||
}
|
||||
47
app/common/library/upload/Driver.php
Normal file
47
app/common/library/upload/Driver.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\library\upload;
|
||||
|
||||
use think\file\UploadedFile;
|
||||
|
||||
/**
|
||||
* 上传驱动抽象类
|
||||
*/
|
||||
abstract class Driver
|
||||
{
|
||||
/**
|
||||
* @var array 配置数据
|
||||
*/
|
||||
protected array $options = [];
|
||||
|
||||
/**
|
||||
* 保存文件
|
||||
* @param UploadedFile $file
|
||||
* @param string $saveName
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function save(UploadedFile $file, string $saveName): bool;
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param string $saveName
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function delete(string $saveName): bool;
|
||||
|
||||
/**
|
||||
* 获取资源 URL 地址;
|
||||
* @param string $saveName 资源保存名称
|
||||
* @param string|bool $domain 是否携带域名 或者直接传入域名
|
||||
* @param string $default 默认值
|
||||
* @return string
|
||||
*/
|
||||
abstract public function url(string $saveName, string|bool $domain = true, string $default = ''): string;
|
||||
|
||||
/**
|
||||
* 文件是否存在
|
||||
* @param string $saveName
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function exists(string $saveName): bool;
|
||||
}
|
||||
148
app/common/library/upload/driver/Local.php
Normal file
148
app/common/library/upload/driver/Local.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\library\upload\driver;
|
||||
|
||||
use ba\Filesystem;
|
||||
use think\facade\Config;
|
||||
use think\file\UploadedFile;
|
||||
use think\exception\FileException;
|
||||
use app\common\library\upload\Driver;
|
||||
|
||||
/**
|
||||
* 上传到本地磁盘的驱动
|
||||
* @see Driver
|
||||
*/
|
||||
class Local extends Driver
|
||||
{
|
||||
protected array $options = [];
|
||||
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$this->options = Config::get('filesystem.disks.public');
|
||||
if (!empty($options)) {
|
||||
$this->options = array_merge($this->options, $options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存文件
|
||||
* @param UploadedFile $file
|
||||
* @param string $saveName
|
||||
* @return bool
|
||||
*/
|
||||
public function save(UploadedFile $file, string $saveName): bool
|
||||
{
|
||||
$savePathInfo = pathinfo($saveName);
|
||||
$saveFullPath = $this->getFullPath($saveName);
|
||||
|
||||
// cgi 直接 move
|
||||
if (request()->isCgi()) {
|
||||
$file->move($saveFullPath, $savePathInfo['basename']);
|
||||
return true;
|
||||
}
|
||||
|
||||
set_error_handler(function ($type, $msg) use (&$error) {
|
||||
$error = $msg;
|
||||
});
|
||||
|
||||
// 建立文件夹
|
||||
if (!is_dir($saveFullPath) && !mkdir($saveFullPath, 0755, true)) {
|
||||
restore_error_handler();
|
||||
throw new FileException(sprintf('Unable to create the "%s" directory (%s)', $saveFullPath, strip_tags($error)));
|
||||
}
|
||||
|
||||
// cli 使用 rename
|
||||
$saveName = $this->getFullPath($saveName, true);
|
||||
if (!rename($file->getPathname(), $saveName)) {
|
||||
restore_error_handler();
|
||||
throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $file->getPathname(), $saveName, strip_tags($error)));
|
||||
}
|
||||
|
||||
restore_error_handler();
|
||||
@chmod($saveName, 0666 & ~umask());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param string $saveName
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(string $saveName): bool
|
||||
{
|
||||
$saveFullName = $this->getFullPath($saveName, true);
|
||||
if ($this->exists($saveFullName)) {
|
||||
@unlink($saveFullName);
|
||||
}
|
||||
Filesystem::delEmptyDir(dirname($saveFullName));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资源 URL 地址
|
||||
* @param string $saveName 资源保存名称
|
||||
* @param string|bool $domain 是否携带域名 或者直接传入域名
|
||||
* @param string $default 默认值
|
||||
* @return string
|
||||
*/
|
||||
public function url(string $saveName, string|bool $domain = true, string $default = ''): string
|
||||
{
|
||||
$saveName = $this->clearRootPath($saveName);
|
||||
|
||||
if ($domain === true) {
|
||||
$domain = '//' . request()->host();
|
||||
} elseif ($domain === false) {
|
||||
$domain = '';
|
||||
}
|
||||
|
||||
$saveName = $saveName ?: $default;
|
||||
if (!$saveName) return $domain;
|
||||
|
||||
$regex = "/^((?:[a-z]+:)?\/\/|data:image\/)(.*)/i";
|
||||
if (preg_match('/^http(s)?:\/\//', $saveName) || preg_match($regex, $saveName) || $domain === false) {
|
||||
return $saveName;
|
||||
}
|
||||
return str_replace('\\', '/', $domain . $saveName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件是否存在
|
||||
* @param string $saveName
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(string $saveName): bool
|
||||
{
|
||||
$saveFullName = $this->getFullPath($saveName, true);
|
||||
return file_exists($saveFullName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件的完整存储路径
|
||||
* @param string $saveName
|
||||
* @param bool $baseName 是否包含文件名
|
||||
* @return string
|
||||
*/
|
||||
public function getFullPath(string $saveName, bool $baseName = false): string
|
||||
{
|
||||
$savePathInfo = pathinfo($saveName);
|
||||
$root = $this->getRootPath();
|
||||
$dirName = $savePathInfo['dirname'] . '/';
|
||||
|
||||
// 以 root 路径开始时单独返回,避免重复调用此方法时造成 $dirName 的错误拼接
|
||||
if (str_starts_with($saveName, $root)) {
|
||||
return Filesystem::fsFit($baseName || !isset($savePathInfo['extension']) ? $saveName : $dirName);
|
||||
}
|
||||
|
||||
return Filesystem::fsFit($root . $dirName . ($baseName ? $savePathInfo['basename'] : ''));
|
||||
}
|
||||
|
||||
public function clearRootPath(string $saveName): string
|
||||
{
|
||||
return str_replace($this->getRootPath(), '', Filesystem::fsFit($saveName));
|
||||
}
|
||||
|
||||
public function getRootPath(): string
|
||||
{
|
||||
return Filesystem::fsFit(str_replace($this->options['url'], '', $this->options['root']));
|
||||
}
|
||||
}
|
||||
24
app/common/middleware/AdminLog.php
Normal file
24
app/common/middleware/AdminLog.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\middleware;
|
||||
|
||||
use Closure;
|
||||
use Throwable;
|
||||
use think\facade\Config;
|
||||
use app\admin\model\AdminLog as AdminLogModel;
|
||||
|
||||
class AdminLog
|
||||
{
|
||||
/**
|
||||
* 写入管理日志
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
$response = $next($request);
|
||||
if (($request->isPost() || $request->isDelete()) && Config::get('buildadmin.auto_write_admin_log')) {
|
||||
AdminLogModel::instance()->record();
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
66
app/common/middleware/AllowCrossDomain.php
Normal file
66
app/common/middleware/AllowCrossDomain.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006~2021 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types=1);
|
||||
|
||||
namespace app\common\middleware;
|
||||
|
||||
use Closure;
|
||||
use think\Request;
|
||||
use think\Response;
|
||||
use think\facade\Config;
|
||||
|
||||
/**
|
||||
* 跨域请求支持
|
||||
* 安全起见,只支持了配置中的域名
|
||||
*/
|
||||
class AllowCrossDomain
|
||||
{
|
||||
protected array $header = [
|
||||
'Access-Control-Allow-Credentials' => 'true',
|
||||
'Access-Control-Max-Age' => 1800,
|
||||
'Access-Control-Allow-Methods' => '*',
|
||||
'Access-Control-Allow-Headers' => '*',
|
||||
];
|
||||
|
||||
/**
|
||||
* 跨域请求检测
|
||||
* @access public
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
* @param array|null $header
|
||||
* @return Response
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, ?array $header = []): Response
|
||||
{
|
||||
$header = !empty($header) ? array_merge($this->header, $header) : $this->header;
|
||||
|
||||
$origin = $request->header('origin');
|
||||
if ($origin && !isset($header['Access-Control-Allow-Origin'])) {
|
||||
$info = parse_url($origin);
|
||||
|
||||
// 获取跨域配置
|
||||
$corsDomain = explode(',', Config::get('buildadmin.cors_request_domain'));
|
||||
$corsDomain[] = $request->host(true);
|
||||
|
||||
if (in_array("*", $corsDomain) || in_array($origin, $corsDomain) || (isset($info['host']) && in_array($info['host'], $corsDomain))) {
|
||||
$header['Access-Control-Allow-Origin'] = $origin;
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->isOptions()) {
|
||||
return response('', 204, $header);
|
||||
}
|
||||
|
||||
$request->allowCrossDomainHeaders = $header;
|
||||
|
||||
return $next($request)->header($header);
|
||||
}
|
||||
}
|
||||
115
app/common/model/Attachment.php
Normal file
115
app/common/model/Attachment.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use Throwable;
|
||||
use think\Model;
|
||||
use think\facade\Event;
|
||||
use app\admin\model\Admin;
|
||||
use app\common\library\Upload;
|
||||
use think\model\relation\BelongsTo;
|
||||
|
||||
/**
|
||||
* Attachment模型
|
||||
* @property string url 文件物理路径
|
||||
* @property int quote 上传(引用)次数
|
||||
* @property int last_upload_time 最后上传时间
|
||||
*/
|
||||
class Attachment extends Model
|
||||
{
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $updateTime = false;
|
||||
|
||||
protected $append = [
|
||||
'suffix',
|
||||
'full_url'
|
||||
];
|
||||
|
||||
/**
|
||||
* 上传类实例,可以通过它调用上传文件驱动,且驱动类具有静态缓存
|
||||
*/
|
||||
protected static Upload $upload;
|
||||
|
||||
protected static function init(): void
|
||||
{
|
||||
self::$upload = new Upload();
|
||||
}
|
||||
|
||||
public function getSuffixAttr($value, $row): string
|
||||
{
|
||||
if ($row['name']) {
|
||||
$suffix = strtolower(pathinfo($row['name'], PATHINFO_EXTENSION));
|
||||
return $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
|
||||
}
|
||||
return 'file';
|
||||
}
|
||||
|
||||
public function getFullUrlAttr($value, $row): string
|
||||
{
|
||||
$driver = self::$upload->getDriver($row['storage'], false);
|
||||
return $driver ? $driver->url($row['url']) : full_url($row['url']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增前
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected static function onBeforeInsert($model): bool
|
||||
{
|
||||
$repeat = $model->where([
|
||||
['sha1', '=', $model->sha1],
|
||||
['topic', '=', $model->topic],
|
||||
['storage', '=', $model->storage],
|
||||
])->find();
|
||||
if ($repeat) {
|
||||
$driver = self::$upload->getDriver($repeat->storage, false);
|
||||
if ($driver && !$driver->exists($repeat->url)) {
|
||||
$repeat->delete();
|
||||
return true;
|
||||
} else {
|
||||
$repeat->quote++;
|
||||
$repeat->last_upload_time = time();
|
||||
$repeat->save();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增后
|
||||
*/
|
||||
protected static function onAfterInsert($model): void
|
||||
{
|
||||
Event::trigger('AttachmentInsert', $model);
|
||||
|
||||
if (!$model->last_upload_time) {
|
||||
$model->quote = 1;
|
||||
$model->last_upload_time = time();
|
||||
$model->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除后
|
||||
*/
|
||||
protected static function onAfterDelete($model): void
|
||||
{
|
||||
Event::trigger('AttachmentDel', $model);
|
||||
|
||||
$driver = self::$upload->getDriver($model->storage, false);
|
||||
if ($driver && $driver->exists($model->url)) {
|
||||
$driver->delete($model->url);
|
||||
}
|
||||
}
|
||||
|
||||
public function admin(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Admin::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
83
app/common/model/Config.php
Normal file
83
app/common/model/Config.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use Throwable;
|
||||
use think\Model;
|
||||
use app\admin\model\Config as adminConfigModel;
|
||||
|
||||
class Config extends Model
|
||||
{
|
||||
/**
|
||||
* 添加系统配置分组
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function addConfigGroup(string $key, string $value): bool
|
||||
{
|
||||
return self::addArrayItem('config_group', $key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除系统配置分组
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function removeConfigGroup(string $key): bool
|
||||
{
|
||||
if (adminConfigModel::where('group', $key)->find()) return false;
|
||||
return self::removeArrayItem('config_group', $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加系统快捷配置入口
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function addQuickEntrance(string $key, string $value): bool
|
||||
{
|
||||
return self::addArrayItem('config_quick_entrance', $key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除系统快捷配置入口
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function removeQuickEntrance(string $key): bool
|
||||
{
|
||||
return self::removeArrayItem('config_quick_entrance', $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为Array类型的配置项添加元素
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function addArrayItem(string $name, string $key, string $value): bool
|
||||
{
|
||||
$configRow = adminConfigModel::where('name', $name)->find();
|
||||
foreach ($configRow->value as $item) {
|
||||
if ($item['key'] == $key) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$configRow->value = array_merge($configRow->value, [['key' => $key, 'value' => $value]]);
|
||||
$configRow->save();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Array类型配置项的一个元素
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function removeArrayItem(string $name, string $key): bool
|
||||
{
|
||||
$configRow = adminConfigModel::where('name', $name)->find();
|
||||
$configRowValue = $configRow->value;
|
||||
foreach ($configRowValue as $iKey => $item) {
|
||||
if ($item['key'] == $key) {
|
||||
unset($configRowValue[$iKey]);
|
||||
}
|
||||
}
|
||||
$configRow->value = $configRowValue;
|
||||
$configRow->save();
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
51
app/common/model/User.php
Normal file
51
app/common/model/User.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 会员公共模型
|
||||
* @property int $id 会员ID
|
||||
* @property string $password 密码密文
|
||||
* @property string $salt 密码盐(废弃待删)
|
||||
* @property int $login_failure 登录失败次数
|
||||
* @property string $last_login_time 上次登录时间
|
||||
* @property string $last_login_ip 上次登录IP
|
||||
* @property string $email 会员邮箱
|
||||
* @property string $mobile 会员手机号
|
||||
* @property string $status 状态:enable=启用,disable=禁用,...(string存储,可自定义其他)
|
||||
*/
|
||||
class User extends Model
|
||||
{
|
||||
protected $autoWriteTimestamp = true;
|
||||
|
||||
public function getAvatarAttr($value): string
|
||||
{
|
||||
return full_url($value, false, config('buildadmin.default_avatar'));
|
||||
}
|
||||
|
||||
public function setAvatarAttr($value): string
|
||||
{
|
||||
return $value == full_url('', false, config('buildadmin.default_avatar')) ? '' : $value;
|
||||
}
|
||||
|
||||
public function resetPassword($uid, $newPassword): int|User
|
||||
{
|
||||
return $this->where(['id' => $uid])->update(['password' => hash_password($newPassword), 'salt' => '']);
|
||||
}
|
||||
|
||||
public function getMoneyAttr($value): string
|
||||
{
|
||||
return bcdiv($value, 100, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户的余额是不可以直接进行修改的,请通过 UserMoneyLog 模型插入记录来实现自动修改余额
|
||||
* 此处定义上 money 的修改器仅为防止直接对余额的修改造成数据错乱
|
||||
*/
|
||||
public function setMoneyAttr($value): string
|
||||
{
|
||||
return bcmul($value, 100, 2);
|
||||
}
|
||||
}
|
||||
41
app/common/model/UserMoneyLog.php
Normal file
41
app/common/model/UserMoneyLog.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\model;
|
||||
|
||||
class UserMoneyLog extends model
|
||||
{
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $updateTime = false;
|
||||
|
||||
public function getMoneyAttr($value): string
|
||||
{
|
||||
return bcdiv($value, 100, 2);
|
||||
}
|
||||
|
||||
public function setMoneyAttr($value): string
|
||||
{
|
||||
return bcmul($value, 100, 2);
|
||||
}
|
||||
|
||||
public function getBeforeAttr($value): string
|
||||
{
|
||||
return bcdiv($value, 100, 2);
|
||||
}
|
||||
|
||||
public function setBeforeAttr($value): string
|
||||
{
|
||||
return bcmul($value, 100, 2);
|
||||
}
|
||||
|
||||
public function getAfterAttr($value): string
|
||||
{
|
||||
return bcdiv($value, 100, 2);
|
||||
}
|
||||
|
||||
public function setAfterAttr($value): string
|
||||
{
|
||||
return bcmul($value, 100, 2);
|
||||
}
|
||||
}
|
||||
11
app/common/model/UserScoreLog.php
Normal file
11
app/common/model/UserScoreLog.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\model;
|
||||
|
||||
class UserScoreLog extends model
|
||||
{
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $updateTime = false;
|
||||
}
|
||||
34
app/common/service/moduleService.php
Normal file
34
app/common/service/moduleService.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use think\Service;
|
||||
use think\facade\Event;
|
||||
use app\admin\library\module\Server;
|
||||
|
||||
class moduleService extends Service
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->moduleAppInit();
|
||||
}
|
||||
|
||||
public function moduleAppInit(): void
|
||||
{
|
||||
$installed = Server::installedList(root_path() . 'modules' . DIRECTORY_SEPARATOR);
|
||||
foreach ($installed as $item) {
|
||||
if ($item['state'] != 1) {
|
||||
continue;
|
||||
}
|
||||
$moduleClass = Server::getClass($item['uid']);
|
||||
if (class_exists($moduleClass)) {
|
||||
if (method_exists($moduleClass, 'AppInit')) {
|
||||
Event::listen('AppInit', function () use ($moduleClass) {
|
||||
$handle = new $moduleClass();
|
||||
$handle->AppInit();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user