2026-06-04 14:15:12 +08:00
|
|
|
|
<?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;
|
2026-06-09 03:36:30 +08:00
|
|
|
|
use app\common\service\SplitTicketRuleService;
|
2026-07-02 03:39:29 +08:00
|
|
|
|
use app\common\service\SplitTicketSyncDispatchService;
|
2026-06-09 03:36:30 +08:00
|
|
|
|
use app\common\service\SplitTicketSyncLogger;
|
2026-06-04 14:15:12 +08:00
|
|
|
|
use think\Db;
|
2026-06-04 17:01:40 +08:00
|
|
|
|
use think\Lang;
|
|
|
|
|
|
use think\Loader;
|
2026-06-04 14:15:12 +08:00
|
|
|
|
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[] 无需鉴权的方法 */
|
2026-07-02 03:39:29 +08:00
|
|
|
|
protected $noNeedRight = ['script', 'syncpolling'];
|
2026-06-04 14:15:12 +08:00
|
|
|
|
|
|
|
|
|
|
/** @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());
|
2026-06-20 04:47:34 +08:00
|
|
|
|
$this->assignconfig('splitLinkFilterList', $this->buildTicketSplitLinkFilterList());
|
2026-06-09 03:36:30 +08:00
|
|
|
|
$this->assignconfig([
|
|
|
|
|
|
'syncConfirmMsg' => __('Sync confirm'),
|
|
|
|
|
|
'syncBackgroundStartedMsg' => __('Sync background started'),
|
|
|
|
|
|
'syncInProgressMsg' => __('Sync in progress'),
|
|
|
|
|
|
'syncTicketStartedMsg' => __('Sync ticket started'),
|
2026-07-02 03:39:29 +08:00
|
|
|
|
'syncDoneMsg' => __('Sync done'),
|
2026-06-09 03:36:30 +08:00
|
|
|
|
]);
|
2026-06-04 14:15:12 +08:00
|
|
|
|
|
|
|
|
|
|
$this->setupPatchFrontend();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 17:01:40 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 加载工单语言包(优先 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 14:15:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 未部署 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');
|
2026-06-10 06:44:57 +08:00
|
|
|
|
// 使用站内相对路径,避免生产环境 domain() 与反向代理/HTTPS 不一致导致 JS 跨域丢 Cookie
|
|
|
|
|
|
$scriptUrl = (string) url('split.ticket/script', ['v' => $version], '', false);
|
2026-06-04 14:15:12 +08:00
|
|
|
|
$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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-20 04:47:34 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 已有工单关联的分流链接(供列表筛选下拉)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @return array<string, string> id => label
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function buildTicketSplitLinkFilterList(): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$ticketTable = $this->model->getTable();
|
|
|
|
|
|
$linkTable = (new LinkModel())->getTable();
|
|
|
|
|
|
$query = Db::table($ticketTable)
|
|
|
|
|
|
->alias('t')
|
|
|
|
|
|
->join($linkTable . ' l', 't.split_link_id = l.id')
|
|
|
|
|
|
->where('t.split_link_id', '>', 0)
|
|
|
|
|
|
->group('t.split_link_id')
|
|
|
|
|
|
->field('l.id,l.link_code,l.description')
|
|
|
|
|
|
->order('l.id', 'desc');
|
|
|
|
|
|
if ($this->dataLimit) {
|
|
|
|
|
|
$adminIds = $this->getDataLimitAdminIds();
|
|
|
|
|
|
if (is_array($adminIds)) {
|
|
|
|
|
|
$query->where('t.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[(string) $row['id']] = $label;
|
|
|
|
|
|
}
|
|
|
|
|
|
return $list;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 按当前筛选条件汇总列表统计列
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param callable|array<mixed> $where buildparams() 返回的闭包或条件数组
|
|
|
|
|
|
* @return array<string, int|float|string>
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function buildListSummary($where): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$stats = $this->model
|
|
|
|
|
|
->where($where)
|
|
|
|
|
|
->fieldRaw(
|
|
|
|
|
|
'COALESCE(SUM(ticket_total), 0) AS sum_ticket_total,'
|
|
|
|
|
|
. ' COALESCE(SUM(complete_count), 0) AS sum_complete_count,'
|
|
|
|
|
|
. ' COALESCE(SUM(speed_per_hour), 0) AS sum_speed_per_hour,'
|
|
|
|
|
|
. ' COALESCE(AVG(CASE WHEN ticket_total > 0 THEN complete_count * 100.0 / ticket_total ELSE 0 END), 0) AS avg_ticket_progress'
|
|
|
|
|
|
)
|
|
|
|
|
|
->find();
|
|
|
|
|
|
if (!$stats) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
'ticket_total' => 0,
|
|
|
|
|
|
'complete_count' => 0,
|
|
|
|
|
|
'speed_per_hour' => '0.00',
|
|
|
|
|
|
'ticket_progress_text' => '0.00%',
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
return [
|
|
|
|
|
|
'ticket_total' => (int) ($stats['sum_ticket_total'] ?? 0),
|
|
|
|
|
|
'complete_count' => (int) ($stats['sum_complete_count'] ?? 0),
|
|
|
|
|
|
'speed_per_hour' => number_format((float) ($stats['sum_speed_per_hour'] ?? 0), 2, '.', ''),
|
|
|
|
|
|
'ticket_progress_text' => number_format((float) ($stats['avg_ticket_progress'] ?? 0), 2, '.', '') . '%',
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 14:15:12 +08:00
|
|
|
|
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);
|
2026-06-20 04:47:34 +08:00
|
|
|
|
$result = [
|
|
|
|
|
|
'total' => $list->total(),
|
|
|
|
|
|
'rows' => $list->items(),
|
|
|
|
|
|
'summary' => $this->buildListSummary($where),
|
|
|
|
|
|
];
|
2026-06-04 14:15:12 +08:00
|
|
|
|
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'],
|
2026-06-20 05:07:11 +08:00
|
|
|
|
$params['sync_success_time'],
|
|
|
|
|
|
$params['sync_success_mode'],
|
2026-06-09 03:36:30 +08:00
|
|
|
|
$params['sync_fail_count'],
|
|
|
|
|
|
$params['speed_snapshot_count'],
|
|
|
|
|
|
$params['speed_snapshot_time'],
|
2026-07-07 14:56:39 +08:00
|
|
|
|
$params['click_count'],
|
|
|
|
|
|
$params['manual_manage']
|
2026-06-04 14:15:12 +08:00
|
|
|
|
);
|
|
|
|
|
|
return $params;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-07 14:56:39 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 用户切换工单状态时写入 manual_manage,防止同步自动恢复开启
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param array<string, mixed> $values
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function applyManualManageForStatusChange(array &$values): void
|
|
|
|
|
|
{
|
|
|
|
|
|
(new SplitTicketRuleService())->applyManualManageForStatusChange($values);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 03:36:30 +08:00
|
|
|
|
/**
|
2026-07-02 03:39:29 +08:00
|
|
|
|
* 手动同步选中工单(投递 CLI 后台执行,Web 请求立即返回)
|
2026-06-09 03:36:30 +08:00
|
|
|
|
*/
|
|
|
|
|
|
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(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2026-07-02 03:39:29 +08:00
|
|
|
|
/** @var int[] $allowedIds */
|
|
|
|
|
|
$allowedIds = [];
|
|
|
|
|
|
$denied = 0;
|
2026-06-09 03:36:30 +08:00
|
|
|
|
foreach ($list as $row) {
|
|
|
|
|
|
if (is_array($adminIds) && !in_array((int) $row[$this->dataLimitField], $adminIds, true)) {
|
2026-07-02 03:39:29 +08:00
|
|
|
|
$denied++;
|
2026-06-09 03:36:30 +08:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-07-02 03:39:29 +08:00
|
|
|
|
$allowedIds[] = (int) $row['id'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($allowedIds === []) {
|
|
|
|
|
|
$this->error(__('You have no permission'));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 尽早释放 Session 锁,避免投递等待期间阻塞同账号其它后台请求(如编辑/新增弹窗)
|
|
|
|
|
|
if (function_exists('session_write_close')) {
|
|
|
|
|
|
session_write_close();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$dispatch = new SplitTicketSyncDispatchService();
|
|
|
|
|
|
$result = $dispatch->dispatchManual($allowedIds);
|
|
|
|
|
|
|
|
|
|
|
|
$queuedCount = count($result['queued']);
|
|
|
|
|
|
$skippedCount = count($result['skipped']);
|
|
|
|
|
|
$failedCount = count($result['failed']);
|
|
|
|
|
|
|
|
|
|
|
|
if ($queuedCount === 0) {
|
|
|
|
|
|
if ($skippedCount > 0) {
|
|
|
|
|
|
$this->error(__('Sync all skipped busy'));
|
|
|
|
|
|
}
|
|
|
|
|
|
$this->error(__('Sync dispatch failed'));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$summary = sprintf(__('Sync dispatch queued'), $queuedCount);
|
|
|
|
|
|
if ($skippedCount > 0) {
|
|
|
|
|
|
$summary .= sprintf(__('Sync dispatch skipped suffix'), $skippedCount);
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($failedCount > 0) {
|
|
|
|
|
|
$summary .= sprintf(__('Sync dispatch failed suffix'), $failedCount);
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($denied > 0) {
|
|
|
|
|
|
$summary .= sprintf(__('Sync dispatch denied suffix'), $denied);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$this->success($summary, null, [
|
|
|
|
|
|
'queued' => $result['queued'],
|
|
|
|
|
|
'syncing' => $result['queued'],
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 轮询手工同步进度(依据 runtime 文件锁)
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function syncpolling(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$idsRaw = trim((string) $this->request->request('ids', ''));
|
|
|
|
|
|
if ($idsRaw === '') {
|
|
|
|
|
|
$this->success('', null, ['syncing' => []]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** @var int[] $ticketIds */
|
|
|
|
|
|
$ticketIds = [];
|
|
|
|
|
|
foreach (explode(',', $idsRaw) as $part) {
|
|
|
|
|
|
$part = trim($part);
|
|
|
|
|
|
if ($part !== '' && ctype_digit($part)) {
|
|
|
|
|
|
$ticketIds[] = (int) $part;
|
2026-06-09 03:36:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-02 03:39:29 +08:00
|
|
|
|
if ($ticketIds === []) {
|
|
|
|
|
|
$this->success('', null, ['syncing' => []]);
|
2026-06-09 03:36:30 +08:00
|
|
|
|
}
|
2026-07-02 03:39:29 +08:00
|
|
|
|
|
|
|
|
|
|
$dispatch = new SplitTicketSyncDispatchService();
|
|
|
|
|
|
$syncing = $dispatch->filterSyncingIds($ticketIds);
|
|
|
|
|
|
|
|
|
|
|
|
$this->success('', null, ['syncing' => $syncing]);
|
2026-06-09 03:36:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 批量更新:工单状态变更时联动号码
|
|
|
|
|
|
*
|
|
|
|
|
|
* @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'])) {
|
2026-07-07 14:56:39 +08:00
|
|
|
|
$this->applyManualManageForStatusChange($values);
|
2026-06-09 03:36:30 +08:00
|
|
|
|
$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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 14:15:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @return string
|
|
|
|
|
|
* @throws DbException
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function add()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (false === $this->request->isPost()) {
|
2026-06-20 04:47:34 +08:00
|
|
|
|
$copyRow = $this->buildCopyRowData();
|
|
|
|
|
|
if ($copyRow !== null) {
|
|
|
|
|
|
$this->view->assign('row', $copyRow);
|
|
|
|
|
|
}
|
2026-06-04 14:15:12 +08:00
|
|
|
|
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'));
|
|
|
|
|
|
}
|
2026-06-09 03:36:30 +08:00
|
|
|
|
$this->success('', null, ['id' => (int) $this->model->id]);
|
2026-06-04 14:15:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-20 04:47:34 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 从 copy_id 构建添加工单表单的预填数据(仅复制业务字段,不含同步统计)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @return array<string, mixed>|null
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function buildCopyRowData(): ?array
|
|
|
|
|
|
{
|
|
|
|
|
|
$copyId = (int) $this->request->get('copy_id/d', 0);
|
|
|
|
|
|
if ($copyId <= 0) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$row = $this->model->get($copyId);
|
|
|
|
|
|
if (!$row) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$adminIds = $this->getDataLimitAdminIds();
|
|
|
|
|
|
if (is_array($adminIds) && !in_array((int) $row[$this->dataLimitField], $adminIds, true)) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$data = $row->toArray();
|
|
|
|
|
|
$copyFields = [
|
|
|
|
|
|
'ticket_type',
|
|
|
|
|
|
'ticket_name',
|
|
|
|
|
|
'ticket_url',
|
|
|
|
|
|
'ticket_total',
|
|
|
|
|
|
'split_link_id',
|
|
|
|
|
|
'number_type',
|
|
|
|
|
|
'number_type_custom',
|
|
|
|
|
|
'order_limit',
|
|
|
|
|
|
'assign_ratio',
|
|
|
|
|
|
'account',
|
|
|
|
|
|
'password',
|
|
|
|
|
|
];
|
|
|
|
|
|
$copyRow = [];
|
|
|
|
|
|
foreach ($copyFields as $field) {
|
|
|
|
|
|
if (array_key_exists($field, $data)) {
|
|
|
|
|
|
$copyRow[$field] = $data[$field];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$copyRow['start_time'] = $data['start_time_text'] ?? '';
|
|
|
|
|
|
$copyRow['end_time'] = $data['end_time_text'] ?? '';
|
|
|
|
|
|
|
|
|
|
|
|
$name = trim((string) ($copyRow['ticket_name'] ?? ''));
|
|
|
|
|
|
if ($name !== '' && mb_strpos($name, ' (副本)') === false) {
|
|
|
|
|
|
$copyRow['ticket_name'] = $name . ' (副本)';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $copyRow;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 14:15:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @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'] = '';
|
|
|
|
|
|
}
|
2026-06-09 03:36:30 +08:00
|
|
|
|
$oldStatus = (string) ($row['status'] ?? 'hidden');
|
2026-07-07 14:56:39 +08:00
|
|
|
|
if (isset($params['status']) && (string) $params['status'] !== $oldStatus) {
|
|
|
|
|
|
$this->applyManualManageForStatusChange($params);
|
|
|
|
|
|
}
|
2026-06-04 14:15:12 +08:00
|
|
|
|
$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'));
|
|
|
|
|
|
}
|
2026-06-09 03:36:30 +08:00
|
|
|
|
if (isset($params['status']) && (string) $params['status'] !== $oldStatus) {
|
|
|
|
|
|
$fresh = $this->model->get($row['id']);
|
|
|
|
|
|
if ($fresh) {
|
|
|
|
|
|
(new SplitTicketRuleService())->syncNumbersWithTicketStatus($fresh);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-04 14:15:12 +08:00
|
|
|
|
$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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|