Browse Source

feat(user): 实现用户权限控制及数据访问层重构

- 新增UserAuthorizationMiddleware中间件,用于用户身份验证和权限控制
- 创建UserBaseDao基类,替代原有的BaseDao,提供用户数据访问基础功能
- 更新多个DAO类继承UserBaseDao,包括AiCommandDao、ArticleCategoryDao等
- 在RequestUtils中添加getUserInfo方法,用于获取当前用户信息
- 修改路由配置,将原有管理员权限中间件替换为用户权限中间件
- 新增User模型类,定义用户表结构和软删除特性
- 添加用户登录路由和相关控制器支持
- 实现附件上传接口的用户权限版本
- 移除BaseDao中的数据权限逻辑,转移到UserBaseDao中处理
- 添加用户数据权限控制,限制用户只能访问自己的数据
master
zhangf@suq.cn 7 days ago
parent
commit
e119a7eabe
  1. 4
      app/dao/AiCommandDao.php
  2. 4
      app/dao/ArticleCategoryDao.php
  3. 4
      app/dao/CreationArticleDao.php
  4. 4
      app/dao/CreationTaskDao.php
  5. 4
      app/dao/DistillationExpandWordDao.php
  6. 4
      app/dao/DistillationQuestionsDao.php
  7. 4
      app/dao/DistillationTransWordDao.php
  8. 4
      app/dao/DistillationWordDao.php
  9. 4
      app/dao/EnterprisePortraitCategoryDao.php
  10. 4
      app/dao/EnterprisePortraitLibraryDao.php
  11. 4
      app/dao/KnowledgeLibraryDao.php
  12. 5
      app/route/route.php
  13. 3
      plugin/piadmin/app/base/BaseDao.php
  14. 799
      plugin/piadmin/app/base/UserBaseDao.php
  15. 45
      plugin/piadmin/app/controller/v1/UserLoginController.php
  16. 17
      plugin/piadmin/app/dao/UserDao.php
  17. 82
      plugin/piadmin/app/middleware/UserAuthorizationMiddleware.php
  18. 21
      plugin/piadmin/app/model/User.php
  19. 6
      plugin/piadmin/app/route/v1/attachment.php
  20. 2
      plugin/piadmin/app/route/v1/route.php
  21. 134
      plugin/piadmin/app/service/UserService.php
  22. 24
      plugin/piadmin/app/utils/RequestUtils.php

4
app/dao/AiCommandDao.php

@ -3,9 +3,9 @@
namespace app\dao;
use app\model\AiCommand;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\base\UserBaseDao;
class AiCommandDao extends BaseDao
class AiCommandDao extends UserBaseDao
{
protected function setModel(): string
{

4
app/dao/ArticleCategoryDao.php

@ -3,9 +3,9 @@
namespace app\dao;
use app\model\ArticleCategory;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\base\UserBaseDao;
class ArticleCategoryDao extends BaseDao
class ArticleCategoryDao extends UserBaseDao
{
protected function setModel(): string
{

4
app/dao/CreationArticleDao.php

@ -3,9 +3,9 @@
namespace app\dao;
use app\model\CreationArticle;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\base\UserBaseDao;
class CreationArticleDao extends BaseDao
class CreationArticleDao extends UserBaseDao
{
protected function setModel(): string
{

4
app/dao/CreationTaskDao.php

@ -3,9 +3,9 @@
namespace app\dao;
use app\model\CreationTask;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\base\UserBaseDao;
class CreationTaskDao extends BaseDao
class CreationTaskDao extends UserBaseDao
{
protected function setModel(): string
{

4
app/dao/DistillationExpandWordDao.php

@ -3,9 +3,9 @@
namespace app\dao;
use app\model\DistillationExpandWord;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\base\UserBaseDao;
class DistillationExpandWordDao extends BaseDao
class DistillationExpandWordDao extends UserBaseDao
{
protected function setModel(): string

4
app/dao/DistillationQuestionsDao.php

@ -3,9 +3,9 @@
namespace app\dao;
use app\model\DistillationQuestions;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\base\UserBaseDao;
class DistillationQuestionsDao extends BaseDao
class DistillationQuestionsDao extends UserBaseDao
{
protected function setModel(): string

4
app/dao/DistillationTransWordDao.php

@ -3,9 +3,9 @@
namespace app\dao;
use app\model\DistillationTransWord;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\base\UserBaseDao;
class DistillationTransWordDao extends BaseDao
class DistillationTransWordDao extends UserBaseDao
{
protected function setModel(): string

4
app/dao/DistillationWordDao.php

@ -3,9 +3,9 @@
namespace app\dao;
use app\model\DistillationWord;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\base\UserBaseDao;
class DistillationWordDao extends BaseDao
class DistillationWordDao extends UserBaseDao
{
protected function setModel(): string

4
app/dao/EnterprisePortraitCategoryDao.php

@ -3,9 +3,9 @@
namespace app\dao;
use app\model\EnterprisePortraitCategory;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\base\UserBaseDao;
class EnterprisePortraitCategoryDao extends BaseDao
class EnterprisePortraitCategoryDao extends UserBaseDao
{
protected function setModel(): string
{

4
app/dao/EnterprisePortraitLibraryDao.php

@ -3,9 +3,9 @@
namespace app\dao;
use app\model\EnterprisePortraitLibrary;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\base\UserBaseDao;
class EnterprisePortraitLibraryDao extends BaseDao
class EnterprisePortraitLibraryDao extends UserBaseDao
{
protected function setModel(): string
{

4
app/dao/KnowledgeLibraryDao.php

@ -3,9 +3,9 @@
namespace app\dao;
use app\model\KnowLedgeLibrary;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\base\UserBaseDao;
class KnowledgeLibraryDao extends BaseDao
class KnowledgeLibraryDao extends UserBaseDao
{
protected function setModel(): string
{

5
app/route/route.php

@ -9,7 +9,7 @@ use app\controller\EnterprisePortraitCategoryController;
use app\controller\EnterprisePortraitLibraryController;
use app\controller\KnowledgeLibraryController;
use app\controller\DistillationWordController;
use plugin\piadmin\app\middleware\AdminAuthorizationMiddleware;
use plugin\piadmin\app\middleware\UserAuthorizationMiddleware;
use Webman\Route;
Route::group('/service/v1', function () {
@ -141,6 +141,7 @@ Route::group('/service/v1', function () {
});
});
})->middleware([
AdminAuthorizationMiddleware::class,
UserAuthorizationMiddleware::class,
// AdminAuthorizationMiddleware::class,
// PermissionsMiddleware::class
]);

3
plugin/piadmin/app/base/BaseDao.php

@ -540,9 +540,6 @@ abstract class BaseDao
{
// 判断是否有搜索条件提供
if ($where) {
//载入数据权限
$admininfo = RequestUtils::getAdminInfo();
$where[] = ['create_by', 'in', $admininfo['dataPermission']];
// 如果有搜索条件,则调用withSearchSelect方法执行带有搜索条件的查询
return $this->withSearchSelect($where, $search);
} else {

799
plugin/piadmin/app/base/UserBaseDao.php

@ -0,0 +1,799 @@
<?php
namespace plugin\piadmin\app\base;
use plugin\piadmin\app\utils\IdUtils;
use plugin\piadmin\app\exception\ApiException;
use plugin\piadmin\app\utils\RequestUtils;
use ReflectionException;
use think\Collection;
use think\db\exception\DbException;
use think\facade\Db;
use think\helper\Str;
use think\Model;
/**
* 抽象基类,提供数据访问对象(DAO)的基本结构和通用方法
* 该类不能直接实例化,旨在被其他DAO类继承,以利用其定义的通用功能
* 主要作用包括:
* - 定义了子类必须实现的抽象方法
* - 提供了数据库连接的通用逻辑(假设实现)
* - 其他通用数据操作方法的模板
*/
abstract class UserBaseDao
{
/**
* 当前表名别名
* @var string
*/
protected string $alias;
/**
* join表别名
* @var string
*/
protected string $joinAlis;
/**
* 获取当前模型
* @return string
*/
abstract protected function setModel(): string;
/**
* 设置join链表模型
* @return string
*/
protected function setJoinModel(): string
{
return '';
}
/**
* 读取数据条数
* @param array $where
* @param bool $search
* @return int
* @throws ReflectionException|DbException
*/
public function count(array $where = [], bool $search = true): int
{
return $this->search($where, $search)->count();
}
/**
* 根据指定条件选择数据列表
*
* 此方法用于根据多个条件查询数据,并支持分页、排序和关联加载等功能
* 它是构建复杂查询的一个灵活入口点,允许开发者指定所需的查询条件和选项,
* 并获取一个数据集合作为结果返回
*
* @param array $where 查询条件数组,用于指定查询的过滤条件
* @param string $field 要查询的字段,默认为 '*',表示查询所有字段
* @param int $page 分页页码,默认为 0,表示不分页
* @param int $limit 每页数据条数,默认为 0,表示不限制条数
* @param string $order 排序条件字符串,默认为空,表示不排序
* @param array $with 关联加载数组,用于指定要加载的关联关系
* @param bool $search 是否使用搜索模式,默认为 false
*
* @return Collection 查询结果集合
* @throws ReflectionException
*/
public function selectList(array $where, string $field = '*', int $page = 0, int $limit = 0,
string $order = '', array $with = [], bool $search = false): Collection
{
return $this->selectModel($where, $field, $page, $limit, $order, $with, $search)->select();
}
/**
* 根据查询条件构造模型
* @param array $where
* @param string $field
* @param int $page
* @param int $limit
* @param string $order
* @param array $with
* @param bool $search
* @return BaseModel
* @throws ReflectionException
*/
private function selectModel(array $where, string $field = '*', int $page = 0, int $limit = 0, string $order = '', array $with = [], bool $search = false): mixed
{
if ($search) {
$model = $this->search($where);
} else {
$model = $this->getModel()->where($where);
}
return $model->field($field)->when($page && $limit, function ($query) use ($page, $limit) {
$query->page($page, $limit);
})->when($order !== '', function ($query) use ($order) {
$query->order($order);
})->when($with, function ($query) use ($with) {
$query->with($with);
});
}
/**
* 获取满足条件的记录数
*
* 该方法用于执行一个查询,以获取数据库中满足给定条件的记录数
* 它通过传递一个数组参数来指定查询条件,然后使用模型的where方法来过滤数据,
* 最后使用count方法来统计满足条件的记录数
*
* @param array $where 查询条件数组,用于指定过滤条件
* @return int 满足条件的记录数
*/
public function getCount(array $where): int
{
return $this->getModel()->where($where)->count();
}
/**
* 获取唯一值的计数
*
* 该方法用于计算给定字段在特定条件下的唯一值数量根据$search参数的值,
* 选择使用search方法还是getModel方法来执行查询
*
* @param array $where 查询条件,用于筛选记录
* @param string $field 需要计算唯一值的字段名称
* @param bool $search 指示是否使用search方法来执行查询,默认为true如果为false,则使用getModel方法
*
* @return int 返回计算得到的唯一值数量如果没有匹配的记录,返回0
*/
public function getDistinctCount(array $where, $field, bool $search = true): int
{
if ($search) {
// 使用search方法执行查询,并计算指定字段的唯一值数量
return $this->search($where)->field('COUNT(distinct(' . $field . ')) as count')->select()->toArray()[0]['count'] ?? 0;
} else {
// 使用getModel方法执行查询,并计算指定字段的唯一值数量
return $this->getModel()->where($where)->field('COUNT(distinct(' . $field . ')) as count')->select()->toArray()[0]['count'] ?? 0;
}
}
/**
* 获取模型
* @return BaseModel
*/
public function getModel(): BaseModel
{
return app()->make($this->setModel());
}
/**
* 获取主键
* @return array|string
*/
protected function getPk(): array|string
{
return $this->getModel()->getPk();
}
/**
* 获取模型对应的表名
* @return string
*/
public function getTableName(): string
{
return $this->getModel()->getName();
}
/**
* 根据主键获取一条或多条数据
* @param $id
* @param array|null $field
* @param array|null $with
* @return array|Model|null
* @throws \think\db\exception\DataNotFoundException
* @throws DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function get($id, ?array $field = [], ?array $with = []): array|Model|null
{
if (is_array($id)) {
$where = $id;
} else {
$where = [$this->getPk() => $id];
}
return $this->getModel()->where($where)->when(count($with), function ($query) use ($with) {
$query->with($with);
})->field($field ?? ['*'])->find();
}
/**
* 查询一条数据是否存在
* @param $map
* @param string $field
* @return bool 是否存在
*/
public function be($map, string $field = '')
{
if (!is_array($map) && empty($field)) $field = $this->getPk();
$map = !is_array($map) ? [$field => $map] : $map;
return 0 < $this->getModel()->where($map)->count();
}
/**
* 根据条件获取一条数据
* @param array $where
* @param string|null $field
* @param array $with
* @return array|Model|null
* @throws \think\db\exception\DataNotFoundException
* @throws DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getOne(array $where, ?string $field = '*', array $with = [])
{
$field = explode(',', $field);
return $this->get($where, $field, $with);
}
/**
* 获取单个字段值
* @param $where
* @param string|null $field
* @param string $order
* @return mixed
*/
public function value($where, ?string $field = '', string $order = ''): mixed
{
$pk = $this->getPk();
return $this->getModel()->where($where)->order($order)->value($field ?: $pk);
}
/**
* 根据条件查询某一列数据
* @param array $where
* @param string $field
* @param string $key
* @return array
*/
public function getColumn(array $where, string $field, string $key = ''): array
{
return $this->getModel()->where($where)->column($field, $key);
}
/**
* 删除记录
*
* 该方法支持根据单个或多个条件删除记录当$id为数组时,表示根据多个条件删除记录;
* $id为单个值时,表示根据主键或指定的$key删除记录
*
* @param array|int|string $id 要删除的记录的ID或条件数组
* @param string|null $key $id为单个值时,指定的字段名,默认为主键
*
* @return bool 成功删除返回true,否则返回false
*/
public function delete(array|int|string $id, ?string $key = null): bool
{
// 判断$id是否为数组,如果是,则直接使用$id作为删除条件
if (is_array($id)) {
$where = $id;
} else {
// 如果$id不是数组,则根据是否设置了$key来决定使用主键还是$key作为删除条件
$where = [is_null($key) ? $this->getPk() : $key => $id];
}
// 使用确定的删除条件执行删除操作,并返回删除结果
return $this->getModel()->where($where)->delete();
}
/**
* 强制删除记录
*
* 该方法用于删除一个指定ID的资源。可以根据$force参数决定是软删除还是硬删除。
*
* @param int $id 要删除的资源的ID
* @param bool $force 是否强制删除,true为硬删除,false为软删除(默认)
* @return mixed 删除操作的结果,具体类型取决于getModel()->destroy方法的返回值
*/
public function destroy(int $id, bool $force = false)
{
return $this->getModel()->destroy($id, $force);
}
/**
* 更新数据
*
* 该方法用于更新数据库中与给定ID或条件相符的记录它接受一个ID(可以是数组、整数或字符串),
* 一个包含要更新的数据的数组,和一个可选的键名称如果ID是一个数组,它将直接用作查询条件
* 否则,它将与主键或提供的键名称一起构建查询条件
*
* @param array|int|string $id 要更新的记录的ID,可以是数组、整数或字符串如果是数组,则视为查询条件
* @param array $data 要更新的数据,以数组形式提供
* @param string|null $key 可选的键名称,用于指定查询时使用的键如果未提供,则使用主键
* @return mixed 更新操作的结果,具体类型取决于底层数据库操作
*/
public function update(array|int|string $id, array $data, ?string $key = null): mixed
{
// 根据$id的类型和$key的值构建查询条件
if (is_array($id)) {
$where = $id;
} else {
$where = [is_null($key) ? $this->getPk() : $key => $id];
}
// 调用模型的update方法执行数据库更新操作
return $this->getModel()::update($data, $where);
}
/**
* @param $where
* @return array|mixed
*/
protected function setWhere($where, ?string $key = null): mixed
{
if (!is_array($where)) {
$where = [is_null($key) ? $this->getPk() : $key => $where];
}
return $where;
}
/**
* 批量更新记录
*
* 该方法用于根据一组ID批量更新数据库中的记录它允许指定更新的数据以及可选的键,
* 如果未提供键,则使用主键进行更新操作
*
* @param array $ids 需要更新的记录的ID数组
* @param array $data 要更新的数据,以键值对形式表示
* @param string|null $key 可选参数,用于指定更新操作使用的键如果未提供,则默认使用主键
* @return BaseModel 更新操作的结果,
*/
public function batchUpdate(array $ids, array $data, ?string $key = null): BaseModel
{
// 获取模型实例,并使用whereIn方法来匹配IDs,如果$key为null,则使用主键
// 这里使用了三元运算符来判断$key是否为null,从而决定使用哪个键进行查询
// 最后调用update方法来执行批量更新操作
return $this->getModel()->whereIn(is_null($key) ? $this->getPk() : $key, $ids)->update($data);
}
/**
* 保存数据到模型
*
* 该方法主要用于将给定的数据数组保存到与模型关联的数据库表中它首先获取模型的字段信息,
* 然后对数据进行预处理,包括设置一些特殊字段的默认值最后,使用处理后的数据创建一个新的模型实例
*
* @param array $data 要保存到模型的数据数组
*/
public function save(array $data): BaseModel
{
// 获取模型的字段信息
$fields = $this->getModelField();
// 对数据进行预处理,设置特殊字段的默认值
$data = $this->setSpecialFieldDefaultValue($data,$fields);
// 使用处理后的数据创建并返回新的模型实例
return $this->getModel()::create($data);
}
/**
* 批量保存数据
*
* @param array $data 要保存的数据数组,每个元素代表一条记录
* @param bool $replace 是否使用替换插入的方式,默认为true
*
* @return mixed 保存结果,具体取决于底层数据库操作的返回值
* @throws \Exception
*/
public function saveAll(array $data, bool $replace = true): mixed
{
// 获取模型字段信息,为后续数据处理做准备
$fields = $this->getModelField();
// 初始化用于保存处理后数据的数组
$saveData = [];
// 遍历传入的数据数组,对每条记录进行处理
foreach ($data as $v) {
// 设置特殊字段的默认值,并将处理后的数据添加到保存数组中
$saveData[] = $this->setSpecialFieldDefaultValue($v,$fields);
}
// 调用模型的saveAll方法,批量保存处理后的数据
return $this->getModel()->saveAll($saveData, $replace);
}
/**
* 批量插入数据
*
* 该方法主要用于批量插入数据到数据库中它首先获取模型的字段信息,然后对输入的数据进行处理,
* 设置一些特殊字段的默认值,最后将处理后的数据批量插入数据库
*
* @param array $data 要插入的数据数组,每个元素代表一行数据
* @return int 插入操作影响的行数
*/
public function insertAll(array $data): int
{
// 获取模型字段信息,用于后续处理数据时参考
$fields = $this->getModelField();
// 初始化一个空数组,用于存储处理后的数据
$saveData = [];
// 遍历输入的数据数组
foreach ($data as $v) {
// 对每行数据,设置特殊字段的默认值,并将处理后的数据添加到保存数组中
$saveData[] = $this->setSpecialFieldDefaultValue($v,$fields);
}
// 调用模型的批量插入方法,插入处理后的数据,并返回插入操作影响的行数
return $this->getModel()->insertAll($saveData);
}
private function setSpecialFieldDefaultValue($data, $fields)
{
// if(in_array('id',$fields)){
// $data['id'] = $data['id'] ?? IdUtils::uuid();
// }
// if(session(('user'))){
// if(in_array('create_by',$fields)){
// $data['create_by'] = $data['create_by'] ?? session('user')['uid'];
// }
// if(in_array('update_by',$fields)){
// $data['update_by'] = $data['update_by'] ?? session('user')['uid'];
// }
// }
$admininfo = RequestUtils::getAdminInfo();
if($admininfo){
if(in_array('create_by',$fields)){
$data['create_by'] = $data['create_by'] ?? $admininfo['id'];
}
if(in_array('update_by',$fields)){
$data['update_by'] = $data['update_by'] ?? $admininfo['id'];
}
}
if(in_array('create_time',$fields)){
$data['create_time'] = $data['create_time'] ?? time();
}
if(in_array('update_time',$fields)){
$data['update_time'] = $data['update_time'] ?? time();
}
return $data;
}
/**
* 获取搜索器和搜索条件key,以及不在搜索器的条件数组
* @param array $where
* @return array[]
* @throws ReflectionException
*/
private function getSearchData(array $where)
{
$with = [];
$otherWhere = [];
$responses = new \ReflectionClass($this->setModel());
foreach ($where as $key => $value) {
if($value === '' || $value === null){
continue;
}
$method = 'search' . Str::studly($key) . 'Attr';
if ($responses->hasMethod($method)) {
$with[] = $key;
} else {
if(!in_array($key,['user'])){
if (!is_array($value)) {
$otherWhere[] = [$key, '=', $value];
} else if (count($value) === 3) {
$otherWhere[] = $value;
}
}
}
}
return [$with, $otherWhere];
}
/**
* 根据搜索器获取搜索内容
* @param $where
* @param $search
* @return BaseModel
* @throws ReflectionException
*/
protected function withSearchSelect($where, $search)
{
[$with, $otherWhere] = $this->getSearchData($where);
return $this->getModel()->withSearch($with, $where)->when($search, function ($query) use ($otherWhere) {
$query->where($this->filterWhere($otherWhere));
});
}
/**
* 过滤数据表中不存在的where条件字段
* @param array $where
* @return array
*/
protected function filterWhere(array $where = []): array
{
$fields = $this->getModel()->getTableFields();
foreach ($where as $key => $item) {
// 带有表别名前缀的去掉前缀
$cols = explode('.', $item[0]);
$col = $cols[count($cols) - 1];
if (!in_array($col, $fields)) {
unset($where[$key]);
}
}
return $where;
}
/**
* 根据给定的条件搜索模型
*
* 当提供了一个非空的条件数组时,该方法会执行一个带有搜索条件的查询;
* 如果条件数组为空,则直接返回模型实例
*
* @param array $where 搜索条件数组,用于指定查询条件,默认为空数组
* @param bool $search 是否执行搜索的标志,用于控制是否应用搜索条件,默认为true
*
* @return mixed 返回一个BaseModel实例,具体类型取决于getModel和withSearchSelect方法的返回值
*/
public function search(array $where = [], bool $search = true): mixed
{
// 判断是否有搜索条件提供
if ($where) {
//载入数据权限
$userInfo = RequestUtils::getUserInfo();
$where[] = ['create_by', 'in', $userInfo['dataPermission']];
// 如果有搜索条件,则调用withSearchSelect方法执行带有搜索条件的查询
return $this->withSearchSelect($where, $search);
} else {
// 如果没有搜索条件,则直接返回模型实例
return $this->getModel();
}
}
/**
* 求和
* @param array $where
* @param string $field
* @param bool $search
* @return float
* @throws ReflectionException
*/
public function sum(array $where, string $field, bool $search = false): float
{
if ($search) {
return $this->search($where)->sum($field);
} else {
return $this->getModel()->where($where)->sum($field);
}
}
/**
* 高精度加法
* @param $key
* @param string $incField
* @param string $inc
* @param string|null $keyField
* @param int $acc
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function bcInc($key, string $incField, string $inc, string $keyField = null, int $acc = 2): bool
{
return $this->bc($key, $incField, $inc, $keyField, 1);
}
/**
* 高精度 减法
* @param $key
* @param string $decField
* @param string $dec
* @param string|null $keyField
* @param int $acc
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function bcDec($key, string $decField, string $dec, string $keyField = null, int $acc = 2): bool
{
return $this->bc($key, $decField, $dec, $keyField, 2);
}
/**
* 高精度计算并保存
* @param $key
* @param string $incField
* @param string $inc
* @param string|null $keyField
* @param int $type
* @param int $acc
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function bc($key, string $incField, string $inc, string $keyField = null, int $type = 1, int $acc = 2): bool
{
if ($keyField === null) {
$result = $this->get($key);
} else {
$result = $this->getOne([$keyField => $key]);
}
if (!$result) return false;
$new = 0;
if ($type === 1) {
$new = bcadd($result[$incField], $inc, $acc);
} else if ($type === 2) {
if ($result[$incField] < $inc) return false;
$new = bcsub($result[$incField], $inc, $acc);
}
$result->{$incField} = $new;
return false !== $result->save();
}
/**
* 获取条件数据中的某个值的最大值
* @param array $where
* @param string $field
* @return mixed
*/
public function getMax(array $where = [], string $field = ''): mixed
{
return $this->getModel()->where($where)->max($field);
}
/**
* 获取条件数据中的某个值的最小值
* @param array $where
* @param string $field
* @return mixed
*/
public function getMin(array $where = [], string $field = ''): mixed
{
return $this->getModel()->where($where)->min($field);
}
/**
* 软删除
* @param string $id
* @return mixed
*/
public function softDel(string $id): mixed
{
return $this->getModel()->where('id', $id)->update(['delete_time' => time()]);
}
/**
* 保存唯一验证
* @param array $where
* @param string $msg
*/
public function saveUnique(array $where, string $msg = '', $field = '*'): void
{
$res = $this->getModel()->field($field)->where($where)->findOrEmpty()->toArray();
if (!empty($res)) {
throw new ApiException($msg);
}
}
/**
* 更新唯一验证
* @param array $where
* @param int $id
* @param string $msg
*/
public function updateUnique(array $where, $id, string $msg = '', $field = '*'): void
{
$res = $this->getModel()->field($field)->where($where)->findOrEmpty()->toArray();
if (!empty($res) && $res['id'] != $id) {
throw new ApiException($msg);
}
}
public function getList($where, $field='*', $page=0, $limit=0, $order='id desc',$with=[],$withCOunt=[]): array
{
$query = $this->search($where)->field($field)->with($with)->withCount($withCOunt)->order($order);
if($page !== 0 && $limit !== 0){
$query = $query->page($page,$limit);
}
return $query->select()->toArray();
}
/**
* 批量插入或更新
* @param array $data
* @param string $duplicateKey
* @return bool
* @throws ApiException
*/
public function batchInsertOrUpdate(array $data,$duplicateKey = 'id'): bool
{
if(empty($data)){
return true;
}
// update on duplicate key
$fields = $this->getModel()->getTableFields();
// 获取所有字段
$userFields = array_keys($data[0]);
// 检查字段是否存在
foreach ($userFields as $field) {
if (!in_array($field, $fields)) {
throw new ApiException($field . '字段不存在',400 );
}
}
$fields = $userFields;
$table = $this->getModel()->getTable();
// 将数据转换为 SQL 语句中的值部分
$values = [];
foreach ($data as $row) {
$valueStr = '(' . implode(',', array_map(function ($value) {
return "'" . addslashes($value) . "'";
}, $row)) . ')';
$values[] = $valueStr;
}
// 构建更新部分的 SQL 语句
$updateStr = '';
foreach ($fields as $field) {
// 移除duplicateKey
if($field == $duplicateKey){
continue;
}
$updateStr .= "`$field` = VALUES(`$field`),";
}
// 构建字段部分的 SQL 语句
$fieldStr = implode(',', array_map(function ($field) {
return "`$field`";
}, $fields));
// 移除最后一个逗号
$updateStr = rtrim($updateStr, ',');
// 构建完整的 SQL 语句
$sql = "INSERT INTO {$table} ({$fieldStr}) VALUES " . implode(',', $values) . " ON DUPLICATE KEY UPDATE {$updateStr}";
// 使用 execute 方法执行 SQL 语句
return Db::execute($sql);
}
/**
* 根据条件更新数据
* @param array $where
* @param array $data
*/
public function updateByWhere(array $where, array $data)
{
return $this->getModel()->where($where)->update($data);
}
/**
* 获取模型字段列表
*
* 该方法用于获取当前模型所在表的所有字段名称
* 它通过查询数据库表的列信息来实现
*
* @return array 返回字段名称数组
*/
public function getModelField(): array
{
// 获取模型对应的表名
$table = $this->getModel()->getTable();
// 执行SQL语句,获取表中所有列的详细信息
$fields = Db::query("SHOW FULL COLUMNS FROM $table");
// 从查询结果中提取所有字段名,并返回
return array_column($fields, 'Field');
}
public function __call(string $name, array $arguments)
{
return call_user_func_array([$this->getModel(), $name], $arguments);
}
}

45
plugin/piadmin/app/controller/v1/UserLoginController.php

@ -0,0 +1,45 @@
<?php
namespace plugin\piadmin\app\controller\v1;
use plugin\piadmin\app\base\BaseController;
use plugin\piadmin\app\service\UserService;
use plugin\piadmin\app\utils\Captcha;
use plugin\piadmin\app\validate\LoginValidate;
use support\Request;
class UserLoginController extends BaseController
{
// 无需登录方法
protected $noNeedLogin = ['login','captcha'];
public function __construct()
{
parent::__construct();
$this->service = app()->make(UserService::class);
}
public function login(Request $request){
$params = $request->only([
'username',
'password'
]);
validate(LoginValidate::class)->scene('loginByPassword')->check($params);
// 用户登录
$res = $this->service->login($params);
return success($res);
}
public function captcha(){
$captcha = Captcha::imageCaptcha();
return success($captcha);
}
}

17
plugin/piadmin/app/dao/UserDao.php

@ -0,0 +1,17 @@
<?php
namespace plugin\piadmin\app\dao;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\model\User;
class UserDao extends BaseDao
{
protected function setModel(): string
{
return User::class;
}
}

82
plugin/piadmin/app/middleware/UserAuthorizationMiddleware.php

@ -0,0 +1,82 @@
<?php
namespace plugin\piadmin\app\middleware;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use plugin\piadmin\app\dao\UserDao;
use plugin\piadmin\app\exception\ApiException;
use plugin\piadmin\app\utils\CacheUtils;
use support\Context;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;
class UserAuthorizationMiddleware implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
$authHeader = config('plugin.piadmin.piadmin.jwt.user.auth_header');
$token = $request->header($authHeader, '');
if (empty($token)) {
throw new ApiException(403);
}
$token = trim(str_replace('Bearer ', '', $token));
// 解token payload
$key = config("plugin.piadmin.piadmin.jwt.user.key");
$keyId = config("plugin.piadmin.piadmin.jwt.user.key_id");
$payload = JWT::decode($token, new Key($key, 'HS256'));
if (empty($token)) {
throw new ApiException(403);
}
// 缓存
$tokenCacheData = CacheUtils::get('user.token.'. md5($token));
if (empty($tokenCacheData)) {
throw new ApiException(403);
}
$tokenCacheData = json_decode($tokenCacheData, true);
if (empty($tokenCacheData) || !isset($tokenCacheData['uid'])) {
throw new ApiException(403);
}
// 验证账号状态
$uid = $tokenCacheData['uid'];
$userDao = app()->make(UserDao::class);
$user = $userDao->getOne(['id' => $uid]);
if (empty($user)) {
throw new ApiException(403);
}
if ($user['status'] != 1 ) {
throw new ApiException(400006);
}
$userInfo = $user->toArray();
$userInfo['is_login'] = true;
$userInfo['uid'] = $userInfo['id'];
$userInfo['token'] = $token;
//数据权限,目前只查自己
$userInfo['dataPermission'] = [$userInfo['id']];
// 检查版本
$this->checkVersionKey($uid, $token);
$request->user = $userInfo;
// todo 补全session
Context::set('user', $userInfo);
// session('user', $userInfo);
return $handler($request);
}
/**
* 版本检查
* @param string|int $uid
* @param string $token
* @return void
* @throws ApiException
*/
private function checkVersionKey(string|int $uid, string $token): void
{
$userVersionKey = "user.version.{$uid}";
$version = CacheUtils::get($userVersionKey);
if ($version != getPiAdminVersion()) {
CacheUtils::delete(md5($token));
throw new ApiException(403);
}
}
}

21
plugin/piadmin/app/model/User.php

@ -0,0 +1,21 @@
<?php
namespace plugin\piadmin\app\model;
use plugin\piadmin\app\base\BaseModel;
use think\model\concern\SoftDelete;
class User extends BaseModel
{
protected $table = 'pi_user';
protected $hidden = ['password'];
use SoftDelete;
protected string $deleteTime = 'delete_time';
protected $defaultSoftDelete = 0;
protected $append = [];
}

6
plugin/piadmin/app/route/v1/attachment.php

@ -2,6 +2,7 @@
use plugin\piadmin\app\controller\v1\AttachmentController;
use plugin\piadmin\app\middleware\AdminAuthorizationMiddleware;
use plugin\piadmin\app\middleware\UserAuthorizationMiddleware;
use Webman\Route;
Route::group('/piadmin/v1', function () {
@ -14,3 +15,8 @@ Route::group('/piadmin/v1', function () {
Route::post('/attachment/delete', [AttachmentController::class, 'delete']);
})->middleware([AdminAuthorizationMiddleware::class]);
Route::group('/piadmin/v1/user', function () {
//文件上传
Route::post('/upload', [AttachmentController::class, 'upload']);
})->middleware([UserAuthorizationMiddleware::class]);

2
plugin/piadmin/app/route/v1/route.php

@ -7,6 +7,7 @@ use plugin\piadmin\app\controller\v1\MenuController;
use plugin\piadmin\app\controller\v1\SystemAdminController;
use plugin\piadmin\app\controller\v1\SystemDeptController;
use plugin\piadmin\app\controller\v1\SystemRoleController;
use plugin\piadmin\app\controller\v1\UserLoginController;
use plugin\piadmin\app\middleware\AdminAuthorizationMiddleware;
use plugin\piadmin\app\middleware\PermissionsMiddleware;
use Webman\Route;
@ -16,6 +17,7 @@ Route::group('/piadmin/v1', function () {
Route::any('/test', [IndexController::class, 'test']);
Route::any('/testlogin', [IndexController::class, 'index']);
Route::post('/login', [LoginController::class, 'login'])->setParams(['perm' => ['login']]);
Route::post('/user/login', [UserLoginController::class, 'login'])->setParams(['perm' => ['userLogin']]);
Route::get('/captcha', [LoginController::class, 'captcha'])->setParams(['perm' => ['captcha']]);
});

134
plugin/piadmin/app/service/UserService.php

@ -0,0 +1,134 @@
<?php
namespace plugin\piadmin\app\service;
use plugin\piadmin\app\base\BaseService;
use plugin\piadmin\app\dao\UserDao;
use plugin\piadmin\app\exception\ApiException;
use plugin\piadmin\app\utils\CacheUtils;
use plugin\piadmin\app\utils\JwtUtils;
class UserService extends BaseService
{
public function __construct(UserDao $dao)
{
$this->dao = $dao;
}
// 用户登录
public function login($params)
{
$user = $this->dao->getModel()->where(function ($query) use ($params){
$query->where('email', '=', $params['username'])
->whereOr('phone', '=', $params['username']);
})->find();
$this->validPassword($user, $params);
$tokenInfo = $this->commonLogin($user, $params);
$tokenInfo['user'] = $user->toArray();
$tokenInfo['expire_time'] = $tokenInfo['expire'] + time();
return $tokenInfo;
}
// ============================================================ 私有方法 ===============================================
/**
* 验证密码
* @param mixed $adminUser
* @param $params
* @return void
*/
private function validPassword(mixed $adminUser, $params): void
{
// 验证登录信息
if (empty($adminUser)) {
throw new ApiException(trans(400005));
}
// 密码验证
if (!password_verify($params['password'], $adminUser['password'])) {
throw new ApiException(trans(400005));
}
}
/**
* 通用登录流程
* @param mixed $adminUser
* @param array $params
* @return array
* @throws ApiException
*/
private function commonLogin(mixed $user, array $params)
{
if ($user['status'] == 2) {
throw new ApiException(trans(400006));
}
$tokenInfo = JwtUtils::createToken($user['id']);
$tokenCacheInfo = [
'uid' => $user['id'],
'type' => 'user_token',
'token' => $tokenInfo['token'],
'expire_time' => time() + $tokenInfo['expire'],
'user' => $user->toArray(),
'login_time' => date('Y-m-d H:i:s'),
'os_type' => $this->getClientOS(),
'browser_name' => $this->getClientBrowserName(),
'login_ip' => request()->getRemoteIp()
];
// 保存token缓存
CacheUtils::set('user.token.' . md5($tokenInfo['token']), json_encode($tokenCacheInfo), (int)$tokenInfo['expire']);
// 保存用户对应版本缓存
$currentVersion = getPiAdminVersion();
$userVersionKey = "user.version.{$user['id']}";
CacheUtils::set($userVersionKey, $currentVersion, (int)$tokenInfo['expire']);
return $tokenInfo;
}
/**
* 获取客户端操作系统
* @return int
*/
private function getClientOS(): int
{
$userAgent = request()->header('user-agent');
if (stripos($userAgent, 'Windows') !== false) {
$os = 1;
} elseif (stripos($userAgent, 'Mac') !== false) {
$os = 2;
} elseif (stripos($userAgent, 'Linux') !== false) {
$os = 3;
} elseif (stripos($userAgent, 'iPhone|iPad|iPod') !== false) {
$os = 4;
} elseif (stripos($userAgent, 'Android') !== false) {
$os = 5;
} else {
$os = 6;
}
return $os;
}
/**
* 获取客户端浏览器名称
* @return string
*/
private function getClientBrowserName(): string
{
$userAgent = request()->header('user-agent');
$pattern = '/(MSIE|Trident|Edge|Firefox|Chrome|Safari|Opera|OPR)\/?\s*(\d+\.\d+)?/i';
preg_match($pattern, $userAgent, $matches);
if (isset($matches[1])) {
// 处理Edge和Opera的特殊标识
if ($matches[1] === 'Trident') return 'Internet Explorer';
if ($matches[1] === 'OPR') return 'Opera';
return $matches[1];
}
return 'Unknown Browser';
}
}

24
plugin/piadmin/app/utils/RequestUtils.php

@ -57,6 +57,30 @@ class RequestUtils
return $admininfo;
}
public static function getUserInfo(): array{
$userinfo = [
'id'=>'',
'name'=>'',
'dataPermission'=> ''
];
try{
$user = request()->user;
if(!empty($user)){
$userinfo = [
'id'=>$user['id'] ?? '',
'name'=>$user['name'] ?? '',
'dataPermission'=>$user['dataPermission'] ?? ''
];
}
}catch(\Throwable $e){
}
return $userinfo;
}
/**
* 获取本次请求的排序参数
* @return array

Loading…
Cancel
Save