MerchantAdminRepository.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  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\merchant;
  12. use app\common\dao\BaseDao;
  13. use app\common\dao\system\merchant\MerchantAdminDao;
  14. use app\common\model\system\merchant\Merchant;
  15. use app\common\model\system\merchant\MerchantAdmin;
  16. use app\common\repositories\BaseRepository;
  17. use app\common\repositories\system\auth\RoleRepository;
  18. use crmeb\exceptions\AuthException;
  19. use crmeb\services\JwtTokenService;
  20. use FormBuilder\Exception\FormBuilderException;
  21. use FormBuilder\Factory\Elm;
  22. use FormBuilder\Form;
  23. use think\db\exception\DataNotFoundException;
  24. use think\db\exception\DbException;
  25. use think\db\exception\ModelNotFoundException;
  26. use think\exception\ValidateException;
  27. use think\facade\Cache;
  28. use think\facade\Config;
  29. use think\facade\Route;
  30. use think\Model;
  31. /**
  32. * 商户管理员
  33. */
  34. class MerchantAdminRepository extends BaseRepository
  35. {
  36. const PASSWORD_TYPE_ADMIN = 1;
  37. const PASSWORD_TYPE_MERCHANT = 2;
  38. const PASSWORD_TYPE_SELF = 3;
  39. /**
  40. * MerchantAdminRepository constructor.
  41. * @param MerchantAdminDao $dao
  42. */
  43. public function __construct(MerchantAdminDao $dao)
  44. {
  45. $this->dao = $dao;
  46. }
  47. /**
  48. * 根据条件获取角色列表
  49. *
  50. * 本函数用于查询特定商家下的角色列表,支持分页和条件查询。它首先计算总记录数,然后获取指定页码和每页数量的角色数据。
  51. * 对查询结果进行处理,合并商家账号信息,并解析角色名称。最后,返回包含角色列表和总记录数的数组。
  52. *
  53. * @param int $merId 商家ID,用于限定查询的商家范围
  54. * @param array $where 查询条件数组,用于筛选角色
  55. * @param int $page 当前页码,用于分页查询
  56. * @param int $limit 每页记录数,用于分页查询
  57. * @return array 返回包含角色列表和总记录数的数组
  58. */
  59. public function getList($merId, array $where, $page, $limit)
  60. {
  61. // 根据商家ID和查询条件进行初步查询
  62. $query = $this->dao->search($merId, $where, 1);
  63. // 计算总记录数
  64. $count = $query->count();
  65. // 进行分页查询,并隐藏某些字段
  66. $list = $query->page($page, $limit)->hidden(['pwd', 'is_del'])->select();
  67. // 查询商家主账号
  68. $topAccount = $this->dao->merIdByAccount($merId);
  69. // 遍历角色列表,合并账号信息,解析角色名称
  70. foreach ($list as $k => $role) {
  71. if ($topAccount) {
  72. // 合并商家主账号和角色账号
  73. $list[$k]['account'] = $topAccount . '@' . $role['account'];
  74. }
  75. // 解析角色名称
  76. $list[$k]['rule_name'] = $role->roleNames();
  77. }
  78. // 返回角色列表和总记录数
  79. return compact('list', 'count');
  80. }
  81. /**
  82. * 创建或编辑管理员表单
  83. *
  84. * 该方法用于生成用于添加或编辑管理员的表单。根据$id$的存在与否决定是创建新管理员还是编辑已有的管理员。
  85. * 表单包括选择角色、输入管理员姓名、账号、电话等字段。如果$id$为空,则还包括输入密码和确认密码的字段。
  86. *
  87. * @param int $merId 商户ID,用于获取角色选项
  88. * @param int|null $id 管理员ID,如果为null则表示创建新管理员,否则为编辑现有管理员
  89. * @param array $formData 表单默认数据,用于预填充表单
  90. * @return Form 返回生成的表单对象
  91. */
  92. public function form(int $merId, ?int $id = null, array $formData = []): Form
  93. {
  94. $form = Elm::createForm(is_null($id) ? Route::buildUrl('merchantAdminCreate')->build() : Route::buildUrl('merchantAdminUpdate', ['id' => $id])->build());
  95. $rules = [
  96. Elm::select('roles', '身份:', [])->options(function () use ($merId) {
  97. $data = app()->make(RoleRepository::class)->getAllOptions($merId);
  98. $options = [];
  99. foreach ($data as $value => $label) {
  100. $options[] = compact('value', 'label');
  101. }
  102. return $options;
  103. })->placeholder('请选择身份')->multiple(true),
  104. Elm::input('real_name', '管理员姓名:')->placeholder('请输入管理员姓名'),
  105. Elm::input('account', '账号:')->placeholder('请输入账号')->required(),
  106. Elm::input('phone', ' 联系电话:')->placeholder('请输入联系电话'),
  107. ];
  108. if (!$id) {
  109. $rules[] = Elm::password('pwd', '密码:')->placeholder('请输入密码')->required();
  110. $rules[] = Elm::password('againPassword', '确认密码:')->placeholder('请输入确认密码')->required();
  111. }
  112. $rules[] = Elm::switches('status', '账号状态:', 1)->inactiveValue(0)->width(60)->activeValue(1)->inactiveText('正常')->activeText('停用');
  113. $form->setRule($rules);
  114. return $form->setTitle(is_null($id) ? '添加管理员' : '编辑管理员')->formData($formData);
  115. }
  116. /**
  117. * 更新表单数据的方法
  118. *
  119. * 本方法用于根据给定的商户ID和表单ID来更新表单数据。它首先通过ID获取现有的表单数据,
  120. * 然后使用这些数据来构建一个新的表单实例。此方法体现了对表单数据的更新操作,
  121. * 是业务逻辑中对数据修改的一个典型应用场景。
  122. *
  123. * @param int $merId 商户ID,用于指定表单所属的商户。
  124. * @param int $id 表单ID,用于唯一标识待更新的表单。
  125. * @return array|Form
  126. */
  127. public function updateForm(int $merId, int $id)
  128. {
  129. // 通过ID获取当前表单的数据,并转换为数组格式
  130. // 这里使用了链式调用,首先通过$this->dao->get($id)获取表单对象,然后调用toArray()方法将其转换为数组
  131. return $this->form($merId, $id, $this->dao->get($id)->toArray());
  132. }
  133. /**
  134. * 创建商家账户
  135. *
  136. * 本函数用于为指定的商家创建一个新的账户。它首先对密码进行加密处理,然后组装账户数据,
  137. * 最后调用创建账户的方法来实际创建账户。
  138. *
  139. * @param Merchant $merchant 商家对象,包含商家的相关信息。
  140. * @param string $account 账户名称。
  141. * @param string $pwd 商家账户的原始密码。
  142. * @return mixed 创建账户操作的结果,具体类型取决于create方法的返回。
  143. */
  144. public function createMerchantAccount(Merchant $merchant, $account, $pwd)
  145. {
  146. // 对密码进行加密处理
  147. $pwd = $this->passwordEncode($pwd);
  148. // 组装账户数据,包括加密后的密码、账户名、商家ID、商家名称、商家电话和初始等级
  149. $data = compact('pwd', 'account') + [
  150. 'mer_id' => $merchant->mer_id,
  151. 'real_name' => $merchant->real_name,
  152. 'phone' => $merchant->mer_phone,
  153. 'level' => 0
  154. ];
  155. // 调用创建账户的方法,传入组装好的数据
  156. return $this->create($data);
  157. }
  158. /**
  159. * 对密码进行加密处理
  160. *
  161. * 本函数使用BCrypt算法对密码进行加密。BCrypt是一种基于口令的加密算法,设计用于存储和验证用户密码。
  162. * 选择BCrypt算法是因为其在安全性和抗暴力破解方面的优势。
  163. *
  164. * @param string $password 需要加密的原始密码
  165. * @return string 返回加密后的密码hash值
  166. */
  167. public function passwordEncode($password)
  168. {
  169. return password_hash($password, PASSWORD_BCRYPT);
  170. }
  171. /**
  172. * 管理员登录方法
  173. *
  174. * 该方法用于处理管理员的登录逻辑。它首先触发一个登录前的事件,然后根据账户格式验证并查询管理员信息。
  175. * 如果账户格式不符合标准(不包含 '@' 符号),则认为是顶级管理员账户。否则,认为是商户管理员账户,并需要验证商户是否存在。
  176. * 如果管理员信息不存在或密码不匹配,将增加登录失败次数并抛出异常。此外,如果管理员账户被禁用,也会抛出异常。
  177. * 成功登录后,更新管理员的登录信息并触发一个登录成功的事件。
  178. *
  179. * @param string $account 管理员账户名
  180. * @param string $password 管理员密码
  181. * @return AdminModel|array|\think\db\false|Model
  182. * @throws ValidateException 如果登录失败或账户状态异常,抛出此异常
  183. */
  184. public function login(string $account, string $password)
  185. {
  186. // 触发登录前的事件,可以用于日志记录、验证码验证等
  187. event('admin.merLogin.before',compact('account', 'password'));
  188. // 分割账户名以获取账号和域名部分
  189. $accountInfo = explode('@', $account, 2);
  190. // 如果账户名不包含 '@',则认为是顶级管理员
  191. if (count($accountInfo) === 1) {
  192. $adminInfo = $this->dao->accountByTopAdmin($accountInfo[0]);
  193. } else {
  194. // 查询商户ID对应的管理员信息
  195. $merId = $this->dao->accountByMerchantId($accountInfo[0]);
  196. // 如果商户ID不存在,增加登录失败次数并抛出异常
  197. if (!$merId){
  198. $key = 'mer_login_failuree_'.$account;
  199. $numb = Cache::get($key) ?? 0;
  200. $numb++;
  201. Cache::set($key,$numb,15*60);
  202. throw new ValidateException('账号或密码错误');
  203. }
  204. // 根据管理员名称和商户ID查询管理员信息
  205. $adminInfo = $this->dao->accountByAdmin($accountInfo[1], $merId);
  206. }
  207. // 如果管理员信息不存在或密码不匹配,增加登录失败次数并抛出异常
  208. if (!$adminInfo || !password_verify($password, $adminInfo->pwd)){
  209. $key = 'mer_login_failuree_'.$account;
  210. $numb = Cache::get($key) ?? 0;
  211. $numb++;
  212. Cache::set($key,$numb,15*60);
  213. throw new ValidateException('账号或密码错误');
  214. }
  215. // 如果管理员账户被禁用,抛出异常
  216. if ($adminInfo['status'] != 1)
  217. throw new ValidateException('账号已关闭');
  218. // 获取商户信息
  219. /**
  220. * @var MerchantRepository $merchantRepository
  221. */
  222. $merchantRepository = app()->make(MerchantRepository::class);
  223. $merchant = $merchantRepository->get($adminInfo->mer_id);
  224. // 如果商户不存在或被禁用,抛出异常
  225. if (!$merchant)
  226. throw new ValidateException('商户不存在');
  227. if (!$merchant['status'])
  228. throw new ValidateException('商户已被锁定');
  229. // 更新管理员的登录信息
  230. $adminInfo->last_time = date('Y-m-d H:i:s');
  231. $adminInfo->last_ip = app('request')->ip();
  232. $adminInfo->login_count++;
  233. $adminInfo->save();
  234. // 触发登录成功的事件,可以用于日志记录、在线状态更新等
  235. event('admin.merLogin',compact('adminInfo'));
  236. // 返回登录成功的管理员信息
  237. return $adminInfo;
  238. }
  239. /**
  240. * 缓存令牌
  241. *
  242. * 本函数用于缓存给定的令牌及其过期时间。缓存机制可以是任何支持的缓存驱动,
  243. * 如文件缓存、数据库缓存、内存缓存等。缓存的目的是为了快速验证令牌的有效性,
  244. * 而不是每次请求都进行复杂的令牌生成逻辑。
  245. *
  246. * @param string $token 令牌字符串。这是由特定算法生成的唯一字符串,用于标识用户或会话。
  247. * @param int $exp 令牌的过期时间,以秒为单位。过期时间过后,令牌将不再有效。
  248. */
  249. public function cacheToken(string $token, int $exp)
  250. {
  251. // 构建缓存键名,并设置缓存值为当前时间加上过期时间,过期时间参数再次确保缓存的有效期。
  252. Cache::set('mer_' . $token, time() + $exp, $exp);
  253. }
  254. /**
  255. * 检查商家令牌的有效性
  256. *
  257. * 本函数用于验证传入的商家令牌是否有效。它首先检查令牌是否存在缓存中,
  258. * 如果不存在,则抛出一个表示令牌无效的异常。如果令牌存在缓存中,
  259. * 它将进一步检查令牌是否过期。如果令牌过期,则同样抛出一个表示令牌过期的异常。
  260. * 这样做的目的是为了确保只有有效的令牌才能用于授权和访问受保护的资源。
  261. *
  262. * @param string $token 商家令牌。这是一个用于标识和验证商家的唯一字符串。
  263. * @throws AuthException 如果令牌无效或过期,则抛出此异常。
  264. */
  265. public function checkToken(string $token)
  266. {
  267. // 检查缓存中是否存在指定的商家令牌
  268. $has = Cache::has('mer_' . $token);
  269. // 如果令牌不存在于缓存中,则抛出无效令牌异常
  270. if (!$has)
  271. throw new AuthException('无效的token');
  272. // 获取商家令牌的缓存时间
  273. $lastTime = Cache::get('mer_' . $token);
  274. // 检查令牌是否过期,如果过期则抛出异常
  275. if (($lastTime + (intval(Config::get('admin.token_valid_exp', 15))) * 60) < time())
  276. throw new AuthException('token 已过期,请重新登录');
  277. }
  278. /**
  279. * 更新商户令牌
  280. *
  281. * 本函数用于更新商户的令牌。令牌是用于验证商户身份的重要凭据,通过更新令牌,可以延长商户的登录状态,
  282. * 或者重新设置商户的访问权限。此操作依赖于缓存系统来存储令牌和其对应的过期时间。
  283. *
  284. * @param string $token 商户的令牌。该令牌是用于标识和验证商户身份的唯一字符串。
  285. */
  286. public function updateToken(string $token)
  287. {
  288. // 组合缓存键名,并设置缓存,缓存值为当前时间戳,过期时间为配置文件中定义的令牌有效时长(默认为15分钟)的60倍。
  289. // 这样做是为了确保令牌在设定的有效期内保持有效,过期后需要重新获取。
  290. Cache::set('mer_' . $token, time(), intval(Config::get('admin.token_valid_exp', 15)) * 60);
  291. }
  292. /**
  293. * 清除指定商户的令牌缓存
  294. *
  295. * 本函数用于从缓存系统中删除指定商户的令牌。这在商户令牌不再需要时,
  296. * 或者在令牌过期或被吊销时非常有用。通过清除令牌的缓存,可以确保
  297. * 令牌不会被意外或恶意使用,增强了系统的安全性。
  298. *
  299. * @param string $token 商户的令牌。此令牌用于唯一标识商户。
  300. */
  301. public function clearToken(string $token)
  302. {
  303. // 构建缓存键名,并删除该缓存项
  304. Cache::delete('mer_' . $token);
  305. }
  306. /**
  307. * 创建管理员令牌
  308. *
  309. * 本函数用于生成针对管理员的JWT令牌,该令牌用于管理员的身份验证。
  310. * 令牌的过期时间通过配置文件定义,默认为3小时。
  311. *
  312. * @param MerchantAdmin $admin 管理员对象,包含管理员信息。
  313. * @return array 返回包含令牌和过期时间的信息。
  314. */
  315. public function createToken(MerchantAdmin $admin)
  316. {
  317. // 实例化JWT令牌服务类
  318. $service = new JwtTokenService();
  319. // 从配置文件中获取管理员令牌的过期时间,默认为3小时
  320. $exp = intval(Config::get('admin.token_exp', 3));
  321. // 生成令牌,指定管理员ID和令牌过期时间
  322. $token = $service->createToken($admin->merchant_admin_id, 'mer', strtotime("+ {$exp}hour"));
  323. // 缓存令牌信息
  324. $this->cacheToken($token['token'], $token['out']);
  325. // 返回生成的令牌信息
  326. return $token;
  327. }
  328. /**
  329. * 验证验证码是否正确。
  330. * 该方法主要用于在非开发环境下验证用户输入的验证码是否与缓存中的验证码匹配。如果验证码不匹配或已过期,
  331. * 将抛出ValidateException异常。在开发环境下,验证码的验证逻辑被跳过,以方便开发。
  332. *
  333. * @param string $key 验证码的唯一标识键,用于拼接缓存键名。
  334. * @param string $code 用户输入的验证码内容。
  335. * @throws ValidateException 如果验证码过期或不正确,则抛出此异常。
  336. */
  337. public function checkCode(string $key, string $code)
  338. {
  339. // 如果不在开发模式下,则进行验证码的验证
  340. if (!env('DEVELOPMENT',false)){
  341. // 从缓存中获取存储的验证码,键名为'am_captcha'加上$key。
  342. $_code = Cache::get('am_captcha' . $key);
  343. // 如果缓存中没有找到验证码,表示验证码已过期,抛出异常。
  344. if (!$_code) {
  345. throw new ValidateException('验证码过期');
  346. }
  347. // 将存储的验证码和用户输入的验证码转换为小写后比较,不匹配则抛出异常。
  348. if (strtolower($_code) != strtolower($code)) {
  349. throw new ValidateException('验证码错误');
  350. }
  351. // 验证码验证通过后,删除缓存中的验证码。
  352. //删除code
  353. Cache::delete('am_captcha' . $key);
  354. }
  355. }
  356. /**
  357. * 创建登录验证码键
  358. *
  359. * 本函数用于生成一个唯一的登录验证码键,并将该验证码与键关联起来存储在缓存中。
  360. * 验证码键的生成结合了微秒时间和随机数,以确保唯一性。
  361. * 验证码在缓存中的有效期通过配置文件设定,以分钟为单位。
  362. *
  363. * @param string $code 登录验证码
  364. * @return string 生成的验证码键
  365. */
  366. public function createLoginKey(string $code)
  367. {
  368. // 生成一个唯一的验证码键,基于当前微秒时间戳和随机数
  369. $key = uniqid(microtime(true), true);
  370. // 将验证码与键关联,存储到缓存中,并设定过期时间
  371. // 缓存有效期通过配置文件获取,默认为5分钟
  372. Cache::set('am_captcha' . $key, $code, Config::get('admin.captcha_exp', 5) * 60);
  373. // 返回生成的验证码键
  374. return $key;
  375. }
  376. /**
  377. * 创建密码修改表单
  378. *
  379. * 本函数用于生成一个用于修改密码的表单。表单的行为根据用户类型的不同而有所不同。
  380. * 可以是系统管理员修改密码,也可以是商家管理员自己修改密码。
  381. *
  382. * @param int $id 用户ID,用于标识特定用户的密码修改操作。
  383. * @param int $userType 用户类型,定义了密码修改的操作范围。2表示商家管理员,其他值表示系统管理员。
  384. * @return Form|string
  385. */
  386. public function passwordForm(int $id, $userType = 2)
  387. {
  388. // 根据用户类型确定表单提交的行动
  389. $action = 'merchantAdminPassword';
  390. if ($userType == self::PASSWORD_TYPE_ADMIN) {
  391. $action = 'systemMerchantAdminPassword';
  392. } else if ($userType == self::PASSWORD_TYPE_SELF) {
  393. $action = 'merchantAdminEditPassword';
  394. }
  395. // 创建表单,包括密码和确认密码两个字段
  396. $form = Elm::createForm(Route::buildUrl($action, $userType == self::PASSWORD_TYPE_SELF ? [] : compact('id'))->build(), [
  397. $rules[] = Elm::password('pwd', '密码:')->placeholder('请输入密码')->required(),
  398. $rules[] = Elm::password('againPassword', '确认密码:')->placeholder('请输入确认密码')->required(),
  399. ]);
  400. // 设置表单标题为“修改密码”
  401. return $form->setTitle('修改密码');
  402. }
  403. /**
  404. * 创建并返回编辑管理员信息的表单
  405. *
  406. * 该方法通过Element UI的表单构建器生成一个用于编辑管理员信息的表单。表单包含了管理员姓名和联系电话两个必填字段。
  407. * 表单的提交地址是通过路由生成的管理员编辑地址。使用该表单可以方便地收集和验证管理员的更新信息。
  408. *
  409. * @param array $formData 管理员当前的信息数据,用于填充表单的默认值。
  410. * @return Form|\think\response\View
  411. */
  412. public function editForm(array $formData)
  413. {
  414. // 通过路由生成编辑管理员信息的URL,用于表单的提交动作
  415. $form = Elm::createForm(Route::buildUrl('merchantAdminEdit')->build());
  416. // 设置表单的验证规则,包括管理员姓名和联系电话两个字段
  417. $form->setRule([
  418. // 管理员姓名字段,必填,用于输入管理员的姓名
  419. Elm::input('merchant_admin_id', '管理员ID:')->disabled(true),
  420. Elm::input('real_name', '管理员姓名:')->placeholder('请输入管理员姓名')->required(),
  421. // 联系电话字段,用于输入管理员的联系电话
  422. Elm::input('phone', '联系电话:')->placeholder('请输入联系电话')
  423. ]);
  424. // 设置表单的标题为“修改信息”,并加载传入的管理员当前信息数据,用于表单显示
  425. return $form->setTitle('修改信息')->formData($formData);
  426. }
  427. /**
  428. * 更新数据库中指定ID的记录。
  429. *
  430. * 本函数用于根据提供的ID和数据数组更新数据库中的相应记录。特别地,如果数据数组中包含'roles'字段,
  431. * 该字段将被转换为以逗号分隔的字符串格式,这是因为数据库中可能需要以这种格式存储角色数据。
  432. *
  433. * @param int $id 要更新的记录的ID。
  434. * @param array $data 包含要更新到数据库的字段和值的数据数组。
  435. * @return mixed 返回DAO层执行更新操作的结果。具体类型取决于DAO层的实现。
  436. */
  437. public function update(int $id, array $data)
  438. {
  439. // 如果$data数组中包含'roles'键,将其值转换为逗号分隔的字符串
  440. if (isset($data['roles'])) {
  441. $data['roles'] = implode(',', $data['roles']);
  442. }
  443. // 调用DAO层的update方法执行更新操作,并返回结果
  444. return $this->dao->update($id, $data);
  445. }
  446. }