Files

453 lines
15 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\admin\controller\split;
use app\admin\model\split\Link as LinkModel;
use app\admin\model\split\Ticket as TicketModel;
use app\common\controller\Backend;
use app\common\service\SplitTicketRuleService;
use app\common\service\SplitTicketSyncLogger;
use app\common\service\SplitTicketSyncService;
use think\Db;
use think\Lang;
use think\Loader;
use think\Exception;
use think\exception\DbException;
use think\exception\PDOException;
use think\exception\ValidateException;
use think\response\Json;
/**
* 工单管理
*
* @icon fa fa-ticket
* @remark 分流工单管理,关联分流链接
*/
class Ticket extends Backend
{
/** @var \app\admin\model\split\Ticket */
protected $model = null;
protected $searchFields = 'ticket_name,ticket_url,ticket_total';
protected $dataLimit = 'personal';
protected $modelValidate = true;
protected $modelSceneValidate = true;
/** @var string[] 无需鉴权的方法 */
protected $noNeedRight = ['script'];
/** @var string patches 视图目录 */
private const PATCH_VIEW_DIR = 'patches/application/admin/view/split/ticket/';
public function _initialize()
{
parent::_initialize();
$lang = $this->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');
// 使用站内相对路径,避免生产环境 domain() 与反向代理/HTTPS 不一致导致 JS 跨域丢 Cookie
$scriptUrl = (string) url('split.ticket/script', ['v' => $version], '', false);
$cfg['jsname'] = $scriptUrl;
$this->view->assign('config', $cfg);
$this->view->config = $cfg;
}
/**
* @return array<int, array<string, string>>
*/
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<string, mixed> $params
* @return array<string, mixed>
*/
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();
}
/**
* 输出后台 JSpatches 未部署到 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);
}
}