request->langset(); $lang = preg_match('/^([a-zA-Z\-_]{2,10})$/i', $lang) ? $lang : 'zh-cn'; $this->model = new \app\admin\model\split\Ticket(); $this->view->assign('ticketTypeList', $this->model->getTicketTypeList()); $this->view->assign('numberTypeList', $this->model->getNumberTypeList()); $this->view->assign('statusList', $this->model->getStatusList()); $this->view->assign('splitLinkList', $this->buildSplitLinkList()); $this->assignconfig('ticketTypeList', $this->model->getTicketTypeList()); $this->assignconfig('numberTypeList', $this->model->getNumberTypeList()); $this->assignconfig('statusList', $this->model->getStatusList()); $this->assignconfig([ 'syncConfirmMsg' => __('Sync confirm'), 'syncBackgroundStartedMsg' => __('Sync background started'), 'syncInProgressMsg' => __('Sync in progress'), 'syncTicketStartedMsg' => __('Sync ticket started'), ]); $this->setupPatchFrontend(); } /** * 加载工单语言包(优先 patches) */ protected function loadlang($name): void { $lang = $this->request->langset(); $lang = preg_match('/^([a-zA-Z\-_]{2,10})$/i', $lang) ? $lang : 'zh-cn'; $name = Loader::parseName($name); $name = preg_match('/^([a-zA-Z0-9_\.\/]+)$/i', $name) ? $name : 'index'; $files = [ APP_PATH . 'admin/lang/' . $lang . '/' . str_replace('.', '/', $name) . '.php', ROOT_PATH . 'patches/application/admin/lang/' . $lang . '/split/ticket.php', ]; $loaded = false; foreach ($files as $file) { if (is_file($file)) { Lang::load($file); $loaded = true; } } if (!$loaded) { parent::loadlang($name); } } /** * 未部署 JS 时指向 script 接口 */ private function setupPatchFrontend(): void { $patchJs = ROOT_PATH . 'patches/public/assets/js/backend/split/ticket.js'; $publicJs = ROOT_PATH . 'public/assets/js/backend/split/ticket.js'; $usePatchJs = is_file($patchJs) && ( !is_file($publicJs) || filemtime($patchJs) >= filemtime($publicJs) ); if (!$usePatchJs) { return; } $cfg = is_array($this->view->config ?? null) ? $this->view->config : []; $version = (string) \think\Config::get('site.version'); $scriptUrl = (string) url('split.ticket/script', ['v' => $version], false, true); if (strpos($scriptUrl, '?') === false) { $scriptUrl .= '?v=' . $version; } if (strpos($scriptUrl, '://') === false) { $scriptUrl = $this->request->domain() . $scriptUrl; } $cfg['jsname'] = $scriptUrl; $this->view->assign('config', $cfg); $this->view->config = $cfg; } /** * @return array> */ private function buildSplitLinkList(): array { $query = (new LinkModel())->where('status', 'normal')->order('id', 'desc'); if ($this->dataLimit) { $adminIds = $this->getDataLimitAdminIds(); if (is_array($adminIds)) { $query->where('admin_id', 'in', $adminIds); } } $list = []; foreach ($query->select() as $row) { $code = (string) $row['link_code']; $desc = (string) $row['description']; $label = $desc !== '' ? $code . ' - ' . $desc : $code; $list[] = [ 'id' => (string) $row['id'], 'label' => $label, ]; } return $list; } private function fetchPatch(string $template): string { $patchFile = ROOT_PATH . self::PATCH_VIEW_DIR . $template . '.html'; $appFile = APP_PATH . 'admin/view/split/ticket/' . $template . '.html'; if (is_file($patchFile)) { $file = $patchFile; } elseif (is_file($appFile)) { $file = $appFile; } else { $this->error('模板文件不存在'); } return (string) $this->view->fetch($file); } /** * @return string|Json * @throws DbException * @throws \think\Exception */ public function index() { $this->request->filter(['strip_tags', 'trim']); if (false === $this->request->isAjax()) { return $this->fetchPatch('index'); } if ($this->request->request('keyField')) { return $this->selectpage(); } [$where, $sort, $order, $offset, $limit] = $this->buildparams(); $ticketTable = $this->model->getTable(); $clickSub = TicketModel::buildClickCountSubQuery(); // 勿 with(splitLink):eagerlyType=0 为 JOIN 预载入,会重写 field 导致子查询 SQL 语法错误 $list = $this->model ->field($ticketTable . '.*,' . $clickSub) ->where($where) ->order($sort, $order) ->paginate($limit); $result = ['total' => $list->total(), 'rows' => $list->items()]; return json($result); } /** * 同步统计字段仅由接口写入,禁止表单提交篡改 * * @param array $params * @return array */ protected function preExcludeFields($params): array { $params = parent::preExcludeFields($params); unset( $params['complete_count'], $params['inbound_count'], $params['speed_per_hour'], $params['number_count'], $params['number_offline_count'], $params['number_banned_count'], $params['online_count'], $params['sync_status'], $params['sync_time'], $params['sync_message'], $params['sync_fail_count'], $params['speed_snapshot_count'], $params['speed_snapshot_time'], $params['click_count'] ); return $params; } /** * 手动同步选中工单 */ public function sync(): void { if (!$this->request->isPost()) { $this->error(__('Invalid parameters')); } $ids = $this->request->post('ids', ''); if ($ids === '') { $this->error(__('Parameter %s can not be empty', 'ids')); } $adminIds = $this->getDataLimitAdminIds(); $pk = $this->model->getPk(); $list = $this->model->where($pk, 'in', $ids)->select(); if (!$list || count($list) === 0) { $this->error(__('No Results were found')); } SplitTicketSyncLogger::log('web', 'manual sync request', [ 'ids' => $ids, 'appDebug' => SplitTicketSyncLogger::isEnabled(), ]); $service = new SplitTicketSyncService(); $ok = 0; $fail = 0; $messages = []; foreach ($list as $row) { if (is_array($adminIds) && !in_array((int) $row[$this->dataLimitField], $adminIds, true)) { $fail++; $messages[] = '#' . $row['id'] . ': 无权限'; continue; } $result = $service->syncOne((int) $row['id'], true); if ($result['success']) { $ok++; } else { $fail++; $messages[] = '#' . $row['id'] . ': ' . ($result['message'] ?? '失败'); } } $summary = sprintf('成功 %d 条,失败 %d 条', $ok, $fail); if ($fail > 0) { $summary .= ';' . implode(';', array_slice($messages, 0, 5)); } $this->success($summary); } /** * 批量更新:工单状态变更时联动号码 * * @param string $ids */ public function multi($ids = '') { if (!$this->request->isPost()) { $this->error(__('Invalid parameters')); } $ids = $ids ?: $this->request->post('ids', ''); if ($ids === '') { $this->error(__('Parameter %s can not be empty', 'ids')); } $params = $this->request->post('params', ''); parse_str((string) $params, $values); $ruleService = new SplitTicketRuleService(); if (isset($values['status'])) { $pk = $this->model->getPk(); $adminIds = $this->getDataLimitAdminIds(); $query = $this->model->where($pk, 'in', $ids); if (is_array($adminIds)) { $query->where($this->dataLimitField, 'in', $adminIds); } $rows = $query->select(); if (!$rows || count($rows) === 0) { $this->error(__('No Results were found')); } $count = 0; Db::startTrans(); try { foreach ($rows as $item) { $count += $item->allowField(true)->isUpdate(true)->save($values); } Db::commit(); } catch (ValidateException $e) { Db::rollback(); $this->error($e->getMessage()); } catch (PDOException|Exception $e) { Db::rollback(); $this->error($e->getMessage()); } foreach ($rows as $row) { $fresh = $this->model->get($row['id']); if ($fresh) { $ruleService->syncNumbersWithTicketStatus($fresh); } } if ($count > 0) { $this->success(); } $this->error(__('No rows were updated')); } parent::multi($ids); } /** * @return string * @throws DbException */ public function add() { if (false === $this->request->isPost()) { return $this->fetchPatch('add'); } $params = $this->request->post('row/a', []); if ($params === []) { $this->error(__('Parameter %s can not be empty', '')); } $params = $this->preExcludeFields($params); if (($params['number_type'] ?? '') !== 'custom') { $params['number_type_custom'] = ''; } if ($this->dataLimit && $this->dataLimitFieldAutoFill) { $params[$this->dataLimitField] = $this->auth->id; } $result = false; Db::startTrans(); try { if ($this->modelValidate) { $name = str_replace('\\model\\', '\\validate\\', get_class($this->model)); $validate = $this->modelSceneValidate ? $name . '.add' : $name; $this->model->validateFailException()->validate($validate); } $result = $this->model->allowField(true)->save($params); Db::commit(); } catch (ValidateException $e) { Db::rollback(); $this->error($e->getMessage()); } catch (PDOException|Exception $e) { Db::rollback(); $this->error($e->getMessage()); } if ($result === false) { $this->error(__('No rows were inserted')); } $this->success('', null, ['id' => (int) $this->model->id]); } /** * @param string|null $ids * @return string * @throws DbException */ public function edit($ids = null) { $row = $this->model->get($ids); if (!$row) { $this->error(__('No Results were found')); } $adminIds = $this->getDataLimitAdminIds(); if (is_array($adminIds) && !in_array((int) $row[$this->dataLimitField], $adminIds, true)) { $this->error(__('You have no permission')); } if (false === $this->request->isPost()) { $rowData = $row->toArray(); $rowData['start_time'] = $rowData['start_time_text'] ?? ''; $rowData['end_time'] = $rowData['end_time_text'] ?? ''; $this->view->assign('row', $rowData); return $this->fetchPatch('edit'); } $params = $this->request->post('row/a', []); if ($params === []) { $this->error(__('Parameter %s can not be empty', '')); } $params = $this->preExcludeFields($params); if (($params['number_type'] ?? '') !== 'custom') { $params['number_type_custom'] = ''; } $oldStatus = (string) ($row['status'] ?? 'hidden'); $result = false; Db::startTrans(); try { if ($this->modelValidate) { $name = str_replace('\\model\\', '\\validate\\', get_class($this->model)); $validate = $this->modelSceneValidate ? $name . '.edit' : $name; $row->validateFailException()->validate($validate); } $result = $row->allowField(true)->save($params); Db::commit(); } catch (ValidateException $e) { Db::rollback(); $this->error($e->getMessage()); } catch (PDOException|Exception $e) { Db::rollback(); $this->error($e->getMessage()); } if ($result === false) { $this->error(__('No rows were updated')); } if (isset($params['status']) && (string) $params['status'] !== $oldStatus) { $fresh = $this->model->get($row['id']); if ($fresh) { (new SplitTicketRuleService())->syncNumbersWithTicketStatus($fresh); } } $this->success(); } /** * 输出后台 JS(patches 未部署到 public 时) */ public function script(): void { $jsFile = ROOT_PATH . 'patches/public/assets/js/backend/split/ticket.js'; if (!is_file($jsFile)) { $jsFile = ROOT_PATH . 'public/assets/js/backend/split/ticket.js'; } if (!is_file($jsFile)) { $this->error('脚本文件不存在'); } $content = file_get_contents($jsFile); if ($content === false) { $this->error('读取脚本失败'); } $response = response($content, 200, [ 'Content-Type' => 'application/javascript; charset=utf-8', 'Cache-Control' => 'public, max-age=3600', ]); throw new \think\exception\HttpResponseException($response); } }