StoreServiceRepository.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  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\store\service;
  12. use app\common\dao\store\service\StoreServiceDao;
  13. use app\common\model\store\service\StoreService;
  14. use app\common\repositories\BaseRepository;
  15. use crmeb\exceptions\AuthException;
  16. use crmeb\services\JwtTokenService;
  17. use FormBuilder\Exception\FormBuilderException;
  18. use FormBuilder\Factory\Elm;
  19. use FormBuilder\Factory\Iview;
  20. use FormBuilder\Form;
  21. use think\db\exception\DataNotFoundException;
  22. use think\db\exception\DbException;
  23. use think\db\exception\ModelNotFoundException;
  24. use think\exception\ValidateException;
  25. use think\facade\Cache;
  26. use think\facade\Config;
  27. use think\facade\Route;
  28. /**
  29. * 客服
  30. */
  31. class StoreServiceRepository extends BaseRepository
  32. {
  33. /**
  34. * StoreServiceRepository constructor.
  35. * @param StoreServiceDao $dao
  36. */
  37. public function __construct(StoreServiceDao $dao)
  38. {
  39. $this->dao = $dao;
  40. }
  41. /**
  42. * 根据条件获取列表数据
  43. *
  44. * 本函数用于根据给定的条件数组 $where,从数据库中检索满足条件的数据列表。
  45. * 它支持分页查询,每页的数据数量由 $limit 指定,查询的页码由 $page 指定。
  46. * 查询结果包括满足条件的数据总数 $count 和实际查询到的数据列表 $list。
  47. *
  48. * @param array $where 查询条件,以数组形式传递,键值对表示字段名和字段值。
  49. * @param int $page 查询的页码,用于实现分页查询。
  50. * @param int $limit 每页显示的数据数量。
  51. * @return array 返回包含 'count' 和 'list' 两个元素的数组,'count' 表示满足条件的数据总数,'list' 表示查询到的数据列表。
  52. */
  53. public function getList(array $where, $page, $limit)
  54. {
  55. // 构建查询语句,根据 $where 条件进行搜索,并包含 'user' 关联数据,但只选取特定字段。
  56. $query = $this->dao->search($where)
  57. ->with(['user' => function ($query) {
  58. // 在 'user' 关联数据中,只选取 'nickname', 'avatar', 'uid', 'cancel_time' 四个字段。
  59. $query->field('nickname,avatar,uid,cancel_time');
  60. }])
  61. ->order('sort DESC,create_time DESC'); // 按 'sort' 和 'create_time' 降序排序。
  62. // 计算满足条件的数据总数。
  63. $count = $query->count();
  64. // 进行分页查询,获取当前页的数据列表。
  65. $list = $query->page($page, $limit)->select();
  66. // 将数据总数和数据列表一起返回。
  67. return compact('count', 'list');
  68. }
  69. /**
  70. * 创建客服表单
  71. * 该方法用于生成添加或编辑客服的表单界面。根据$merId参数的值,决定是创建新的客服还是编辑已有的客服。
  72. * 如果是编辑客服,表单中会包含开关控件来配置客服的权限和状态;如果是新建客服,密码和确认密码字段将是必填的。
  73. *
  74. * @param string $merId 商户ID,如果存在,则表示编辑商户客服;如果不存在,则表示添加管理员客服。
  75. * @param bool $isUpdate 表示是否为更新操作,默认为false,即添加操作。
  76. * @return string 返回生成的表单HTML代码。
  77. */
  78. public function form($merId, $isUpdate = false)
  79. {
  80. // 创建密码字段用于输入客服密码
  81. $pwd = Elm::password('pwd', '客服密码:');
  82. // 创建确认密码字段
  83. $confirm_pwd = Elm::password('confirm_pwd', '确认密码:');
  84. // 如果不是更新操作,密码字段必须填写
  85. if (!$isUpdate) {
  86. $pwd->required();
  87. $confirm_pwd->required();
  88. }
  89. // 初始化管理员权限规则数组
  90. $adminRule = $filed = [];
  91. // 如果提供了商户ID,生成管理员权限配置的开关控件
  92. if($merId){
  93. $adminRule = [
  94. Elm::switches('customer', '订单管理:', 1)->activeValue(1)->inactiveValue(0)->inactiveText('关')
  95. ->activeText('开')->col(12),
  96. // 商品管理开关
  97. Elm::switches('is_goods', '商品管理:', 1)->activeValue(1)->inactiveValue(0)->inactiveText('关')->activeText('开')->col(12),
  98. // 开启核销开关
  99. Elm::switches('is_verify', '开启核销:', 1)->activeValue(1)->inactiveValue(0)->inactiveText('关')->activeText('开'),
  100. // 订单通知开关,开启时需要输入通知电话
  101. ];
  102. }
  103. // 订单管理开关
  104. $adminRule[] = Elm::switches('notify', '订单通知:', 1)->activeValue(1)->inactiveValue(0)->inactiveText('关')->activeText('开')->control([
  105. [
  106. 'value' => 1,
  107. 'rule' => [
  108. Elm::input('phone', '通知电话:')
  109. ]
  110. ]
  111. ]);
  112. // 定义管理员权限字段的规则
  113. $filed = [
  114. "value" => 1,
  115. "rule" => [
  116. "customer","is_goods","is_verify","notify"
  117. ]
  118. ];
  119. // 添加排序字段,允许输入0到99999之间的整数
  120. $adminRule[] = Elm::number('sort', '排序:', 0)->precision(0)->max(99999);
  121. // 根据是否有商户ID,确定配置的前缀,用于生成用户列表和上传图片的URL
  122. $prefix = $merId ? config('admin.merchant_prefix') : config('admin.admin_prefix');
  123. // 生成并返回表单HTML代码
  124. return Elm::createForm(Route::buildUrl('merchantServiceCreate')->build(), array_merge([
  125. // 用户选择框,从用户列表中选择用户
  126. Elm::frameImage('uid', '用户:', '/' . $prefix . '/setting/userList?field=uid&type=1')->prop('srcKey', 'src')->width('1000px')->height('600px')->appendValidate(Iview::validateObject()->message('请选择用户')->required())->icon('el-icon-camera')->modal(['modal' => false]),
  127. // 头像上传控件
  128. Elm::frameImage('avatar', '客服头像:', '/' . $prefix . '/setting/uploadPicture?field=avatar&type=1')->width('1000px')->height('600px')->props(['footer' => false])->icon('el-icon-camera')->modal(['modal' => false]),
  129. // 昵称输入框
  130. Elm::input('nickname', '客服昵称:')->placeholder('请输入客服昵称')->required(),
  131. // 账号输入框
  132. Elm::input('account', '客服账号:')->placeholder('请输入客服账号')->required(),
  133. // 密码输入框
  134. $pwd,
  135. // 确认密码输入框
  136. $confirm_pwd,
  137. // 账号状态开关,开启表示账号可用
  138. Elm::switches('is_open', '账号状态:', 1)->activeValue(1)->inactiveValue(0)->inactiveText('关')->activeText('开')->col(12)->control([$filed]),
  139. // 客服状态开关,开启表示客服在线
  140. Elm::switches('status', '客服状态:', 1)->activeValue(1)->inactiveValue(0)->inactiveText('关')->activeText('开')->col(12),
  141. ], $adminRule))->setTitle('添加客服');
  142. }
  143. /**
  144. * 更新表单信息
  145. * 该方法用于根据给定的ID获取表单数据,并准备相应的数据以用于表单编辑界面。
  146. * @param int $id 表单ID,用于查询特定的表单数据。
  147. * @return mixed 返回一个用于编辑表单的视图对象,该对象已配置好相关的表单数据和操作URL。
  148. */
  149. public function updateForm($id)
  150. {
  151. // 根据$id获取表单数据,同时只查询用户的avatar和uid字段
  152. $service = $this->dao->getWith($id, ['user' => function ($query) {
  153. $query->field('avatar,uid');
  154. }])->toArray();
  155. // 如果获取到用户信息
  156. if($service['user'] ?? null){
  157. // 构建一个新的uid字段,包含用户的id和头像信息
  158. $service['uid'] = ['id' => $service['uid'], 'src' => $service['user']['avatar'] ?: $service['avatar']];
  159. }else{
  160. // 如果没有用户信息,移除uid字段
  161. unset($service['uid']);
  162. }
  163. // 移除不用于表单编辑的字段
  164. unset($service['user'], $service['pwd']);
  165. // 返回一个配置好表单数据、标题和操作URL的表单视图对象
  166. return $this->form($service['mer_id'], true)
  167. ->formData($service)
  168. ->setTitle('编辑客服')
  169. ->setAction(Route::buildUrl('merchantServiceUpdate', compact('id'))->build());
  170. }
  171. /**
  172. * 根据商家ID和用户ID获取聊天服务对象
  173. *
  174. * 本函数旨在根据提供的商家ID和用户ID,获取与之相关的聊天服务信息。
  175. * 如果用户ID提供且有效,将尝试根据最后的服务记录来获取服务对象。
  176. * 如果没有有效的最后服务记录,或者用户ID未提供,则随机获取一个可用的服务对象。
  177. *
  178. * @param string $merId 商家ID,用于确定聊天服务的范围
  179. * @param int $uid 用户ID,可选,用于获取用户特定的服务记录
  180. * @return object|null 返回聊天服务对象,如果无法获取则返回null
  181. */
  182. public function getChatService($merId, $uid = 0)
  183. {
  184. // 默认服务对象为空
  185. $service = null;
  186. // 如果用户ID提供,尝试获取最后的服务记录
  187. if ($uid) {
  188. // 实例化存储服务日志的仓库
  189. $logRepository = app()->make(StoreServiceLogRepository::class);
  190. // 获取指定商家和用户最后的服务ID
  191. $lastServiceId = $logRepository->getLastServiceId($merId, $uid);
  192. }
  193. // 如果存在有效的最后服务ID,尝试获取对应的服务对象
  194. if (isset($lastServiceId) && $lastServiceId) {
  195. $service = $this->getValidServiceInfo($lastServiceId);
  196. }
  197. // 如果已经获取到服务对象,则直接返回
  198. if ($service) return $service;
  199. // 如果没有获取到服务对象,尝试随机获取一个服务对象
  200. $service = $this->dao->getRandService($merId);
  201. // 如果随机获取成功,则返回服务对象
  202. if ($service) return $service;
  203. }
  204. /**
  205. * 根据用户ID获取服务列表
  206. * 此函数用于查询与特定用户相关联的服务列表。它支持过滤条件和系统服务的排序方向。
  207. *
  208. * @param int $uid 用户ID,用于查询与该用户相关联的服务。
  209. * @param array $where 查询过滤条件,允许通过数组传递额外的过滤条件。
  210. * @param int $is_sys 系统服务标志,用于确定服务列表是按升序还是降序排列系统服务。
  211. * 1 表示升序,非1表示降序。
  212. * @return array 返回一个经过处理的服务列表数组,每个服务包括商户信息。
  213. */
  214. public function getServices($uid, array $where = [],$is_sys = 1)
  215. {
  216. // 添加用户ID到查询条件
  217. $where['uid'] = $uid;
  218. // 执行查询,带条件和排序,并隐藏密码字段
  219. $list = $this->search($where)
  220. ->with(['merchant' => function ($query) {
  221. // 仅加载商户的特定字段
  222. $query->field('mer_id,mer_avatar,mer_name,status');
  223. }])
  224. ->where('mer_id', $is_sys ? '=' : '>',0)
  225. ->order('mer_id '. ($is_sys ? 'ASC' : 'DESC'))
  226. ->select()
  227. ->hidden(['pwd'])
  228. ->toArray();
  229. // 获取系统配置,用于填充默认商户信息
  230. $config = systemConfig(['site_logo', 'site_name']);
  231. // 遍历服务列表,为系统服务填充默认商户信息
  232. foreach ($list as &$item){
  233. if ($item['mer_id'] == 0 || !$item['merchant'] || $item['merchant']['status'] == 0) {
  234. // 系统服务使用系统配置的Logo和名称
  235. $item['merchant'] = [
  236. 'mer_avatar' => $config['site_logo'],
  237. 'mer_name' => $config['site_name'],
  238. 'mer_id' => 0,
  239. ];
  240. $item['mer_id'] = 0;
  241. }
  242. }
  243. unset($item); // 断开引用,避免潜在的引用问题
  244. // 返回处理后的服务列表
  245. return $list;
  246. }
  247. /**
  248. * 创建服务令牌
  249. *
  250. * 本函数用于生成针对管理员服务的JWT令牌。令牌用于在一段时间内验证请求的合法性。
  251. * 它通过JwtTokenService创建令牌,并将令牌及其过期时间存储到缓存中。
  252. *
  253. * @param StoreService $admin 管理员服务对象,用于获取服务ID,该ID是生成令牌的标识之一。
  254. * @return array 返回包含令牌和过期时间的数组。
  255. */
  256. public function createToken(StoreService $admin)
  257. {
  258. // 实例化JWT令牌服务类
  259. $service = new JwtTokenService();
  260. // 从配置中获取令牌的过期时间,默认为3小时,转换为整型
  261. $exp = intval(Config::get('admin.token_exp', 3));
  262. // 使用服务ID、令牌类型和服务过期时间创建令牌
  263. $token = $service->createToken($admin->service_id, 'service', strtotime("+ {$exp}hour"));
  264. // 将生成的令牌及其过期时间存储到缓存中
  265. $this->cacheToken($token['token'], $token['out']);
  266. // 返回生成的令牌信息
  267. return $token;
  268. }
  269. /**
  270. * 缓存令牌
  271. *
  272. * 本函数用于缓存给定的令牌及其过期时间。缓存的目的是为了提高访问效率,
  273. * 避免频繁的数据库查询或计算。令牌通常用于认证或访问控制等场景。
  274. *
  275. * @param string $token 令牌字符串。这是一个唯一标识,用于在缓存中查找或标识缓存项。
  276. * @param int $exp 令牌的过期时间,以秒为单位。这个时间从当前时间开始计算。
  277. */
  278. public function cacheToken(string $token, int $exp)
  279. {
  280. // 构建缓存键名,并设置缓存值为当前时间加上过期时间,过期时间参数再次强调了缓存的持续时间。
  281. Cache::set('service_' . $token, time() + $exp, $exp);
  282. }
  283. /**
  284. * 检查令牌的有效性
  285. *
  286. * 本函数用于验证传入的令牌是否有效,有效意味着该令牌曾在指定时间内被使用过。
  287. * 它首先检查令牌是否存在于缓存中,如果不存在,则抛出一个授权异常,指出令牌无效。
  288. * 接着,它获取令牌的最后使用时间,并计算令牌自上次使用以来是否已过期。
  289. * 如果令牌过期,同样抛出一个授权异常,指出令牌已过期。
  290. * 这样做的目的是为了确保每个请求的合法性,防止未授权的访问。
  291. *
  292. * @param string $token 待验证的令牌
  293. * @throws AuthException 如果令牌无效或已过期
  294. */
  295. public function checkToken(string $token)
  296. {
  297. // 检查缓存中是否存在该令牌
  298. $has = Cache::has('service_' . $token);
  299. // 如果令牌不存在于缓存中,则抛出无效令牌异常
  300. if (!$has)
  301. throw new AuthException('无效的token');
  302. // 获取令牌的最后使用时间
  303. $lastTime = Cache::get('service_' . $token);
  304. // 检查令牌是否过期,如果过期则抛出异常
  305. if (($lastTime + (intval(Config::get('admin.token_valid_exp', 15))) * 60) < time())
  306. throw new AuthException('token 已过期');
  307. }
  308. /**
  309. * 更新令牌的缓存时间
  310. *
  311. * 本函数用于更新特定令牌的缓存时间,确保令牌在一段时间内保持有效。
  312. * 它通过计算配置文件中指定的令牌有效时长(默认为15分钟),并更新缓存来实现。
  313. * 如果令牌在缓存中超时,用户需要重新获取令牌以进行操作。
  314. *
  315. * @param string $token 需要更新缓存时间的令牌字符串
  316. */
  317. public function updateToken(string $token)
  318. {
  319. // 根据配置文件中设定的令牌有效时长(默认15分钟),更新令牌的缓存时间
  320. Cache::set('service_' . $token, time(), intval(Config::get('admin.token_valid_exp', 15)) * 60);
  321. }
  322. /**
  323. * 清除指定令牌的缓存。
  324. *
  325. * 本函数用于从缓存系统中删除特定标识符的令牌,以实现令牌的有效 令牌通常用于认证或访问控制等场景,因此,及时清除无效的期管理或当令牌不再需要时令牌对于维护系统安全至关重要。
  326. *确保其被安全删除。
  327. *
  328. * @param string $token 需要清除的令牌字符串。
  329. */
  330. public function clearToken(string $token)
  331. {
  332. // 根据令牌生成缓存键名,并删除该缓存项。
  333. Cache::delete('service_' . $token);
  334. }
  335. /**
  336. * 检测验证码
  337. * @param string $key key
  338. * @param string $code 验证码
  339. * @author 张先生
  340. * @date 2020-03-26
  341. */
  342. public function checkCode(string $key, string $code)
  343. {
  344. $_code = Cache::get('ser_captcha' . $key);
  345. if (!$_code) {
  346. throw new ValidateException('验证码过期');
  347. }
  348. if (strtolower($_code) != strtolower($code)) {
  349. throw new ValidateException('验证码错误');
  350. }
  351. //删除code
  352. Cache::delete('ser_captcha' . $key);
  353. }
  354. /**
  355. * 创建登录验证码键
  356. *
  357. * 本函数用于生成一个唯一的登录验证码键,并将该验证码与键关联起来存储在缓存中。
  358. * 验证码键的生成结合了微秒时间和随机数,以确保唯一性。
  359. * 验证码在缓存中的有效期通过配置文件定义,以分钟为单位。
  360. *
  361. * @param string $code 登录验证码
  362. * @return string 生成的验证码键
  363. */
  364. public function createLoginKey(string $code)
  365. {
  366. // 生成一个唯一的验证码键,基于当前微秒时间戳和随机数
  367. $key = uniqid(microtime(true), true);
  368. // 将验证码与键关联,存储到缓存中,并设定过期时间
  369. // 缓存有效期通过配置文件获取,默认为5分钟
  370. Cache::set('ser_captcha' . $key, $code, Config::get('admin.captcha_exp', 5) * 60);
  371. // 返回生成的验证码键
  372. return $key;
  373. }
  374. }