AdminRepository.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2016~2024 https://www.crmeb.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
  8. // +----------------------------------------------------------------------
  9. // | Author: CRMEB Team <admin@crmeb.com>
  10. // +----------------------------------------------------------------------
  11. namespace app\common\repositories\system\admin;
  12. //附件
  13. use app\common\dao\system\admin\AdminDao;
  14. use app\common\model\system\admin\Admin;
  15. use app\common\repositories\BaseRepository;
  16. use app\common\repositories\system\auth\RoleRepository;
  17. use crmeb\exceptions\AuthException;
  18. use crmeb\services\JwtTokenService;
  19. use FormBuilder\Exception\FormBuilderException;
  20. use FormBuilder\Factory\Elm;
  21. use FormBuilder\Form;
  22. use think\db\exception\DataNotFoundException;
  23. use think\db\exception\DbException;
  24. use think\db\exception\ModelNotFoundException;
  25. use think\exception\ValidateException;
  26. use think\facade\Cache;
  27. use think\facade\Config;
  28. use think\facade\Route;
  29. use think\Model;
  30. use app\common\repositories\system\merchant\MerchantRegionRepository;
  31. /**
  32. * 后台管理员
  33. */
  34. class AdminRepository extends BaseRepository
  35. {
  36. public function __construct(AdminDao $dao)
  37. {
  38. /**
  39. * @var AdminDao
  40. */
  41. $this->dao = $dao;
  42. }
  43. /**
  44. * 获取角色列表
  45. *
  46. * 根据给定的条件数组$where,分页获取角色列表。每页包含$limit个角色。
  47. * 返回包含角色列表和总角色数量的数据数组。
  48. *
  49. * @param array $where 查询条件数组
  50. * @param int $page 当前页码
  51. * @param int $limit 每页的角色数量
  52. * @return array 包含角色列表和总角色数量的数组
  53. */
  54. public function getList(array $where, $page, $limit)
  55. {
  56. // 根据条件查询角色信息
  57. $query = $this->dao->search($where);
  58. // 计算满足条件的角色总数量
  59. $count = $query->count();
  60. // 获取当前页的角色列表,隐藏某些字段,并进行分页
  61. $list = $query->page($page, $limit)->hidden(['pwd', 'is_del', 'update_time'])->select()->append(['region_name']);
  62. // 遍历角色列表,为每个角色添加角色名
  63. foreach ($list as $k => $role) {
  64. $list[$k]['rule_name'] = $role->roleNames();
  65. }
  66. // 返回角色列表和总数量
  67. return compact('list', 'count');
  68. }
  69. /**
  70. * 对密码进行加密处理
  71. *
  72. * 本函数使用BCrypt算法对密码进行加密。BCrypt是一种基于口令的加密算法,设计用于存储和验证用户密码。
  73. * 选择BCrypt算法是因为其在安全性和抗暴力破解方面的优势。
  74. *
  75. * @param string $password 需要加密的原始密码
  76. * @return string 返回加密后的密码hash值
  77. */
  78. public function passwordEncode($password)
  79. {
  80. return password_hash($password, PASSWORD_BCRYPT);
  81. }
  82. /**
  83. * 更新
  84. * @param int $id id
  85. * @param array $data 数组
  86. * @return int
  87. * @throws DbException
  88. * @author 张先生
  89. * @date 2020-03-26
  90. */
  91. public function update(int $id, array $data)
  92. {
  93. if (isset($data['roles']))
  94. $data['roles'] = implode(',', $data['roles']);
  95. return $this->dao->update($id, $data);
  96. }
  97. /**
  98. * 创建修改密码的表单
  99. *
  100. * 该方法用于生成一个用于修改密码的表单。表单的提交URL根据$isSelf参数的不同而变化,
  101. * 用于区分是修改当前管理员的密码还是其他管理员的密码。表单包含两个密码字段,
  102. * 用于输入新密码和确认新密码。
  103. *
  104. * @param int $id 管理员ID,当$isSelf为false时,用于指定修改哪个管理员的密码。
  105. * @param bool $isSelf 指示是否修改当前管理员的密码。如果为true,则修改当前管理员的密码;
  106. * 如果为false,则修改指定ID的管理员的密码。
  107. * @return Form|\think\form\Form
  108. */
  109. public function passwordForm(int $id, $isSelf = false)
  110. {
  111. // 根据$isSelf参数构建表单的提交URL
  112. $url = Route::buildUrl($isSelf ? 'systemAdminEditPassword' : 'systemAdminPassword', $isSelf ? [] : compact('id'))->build();
  113. // 创建表单,并定义表单包含的字段:密码和确认密码
  114. $form = Elm::createForm($url, [
  115. $rules[] = Elm::password('pwd', '密码:')->placeholder('请输入密码')->required(),
  116. $rules[] = Elm::password('againPassword', '确认密码:')->placeholder('请输入确认密码')->required(),
  117. ]);
  118. // 设置表单的标题
  119. return $form->setTitle('修改密码');
  120. }
  121. /**
  122. * 创建编辑管理员信息的表单
  123. *
  124. * 该方法通过Elm库构建一个表单,用于编辑管理员的个人信息。表单中包含了管理员ID、姓名和电话等必填项。
  125. * 表单的提交地址是通过路由系统生成的管理员编辑地址。
  126. *
  127. * @param array $formData 管理员的当前数据,用于填充表单
  128. * @return Form|\think\form\Form
  129. */
  130. public function editForm(array $formData)
  131. {
  132. // 创建表单对象,并设置表单的提交URL
  133. $form = Elm::createForm(Route::buildUrl('systemAdminEdit')->build());
  134. // 设置表单的验证规则和字段
  135. $form->setRule([
  136. // 设置管理员姓名字段,必填,带有占位提示
  137. Elm::input('admin_id', '管理员ID:')->disabled(true),
  138. Elm::input('real_name', '管理员姓名:')->placeholder('请输入管理员姓名')->required(),
  139. // 设置联系电话字段,非必填,带有占位提示
  140. Elm::input('phone', '联系电话:')->placeholder('请输入联系电话')
  141. ]);
  142. // 设置表单标题,并加载传入的管理员数据
  143. return $form->setTitle('修改信息')->formData($formData);
  144. }
  145. /**
  146. * 创建或编辑管理员表单
  147. *
  148. * 该方法用于生成一个包含各种输入字段的表单,用于创建或编辑系统管理员。
  149. * 表单字段包括选择角色、管理员姓名、账号、电话,以及在创建时的密码和确认密码。
  150. * 表单的URL和提交动作根据是否在编辑现有管理员的情况下动态确定。
  151. *
  152. * @param int|null $id 管理员ID,如果为null,则表示正在创建新管理员;否则,表示正在编辑现有管理员。
  153. * @param array $formData 表单数据数组,用于预填充表单字段。
  154. * @return Form 返回生成的表单对象。
  155. */
  156. public function form(?int $id = null, array $formData = []): Form
  157. {
  158. // 根据$id的值决定表单提交的URL,如果是新建则指向create路由,否则指向update路由,并带上$id。
  159. $form = Elm::createForm(is_null($id) ? Route::buildUrl('systemAdminCreate')->build() : Route::buildUrl('systemAdminUpdate', ['id' => $id])->build());
  160. // 定义表单的验证规则和字段。
  161. $rules = [
  162. // 选择角色字段,使用多重选择,选项从RoleRepository获取。
  163. Elm::select('roles', '身份:', [])->options(function () {
  164. $data = app()->make(RoleRepository::class)->getAllOptions(0);
  165. $options = [];
  166. foreach ($data as $value => $label) {
  167. $options[] = compact('value', 'label');
  168. }
  169. return $options;
  170. })->multiple(true)->required(),
  171. // 管理员姓名输入字段。
  172. Elm::input('real_name', '管理员姓名:')->placeholder('请输入管理员姓名'),
  173. // 账号输入字段,必需。
  174. Elm::input('account', '账号:')->placeholder('请输入账号')->required(),
  175. // 电话输入字段。
  176. Elm::input('phone', '联系电话:')->placeholder('请输入联系电话'),
  177. Elm::cascader('region_ids', '选择管理分组:')->options(function (){
  178. $data = app()->make(MerchantRegionRepository::class)->getAllOptions(null);
  179. return formatCascaderData($data,'name');
  180. })->props(['props' => ['checkStrictly' => true, 'emitPath' => false, 'multiple' => true]]),
  181. ];
  182. // 如果是新建管理员,则添加密码和确认密码字段。
  183. if (!$id) {
  184. $rules[] = Elm::password('pwd', '密码:')->placeholder('请输入密码')->required();
  185. $rules[] = Elm::password('againPassword', '确认密码:')->placeholder('请输入确认密码')->required();
  186. }
  187. // 开启/关闭状态开关字段。
  188. $rules[] = Elm::switches('status', '账号状态:', 1)->width(60)->inactiveValue(0)->activeValue(1)->inactiveText('停用')->activeText('正常');
  189. // 设置表单的验证规则。
  190. $form->setRule($rules);
  191. // 设置表单标题和初始数据。
  192. return $form->setTitle(is_null($id) ? '添加管理员' : '编辑管理员')->formData($formData);
  193. }
  194. /**
  195. * 更新表单数据。
  196. * 该方法通过指定的ID获取表单数据,并使用这些数据来更新表单。
  197. * 主要用于在前端展示已存在数据的表单,以便用户可以查看并修改这些数据。
  198. *
  199. * @param int $id 表单数据的唯一标识ID。
  200. * @return array 返回包含表单数据的数组。
  201. */
  202. public function updateForm(int $id)
  203. {
  204. // 通过ID获取表单数据,并转换为数组格式,用于更新表单
  205. return $this->form($id, $this->dao->get($id)->toArray());
  206. }
  207. /**
  208. * 管理员登录方法
  209. *
  210. * 本方法用于处理管理员的登录逻辑。首先,它触发一个登录前的事件,允许任何监听者对此过程进行干预。
  211. * 接着,它验证管理员账号和密码是否正确。如果登录信息不正确,会记录登录失败次数,并抛出一个验证异常。
  212. * 如果账号存在但被禁用,也会抛出一个验证异常。如果账号验证成功,更新管理员的登录信息,如最后登录时间、IP和登录次数,
  213. * 并触发一个登录成功的事件。最后,返回管理员信息。
  214. *
  215. * @param string $account 管理员账号
  216. * @param string $password 管理员密码
  217. * @return array|object
  218. * @throws ValidateException 如果登录失败或账号被禁用
  219. */
  220. public function login(string $account, string $password)
  221. {
  222. // 触发登录前的事件,允许进行额外的验证或操作
  223. event('admin.login.before',compact('account','password'));
  224. // 根据管理员账号查询管理员信息
  225. $adminInfo = $this->dao->accountByAdmin($account);
  226. // 验证管理员信息是否存在且密码是否正确
  227. if (!$adminInfo || !password_verify($password, $adminInfo->pwd)){
  228. // 记录登录失败次数,防止恶意登录尝试
  229. $key = 'sys_login_failuree_'.$account;
  230. $numb = Cache::get($key) ?? 0;
  231. $numb++;
  232. Cache::set($key,$numb,15*60);
  233. throw new ValidateException('账号或密码错误');
  234. }
  235. // 检查管理员账号是否被禁用
  236. if ($adminInfo['status'] != 1)
  237. throw new ValidateException('账号已关闭');
  238. // 更新管理员的登录信息
  239. $adminInfo->last_time = date('Y-m-d H:i:s');
  240. $adminInfo->last_ip = app('request')->ip();
  241. $adminInfo->login_count++;
  242. $adminInfo->save();
  243. // 触发登录成功的事件,允许进行额外的操作,如记录日志等
  244. event('admin.login',compact('adminInfo'));
  245. // 返回管理员信息
  246. return $adminInfo;
  247. }
  248. /**
  249. * 登录尝试次数限制
  250. * @param $account
  251. * @param int $number
  252. * @param int $n
  253. * @author Qinii
  254. * @day 7/6/21
  255. */
  256. public function loginFailure($account,$number = 5,$n = 3)
  257. {
  258. $key = 'sys_login_failuree_'.$account;
  259. $numb = Cache::get($key) ?? 0;
  260. $numb++;
  261. if($numb >= $number){
  262. $fail_key = 'sys_login_freeze_'.$account;
  263. Cache::tag('sys_login_freeze')->set($fail_key,1,15*60);
  264. throw new ValidateException('账号或密码错误次数太多,请稍后在尝试');
  265. }else{
  266. Cache::tag('sys_login_freeze')->set($key,$numb,5*60);
  267. $msg = '账号或密码错误';
  268. $_n = $number - $numb;
  269. if($_n <= $n){
  270. $msg .= ',还可尝试'.$_n.'次';
  271. }
  272. throw new ValidateException($msg);
  273. }
  274. }
  275. /**
  276. * 缓存管理员令牌
  277. *
  278. * 本函数用于缓存管理员的登录令牌,以便在后续请求中验证管理员的身份。
  279. * 令牌的缓存时间由$exp参数指定,单位为秒。缓存机制能够减少数据库的访问频率,
  280. * 提高系统性能,尤其是在高并发的场景下。
  281. *
  282. * @param string $token 管理员的登录令牌
  283. * @param int $exp 令牌的过期时间,单位为秒
  284. */
  285. public function cacheToken(string $token, int $exp)
  286. {
  287. // 通过键名'admin_'和当前时间加上过期时间来设置令牌的缓存
  288. Cache::set('admin_' . $token, time() + $exp, $exp);
  289. }
  290. /**
  291. * 检查管理员的令牌是否有效。
  292. *
  293. * 本函数通过验证令牌是否存在以及是否过期来确保管理员的会话仍然有效。
  294. * 如果令牌不存在或已过期,则抛出一个授权异常。
  295. *
  296. * @param string $token 管理员的令牌。这是用于验证管理员身份的唯一字符串。
  297. * @throws AuthException 如果令牌无效或已过期,则抛出此异常。
  298. */
  299. public function checkToken(string $token)
  300. {
  301. // 检查令牌是否存在于缓存中
  302. $has = Cache::has('admin_' . $token);
  303. // 如果令牌不存在,则抛出授权异常
  304. if (!$has)
  305. throw new AuthException('无效的token');
  306. // 获取令牌的最后活动时间
  307. $lastTime = Cache::get('admin_' . $token);
  308. // 检查令牌是否过期,如果过期,则抛出授权异常
  309. if (($lastTime + (intval(Config::get('admin.token_valid_exp', 15))) * 60) < time())
  310. throw new AuthException('token 已过期,请重新登录');
  311. }
  312. /**
  313. * 更新管理员令牌的缓存
  314. *
  315. * 本函数用于更新管理员令牌的缓存时间。令牌的缓存时间基于配置文件中设定的令牌有效时长,
  316. * 默认为15分钟。通过将令牌的缓存时间设置为当前时间,可以确保令牌在一段时间内保持有效。
  317. * 这对于维护会话状态和确保安全性非常重要。
  318. *
  319. * @param string $token 管理员的令牌字符串。这是用于标识和验证管理员身份的唯一令牌。
  320. */
  321. public function updateToken(string $token)
  322. {
  323. // 根据管理员令牌生成缓存键,并设置缓存时间为配置的令牌有效时长(默认为15分钟)。
  324. Cache::set('admin_' . $token, time(), intval(Config::get('admin.token_valid_exp', 15)) * 60);
  325. }
  326. /**
  327. * 清除指定的管理员令牌
  328. *
  329. * 本函数用于从缓存中删除指定的管理员令牌,以实现登出功能或令牌失效。
  330. * 通过删除令牌,可以确保之前的登录状态被正确终止,防止未授权的访问。
  331. *
  332. * @param string $token 需要清除的管理员令牌
  333. */
  334. public function clearToken(string $token)
  335. {
  336. // 根据管理员令牌生成缓存键,并删除该缓存项
  337. Cache::delete('admin_' . $token);
  338. }
  339. /**
  340. * 创建管理员令牌
  341. *
  342. * 本函数用于生成管理员的JWT令牌,该令牌用于管理员的身份验证。
  343. * 它通过JwtTokenService创建令牌,并将令牌及其过期时间存储到缓存中。
  344. *
  345. * @param Admin $admin 管理员对象,包含管理员的身份信息。
  346. * @return array 返回包含令牌和过期时间的数组。
  347. */
  348. public function createToken(Admin $admin)
  349. {
  350. // 实例化JWT令牌服务类
  351. $service = new JwtTokenService();
  352. // 从配置中获取管理员令牌的过期时间,默认为3小时
  353. $exp = intval(Config::get('admin.token_exp', 3));
  354. // 使用JwtTokenService创建令牌,指定管理员ID、类型为'admin',以及过期时间
  355. $token = $service->createToken($admin->admin_id, 'admin', strtotime("+ {$exp}hour"));
  356. // 将生成的令牌及其过期时间存储到缓存中
  357. $this->cacheToken($token['token'], $token['out']);
  358. // 返回生成的令牌信息
  359. return $token;
  360. }
  361. /**
  362. * 检测验证码
  363. * @param string $key key
  364. * @param string $code 验证码
  365. * @author 张先生
  366. * @date 2020-03-26
  367. */
  368. public function checkCode(string $key, string $code)
  369. {
  370. if (!env('DEVELOPMENT',false)) {
  371. $_code = Cache::get('am_captcha' . $key);
  372. if (!$_code) {
  373. throw new ValidateException('验证码过期');
  374. }
  375. if (strtolower($_code) != strtolower($code)) {
  376. throw new ValidateException('验证码错误');
  377. }
  378. //删除code
  379. Cache::delete('am_captcha' . $key);
  380. }
  381. }
  382. /**
  383. * 创建登录验证码键
  384. *
  385. * 本函数用于生成一个唯一的登录验证码键,并将该验证码与键关联起来存储在缓存中。
  386. * 验证码键的生成结合了微秒时间和随机数,以确保唯一性。
  387. * 验证码在缓存中的有效期通过配置文件设定,以分钟为单位。
  388. *
  389. * @param string $code 登录验证码
  390. * @return string 生成的验证码键
  391. */
  392. public function createLoginKey(string $code)
  393. {
  394. // 生成一个唯一的验证码键,基于当前微秒时间戳和随机数
  395. $key = uniqid(microtime(true), true);
  396. // 将验证码与键关联,存储到缓存中,并设定过期时间
  397. // 缓存有效期通过配置文件获取,默认为5分钟
  398. Cache::set('am_captcha' . $key, $code, Config::get('admin.captcha_exp', 5) * 60);
  399. // 返回生成的验证码键
  400. return $key;
  401. }
  402. }