提交同步状态为异步
This commit is contained in:
@@ -8,8 +8,8 @@ use app\admin\model\split\Link as LinkModel;
|
|||||||
use app\admin\model\split\Ticket as TicketModel;
|
use app\admin\model\split\Ticket as TicketModel;
|
||||||
use app\common\controller\Backend;
|
use app\common\controller\Backend;
|
||||||
use app\common\service\SplitTicketRuleService;
|
use app\common\service\SplitTicketRuleService;
|
||||||
|
use app\common\service\SplitTicketSyncDispatchService;
|
||||||
use app\common\service\SplitTicketSyncLogger;
|
use app\common\service\SplitTicketSyncLogger;
|
||||||
use app\common\service\SplitTicketSyncService;
|
|
||||||
use think\Db;
|
use think\Db;
|
||||||
use think\Lang;
|
use think\Lang;
|
||||||
use think\Loader;
|
use think\Loader;
|
||||||
@@ -39,7 +39,7 @@ class Ticket extends Backend
|
|||||||
protected $modelSceneValidate = true;
|
protected $modelSceneValidate = true;
|
||||||
|
|
||||||
/** @var string[] 无需鉴权的方法 */
|
/** @var string[] 无需鉴权的方法 */
|
||||||
protected $noNeedRight = ['script'];
|
protected $noNeedRight = ['script', 'syncpolling'];
|
||||||
|
|
||||||
/** @var string patches 视图目录 */
|
/** @var string patches 视图目录 */
|
||||||
private const PATCH_VIEW_DIR = 'patches/application/admin/view/split/ticket/';
|
private const PATCH_VIEW_DIR = 'patches/application/admin/view/split/ticket/';
|
||||||
@@ -64,6 +64,7 @@ class Ticket extends Backend
|
|||||||
'syncBackgroundStartedMsg' => __('Sync background started'),
|
'syncBackgroundStartedMsg' => __('Sync background started'),
|
||||||
'syncInProgressMsg' => __('Sync in progress'),
|
'syncInProgressMsg' => __('Sync in progress'),
|
||||||
'syncTicketStartedMsg' => __('Sync ticket started'),
|
'syncTicketStartedMsg' => __('Sync ticket started'),
|
||||||
|
'syncDoneMsg' => __('Sync done'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->setupPatchFrontend();
|
$this->setupPatchFrontend();
|
||||||
@@ -284,7 +285,7 @@ class Ticket extends Backend
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 手动同步选中工单
|
* 手动同步选中工单(投递 CLI 后台执行,Web 请求立即返回)
|
||||||
*/
|
*/
|
||||||
public function sync(): void
|
public function sync(): void
|
||||||
{
|
{
|
||||||
@@ -307,31 +308,84 @@ class Ticket extends Backend
|
|||||||
'appDebug' => SplitTicketSyncLogger::isEnabled(),
|
'appDebug' => SplitTicketSyncLogger::isEnabled(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$service = new SplitTicketSyncService();
|
/** @var int[] $allowedIds */
|
||||||
$ok = 0;
|
$allowedIds = [];
|
||||||
$fail = 0;
|
$denied = 0;
|
||||||
$messages = [];
|
|
||||||
|
|
||||||
foreach ($list as $row) {
|
foreach ($list as $row) {
|
||||||
if (is_array($adminIds) && !in_array((int) $row[$this->dataLimitField], $adminIds, true)) {
|
if (is_array($adminIds) && !in_array((int) $row[$this->dataLimitField], $adminIds, true)) {
|
||||||
$fail++;
|
$denied++;
|
||||||
$messages[] = '#' . $row['id'] . ': 无权限';
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$result = $service->syncOne((int) $row['id'], true);
|
$allowedIds[] = (int) $row['id'];
|
||||||
if ($result['success']) {
|
}
|
||||||
$ok++;
|
|
||||||
} else {
|
if ($allowedIds === []) {
|
||||||
$fail++;
|
$this->error(__('You have no permission'));
|
||||||
$messages[] = '#' . $row['id'] . ': ' . ($result['message'] ?? '失败');
|
}
|
||||||
|
|
||||||
|
// 尽早释放 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$summary = sprintf('成功 %d 条,失败 %d 条', $ok, $fail);
|
if ($ticketIds === []) {
|
||||||
if ($fail > 0) {
|
$this->success('', null, ['syncing' => []]);
|
||||||
$summary .= ';' . implode(';', array_slice($messages, 0, 5));
|
|
||||||
}
|
}
|
||||||
$this->success($summary);
|
|
||||||
|
$dispatch = new SplitTicketSyncDispatchService();
|
||||||
|
$syncing = $dispatch->filterSyncingIds($ticketIds);
|
||||||
|
|
||||||
|
$this->success('', null, ['syncing' => $syncing]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ return [
|
|||||||
'Sync in progress' => '同步中',
|
'Sync in progress' => '同步中',
|
||||||
'Sync ticket started' => '正在同步工单',
|
'Sync ticket started' => '正在同步工单',
|
||||||
'Sync done' => '同步完成',
|
'Sync done' => '同步完成',
|
||||||
|
'Sync dispatch queued' => '已提交 %d 条同步任务到后台执行',
|
||||||
|
'Sync dispatch skipped suffix' => ',%d 条已在同步中已跳过',
|
||||||
|
'Sync dispatch failed suffix' => ',%d 条启动失败',
|
||||||
|
'Sync dispatch denied suffix' => ',%d 条无权限未提交',
|
||||||
|
'Sync all skipped busy' => '所选工单均在同步中,请稍后再试',
|
||||||
|
'Sync dispatch failed' => '未能启动同步任务,请检查服务器是否允许后台执行 CLI(exec/shell_exec)',
|
||||||
'Sync status success' => '同步成功',
|
'Sync status success' => '同步成功',
|
||||||
'Sync status error' => '同步异常',
|
'Sync status error' => '同步异常',
|
||||||
'Sync status pending' => '待同步',
|
'Sync status pending' => '待同步',
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工单手工同步 CLI 后台投递
|
||||||
|
*
|
||||||
|
* Web 请求仅负责鉴权与投递,实际 Spider 在独立 CLI 进程中执行,
|
||||||
|
* 避免长时间占用 PHP-FPM 与 Session 锁导致后台其它页面无法打开。
|
||||||
|
*/
|
||||||
|
class SplitTicketSyncDispatchService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 投递手工同步任务到后台 CLI
|
||||||
|
*
|
||||||
|
* @param int[] $ticketIds 已通过权限校验的工单 ID
|
||||||
|
* @return array{queued:int[],skipped:int[],failed:int[]}
|
||||||
|
*/
|
||||||
|
public function dispatchManual(array $ticketIds): array
|
||||||
|
{
|
||||||
|
$queued = [];
|
||||||
|
$skipped = [];
|
||||||
|
$failed = [];
|
||||||
|
|
||||||
|
$php = $this->resolvePhpBinary();
|
||||||
|
$think = $this->resolveThinkScript();
|
||||||
|
$root = $this->resolveRootPath();
|
||||||
|
$lockService = new SplitTicketSyncLockService();
|
||||||
|
|
||||||
|
foreach ($ticketIds as $rawId) {
|
||||||
|
$ticketId = (int) $rawId;
|
||||||
|
if ($ticketId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($lockService->isLocked($ticketId)) {
|
||||||
|
$skipped[] = $ticketId;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->spawnCli($php, $think, $root, $ticketId)) {
|
||||||
|
$queued[] = $ticketId;
|
||||||
|
} else {
|
||||||
|
$failed[] = $ticketId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SplitTicketSyncLogger::log('web', 'manual sync dispatched', [
|
||||||
|
'queued' => $queued,
|
||||||
|
'skipped' => $skipped,
|
||||||
|
'failed' => $failed,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'queued' => $queued,
|
||||||
|
'skipped' => $skipped,
|
||||||
|
'failed' => $failed,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询仍在同步中的工单 ID(依据 runtime 文件锁)
|
||||||
|
*
|
||||||
|
* @param int[] $ticketIds
|
||||||
|
* @return int[]
|
||||||
|
*/
|
||||||
|
public function filterSyncingIds(array $ticketIds): array
|
||||||
|
{
|
||||||
|
$lockService = new SplitTicketSyncLockService();
|
||||||
|
$syncing = [];
|
||||||
|
foreach ($ticketIds as $rawId) {
|
||||||
|
$ticketId = (int) $rawId;
|
||||||
|
if ($ticketId > 0 && $lockService->isLocked($ticketId)) {
|
||||||
|
$syncing[] = $ticketId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $syncing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台启动 split:sync-tickets CLI
|
||||||
|
*/
|
||||||
|
private function spawnCli(string $php, string $think, string $root, int $ticketId): bool
|
||||||
|
{
|
||||||
|
$logDir = $this->resolveLogDir();
|
||||||
|
$logFile = $logDir . 'cli_' . $ticketId . '_' . date('YmdHis') . '.log';
|
||||||
|
|
||||||
|
$inner = sprintf(
|
||||||
|
'%s %s split:sync-tickets --ticket=%d >> %s 2>&1',
|
||||||
|
escapeshellarg($php),
|
||||||
|
escapeshellarg($think),
|
||||||
|
$ticketId,
|
||||||
|
escapeshellarg($logFile)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($this->isWindows()) {
|
||||||
|
$command = sprintf('start /B cmd /C %s', $inner);
|
||||||
|
} else {
|
||||||
|
$command = sprintf('cd %s && nohup %s &', escapeshellarg($root), $inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->canUseExec()) {
|
||||||
|
@exec($command, $output, $exitCode);
|
||||||
|
SplitTicketSyncLogger::log('web', 'cli spawn exec', [
|
||||||
|
'ticketId' => $ticketId,
|
||||||
|
'command' => $command,
|
||||||
|
'exitCode' => $exitCode,
|
||||||
|
]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->canUseShellExec()) {
|
||||||
|
@shell_exec($command);
|
||||||
|
SplitTicketSyncLogger::log('web', 'cli spawn shell_exec', [
|
||||||
|
'ticketId' => $ticketId,
|
||||||
|
'command' => $command,
|
||||||
|
]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SplitTicketSyncLogger::log('web', 'cli spawn failed: exec disabled', [
|
||||||
|
'ticketId' => $ticketId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolvePhpBinary(): string
|
||||||
|
{
|
||||||
|
if (defined('PHP_BINARY') && PHP_BINARY !== '') {
|
||||||
|
return PHP_BINARY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'php';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveThinkScript(): string
|
||||||
|
{
|
||||||
|
$root = $this->resolveRootPath();
|
||||||
|
return rtrim($root, '/\\') . DIRECTORY_SEPARATOR . 'think';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveRootPath(): string
|
||||||
|
{
|
||||||
|
if (defined('ROOT_PATH') && ROOT_PATH !== '') {
|
||||||
|
return rtrim(ROOT_PATH, '/\\') . DIRECTORY_SEPARATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rtrim(dirname(__DIR__, 3), '/\\') . DIRECTORY_SEPARATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveLogDir(): string
|
||||||
|
{
|
||||||
|
$runtime = defined('RUNTIME_PATH') ? RUNTIME_PATH : ($this->resolveRootPath() . 'runtime' . DIRECTORY_SEPARATOR);
|
||||||
|
$dir = rtrim($runtime, '/\\') . DIRECTORY_SEPARATOR . 'split_ticket_sync' . DIRECTORY_SEPARATOR;
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
@mkdir($dir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isWindows(): bool
|
||||||
|
{
|
||||||
|
return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
private function disabledFunctions(): array
|
||||||
|
{
|
||||||
|
$raw = (string) ini_get('disable_functions');
|
||||||
|
if ($raw === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_filter(array_map('trim', explode(',', $raw)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function canUseExec(): bool
|
||||||
|
{
|
||||||
|
return function_exists('exec') && !in_array('exec', $this->disabledFunctions(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function canUseShellExec(): bool
|
||||||
|
{
|
||||||
|
return function_exists('shell_exec') && !in_array('shell_exec', $this->disabledFunctions(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,6 +43,20 @@ class SplitTicketSyncLockService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工单是否处于同步中(锁文件存在且未过期)
|
||||||
|
*/
|
||||||
|
public function isLocked(int $ticketId): bool
|
||||||
|
{
|
||||||
|
$path = $this->lockPath($ticketId);
|
||||||
|
if ($this->isStaleLock($path)) {
|
||||||
|
@unlink($path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_file($path);
|
||||||
|
}
|
||||||
|
|
||||||
private function lockPath(int $ticketId): string
|
private function lockPath(int $ticketId): string
|
||||||
{
|
{
|
||||||
$runtime = defined('RUNTIME_PATH') ? RUNTIME_PATH : (dirname(__DIR__, 3) . '/runtime/');
|
$runtime = defined('RUNTIME_PATH') ? RUNTIME_PATH : (dirname(__DIR__, 3) . '/runtime/');
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use app\admin\model\split\Link as LinkModel;
|
|||||||
use app\admin\model\split\Ticket as TicketModel;
|
use app\admin\model\split\Ticket as TicketModel;
|
||||||
use app\common\controller\Backend;
|
use app\common\controller\Backend;
|
||||||
use app\common\service\SplitTicketRuleService;
|
use app\common\service\SplitTicketRuleService;
|
||||||
|
use app\common\service\SplitTicketSyncDispatchService;
|
||||||
use app\common\service\SplitTicketSyncLogger;
|
use app\common\service\SplitTicketSyncLogger;
|
||||||
use app\common\service\SplitTicketSyncService;
|
|
||||||
use think\Db;
|
use think\Db;
|
||||||
use think\Lang;
|
use think\Lang;
|
||||||
use think\Loader;
|
use think\Loader;
|
||||||
@@ -39,7 +39,7 @@ class Ticket extends Backend
|
|||||||
protected $modelSceneValidate = true;
|
protected $modelSceneValidate = true;
|
||||||
|
|
||||||
/** @var string[] 无需鉴权的方法 */
|
/** @var string[] 无需鉴权的方法 */
|
||||||
protected $noNeedRight = ['script'];
|
protected $noNeedRight = ['script', 'syncpolling'];
|
||||||
|
|
||||||
/** @var string patches 视图目录 */
|
/** @var string patches 视图目录 */
|
||||||
private const PATCH_VIEW_DIR = 'patches/application/admin/view/split/ticket/';
|
private const PATCH_VIEW_DIR = 'patches/application/admin/view/split/ticket/';
|
||||||
@@ -64,6 +64,7 @@ class Ticket extends Backend
|
|||||||
'syncBackgroundStartedMsg' => __('Sync background started'),
|
'syncBackgroundStartedMsg' => __('Sync background started'),
|
||||||
'syncInProgressMsg' => __('Sync in progress'),
|
'syncInProgressMsg' => __('Sync in progress'),
|
||||||
'syncTicketStartedMsg' => __('Sync ticket started'),
|
'syncTicketStartedMsg' => __('Sync ticket started'),
|
||||||
|
'syncDoneMsg' => __('Sync done'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->setupPatchFrontend();
|
$this->setupPatchFrontend();
|
||||||
@@ -284,7 +285,7 @@ class Ticket extends Backend
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 手动同步选中工单
|
* 手动同步选中工单(投递 CLI 后台执行,Web 请求立即返回)
|
||||||
*/
|
*/
|
||||||
public function sync(): void
|
public function sync(): void
|
||||||
{
|
{
|
||||||
@@ -307,31 +308,84 @@ class Ticket extends Backend
|
|||||||
'appDebug' => SplitTicketSyncLogger::isEnabled(),
|
'appDebug' => SplitTicketSyncLogger::isEnabled(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$service = new SplitTicketSyncService();
|
/** @var int[] $allowedIds */
|
||||||
$ok = 0;
|
$allowedIds = [];
|
||||||
$fail = 0;
|
$denied = 0;
|
||||||
$messages = [];
|
|
||||||
|
|
||||||
foreach ($list as $row) {
|
foreach ($list as $row) {
|
||||||
if (is_array($adminIds) && !in_array((int) $row[$this->dataLimitField], $adminIds, true)) {
|
if (is_array($adminIds) && !in_array((int) $row[$this->dataLimitField], $adminIds, true)) {
|
||||||
$fail++;
|
$denied++;
|
||||||
$messages[] = '#' . $row['id'] . ': 无权限';
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$result = $service->syncOne((int) $row['id'], true);
|
$allowedIds[] = (int) $row['id'];
|
||||||
if ($result['success']) {
|
}
|
||||||
$ok++;
|
|
||||||
} else {
|
if ($allowedIds === []) {
|
||||||
$fail++;
|
$this->error(__('You have no permission'));
|
||||||
$messages[] = '#' . $row['id'] . ': ' . ($result['message'] ?? '失败');
|
}
|
||||||
|
|
||||||
|
// 尽早释放 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$summary = sprintf('成功 %d 条,失败 %d 条', $ok, $fail);
|
if ($ticketIds === []) {
|
||||||
if ($fail > 0) {
|
$this->success('', null, ['syncing' => []]);
|
||||||
$summary .= ';' . implode(';', array_slice($messages, 0, 5));
|
|
||||||
}
|
}
|
||||||
$this->success($summary);
|
|
||||||
|
$dispatch = new SplitTicketSyncDispatchService();
|
||||||
|
$syncing = $dispatch->filterSyncingIds($ticketIds);
|
||||||
|
|
||||||
|
$this->success('', null, ['syncing' => $syncing]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ return [
|
|||||||
'Sync in progress' => '同步中',
|
'Sync in progress' => '同步中',
|
||||||
'Sync ticket started' => '正在同步工单',
|
'Sync ticket started' => '正在同步工单',
|
||||||
'Sync done' => '同步完成',
|
'Sync done' => '同步完成',
|
||||||
|
'Sync dispatch queued' => '已提交 %d 条同步任务到后台执行',
|
||||||
|
'Sync dispatch skipped suffix' => ',%d 条已在同步中已跳过',
|
||||||
|
'Sync dispatch failed suffix' => ',%d 条启动失败',
|
||||||
|
'Sync dispatch denied suffix' => ',%d 条无权限未提交',
|
||||||
|
'Sync all skipped busy' => '所选工单均在同步中,请稍后再试',
|
||||||
|
'Sync dispatch failed' => '未能启动同步任务,请检查服务器是否允许后台执行 CLI(exec/shell_exec)',
|
||||||
'Sync status success' => '同步成功',
|
'Sync status success' => '同步成功',
|
||||||
'Sync status error' => '同步异常',
|
'Sync status error' => '同步异常',
|
||||||
'Sync status pending' => '待同步',
|
'Sync status pending' => '待同步',
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工单手工同步 CLI 后台投递
|
||||||
|
*
|
||||||
|
* Web 请求仅负责鉴权与投递,实际 Spider 在独立 CLI 进程中执行,
|
||||||
|
* 避免长时间占用 PHP-FPM 与 Session 锁导致后台其它页面无法打开。
|
||||||
|
*/
|
||||||
|
class SplitTicketSyncDispatchService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 投递手工同步任务到后台 CLI
|
||||||
|
*
|
||||||
|
* @param int[] $ticketIds 已通过权限校验的工单 ID
|
||||||
|
* @return array{queued:int[],skipped:int[],failed:int[]}
|
||||||
|
*/
|
||||||
|
public function dispatchManual(array $ticketIds): array
|
||||||
|
{
|
||||||
|
$queued = [];
|
||||||
|
$skipped = [];
|
||||||
|
$failed = [];
|
||||||
|
|
||||||
|
$php = $this->resolvePhpBinary();
|
||||||
|
$think = $this->resolveThinkScript();
|
||||||
|
$root = $this->resolveRootPath();
|
||||||
|
$lockService = new SplitTicketSyncLockService();
|
||||||
|
|
||||||
|
foreach ($ticketIds as $rawId) {
|
||||||
|
$ticketId = (int) $rawId;
|
||||||
|
if ($ticketId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($lockService->isLocked($ticketId)) {
|
||||||
|
$skipped[] = $ticketId;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->spawnCli($php, $think, $root, $ticketId)) {
|
||||||
|
$queued[] = $ticketId;
|
||||||
|
} else {
|
||||||
|
$failed[] = $ticketId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SplitTicketSyncLogger::log('web', 'manual sync dispatched', [
|
||||||
|
'queued' => $queued,
|
||||||
|
'skipped' => $skipped,
|
||||||
|
'failed' => $failed,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'queued' => $queued,
|
||||||
|
'skipped' => $skipped,
|
||||||
|
'failed' => $failed,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询仍在同步中的工单 ID(依据 runtime 文件锁)
|
||||||
|
*
|
||||||
|
* @param int[] $ticketIds
|
||||||
|
* @return int[]
|
||||||
|
*/
|
||||||
|
public function filterSyncingIds(array $ticketIds): array
|
||||||
|
{
|
||||||
|
$lockService = new SplitTicketSyncLockService();
|
||||||
|
$syncing = [];
|
||||||
|
foreach ($ticketIds as $rawId) {
|
||||||
|
$ticketId = (int) $rawId;
|
||||||
|
if ($ticketId > 0 && $lockService->isLocked($ticketId)) {
|
||||||
|
$syncing[] = $ticketId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $syncing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台启动 split:sync-tickets CLI
|
||||||
|
*/
|
||||||
|
private function spawnCli(string $php, string $think, string $root, int $ticketId): bool
|
||||||
|
{
|
||||||
|
$logDir = $this->resolveLogDir();
|
||||||
|
$logFile = $logDir . 'cli_' . $ticketId . '_' . date('YmdHis') . '.log';
|
||||||
|
|
||||||
|
$inner = sprintf(
|
||||||
|
'%s %s split:sync-tickets --ticket=%d >> %s 2>&1',
|
||||||
|
escapeshellarg($php),
|
||||||
|
escapeshellarg($think),
|
||||||
|
$ticketId,
|
||||||
|
escapeshellarg($logFile)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($this->isWindows()) {
|
||||||
|
$command = sprintf('start /B cmd /C %s', $inner);
|
||||||
|
} else {
|
||||||
|
$command = sprintf('cd %s && nohup %s &', escapeshellarg($root), $inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->canUseExec()) {
|
||||||
|
@exec($command, $output, $exitCode);
|
||||||
|
SplitTicketSyncLogger::log('web', 'cli spawn exec', [
|
||||||
|
'ticketId' => $ticketId,
|
||||||
|
'command' => $command,
|
||||||
|
'exitCode' => $exitCode,
|
||||||
|
]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->canUseShellExec()) {
|
||||||
|
@shell_exec($command);
|
||||||
|
SplitTicketSyncLogger::log('web', 'cli spawn shell_exec', [
|
||||||
|
'ticketId' => $ticketId,
|
||||||
|
'command' => $command,
|
||||||
|
]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SplitTicketSyncLogger::log('web', 'cli spawn failed: exec disabled', [
|
||||||
|
'ticketId' => $ticketId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolvePhpBinary(): string
|
||||||
|
{
|
||||||
|
if (defined('PHP_BINARY') && PHP_BINARY !== '') {
|
||||||
|
return PHP_BINARY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'php';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveThinkScript(): string
|
||||||
|
{
|
||||||
|
$root = $this->resolveRootPath();
|
||||||
|
return rtrim($root, '/\\') . DIRECTORY_SEPARATOR . 'think';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveRootPath(): string
|
||||||
|
{
|
||||||
|
if (defined('ROOT_PATH') && ROOT_PATH !== '') {
|
||||||
|
return rtrim(ROOT_PATH, '/\\') . DIRECTORY_SEPARATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rtrim(dirname(__DIR__, 3), '/\\') . DIRECTORY_SEPARATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveLogDir(): string
|
||||||
|
{
|
||||||
|
$runtime = defined('RUNTIME_PATH') ? RUNTIME_PATH : ($this->resolveRootPath() . 'runtime' . DIRECTORY_SEPARATOR);
|
||||||
|
$dir = rtrim($runtime, '/\\') . DIRECTORY_SEPARATOR . 'split_ticket_sync' . DIRECTORY_SEPARATOR;
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
@mkdir($dir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isWindows(): bool
|
||||||
|
{
|
||||||
|
return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
private function disabledFunctions(): array
|
||||||
|
{
|
||||||
|
$raw = (string) ini_get('disable_functions');
|
||||||
|
if ($raw === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_filter(array_map('trim', explode(',', $raw)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function canUseExec(): bool
|
||||||
|
{
|
||||||
|
return function_exists('exec') && !in_array('exec', $this->disabledFunctions(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function canUseShellExec(): bool
|
||||||
|
{
|
||||||
|
return function_exists('shell_exec') && !in_array('shell_exec', $this->disabledFunctions(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,6 +43,20 @@ class SplitTicketSyncLockService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工单是否处于同步中(锁文件存在且未过期)
|
||||||
|
*/
|
||||||
|
public function isLocked(int $ticketId): bool
|
||||||
|
{
|
||||||
|
$path = $this->lockPath($ticketId);
|
||||||
|
if ($this->isStaleLock($path)) {
|
||||||
|
@unlink($path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_file($path);
|
||||||
|
}
|
||||||
|
|
||||||
private function lockPath(int $ticketId): string
|
private function lockPath(int $ticketId): string
|
||||||
{
|
{
|
||||||
$runtime = defined('RUNTIME_PATH') ? RUNTIME_PATH : (dirname(__DIR__, 3) . '/runtime/');
|
$runtime = defined('RUNTIME_PATH') ? RUNTIME_PATH : (dirname(__DIR__, 3) . '/runtime/');
|
||||||
|
|||||||
@@ -244,6 +244,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
|||||||
|
|
||||||
/** @type {number[]} 正在手动同步的工单 ID */
|
/** @type {number[]} 正在手动同步的工单 ID */
|
||||||
syncingTicketIds: [],
|
syncingTicketIds: [],
|
||||||
|
syncPollTimer: null,
|
||||||
|
syncPollStartedAt: 0,
|
||||||
|
syncPollGraceMs: 8000,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染筛选结果汇总行(全量筛选数据,非当前页)
|
* 渲染筛选结果汇总行(全量筛选数据,非当前页)
|
||||||
@@ -269,7 +272,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 后台同步:标记「同步中」并请求 sync 接口
|
* 后台同步:标记「同步中」、投递 CLI,并轮询直至锁释放
|
||||||
*
|
*
|
||||||
* @param {object} table bootstrapTable 实例
|
* @param {object} table bootstrapTable 实例
|
||||||
* @param {number[]} ids 工单 ID
|
* @param {number[]} ids 工单 ID
|
||||||
@@ -292,13 +295,81 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
|||||||
url: 'split.ticket/sync',
|
url: 'split.ticket/sync',
|
||||||
data: {ids: ids.join(',')},
|
data: {ids: ids.join(',')},
|
||||||
loading: false
|
loading: false
|
||||||
}, function () {
|
}, function (data, ret) {
|
||||||
|
var queued = (ret.data && ret.data.queued) ? ret.data.queued : ids;
|
||||||
|
Controller.api.syncingTicketIds = (queued || []).map(function (id) {
|
||||||
|
return parseInt(id, 10);
|
||||||
|
}).filter(function (id) {
|
||||||
|
return !isNaN(id) && id > 0;
|
||||||
|
});
|
||||||
|
if (Controller.api.syncingTicketIds.length) {
|
||||||
|
Controller.api.startSyncPolling(table);
|
||||||
|
} else {
|
||||||
Controller.api.finishTicketsSync(table);
|
Controller.api.finishTicketsSync(table);
|
||||||
|
}
|
||||||
}, function () {
|
}, function () {
|
||||||
Controller.api.finishTicketsSync(table);
|
Controller.api.finishTicketsSync(table);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮询同步锁状态,全部完成后刷新列表
|
||||||
|
*/
|
||||||
|
startSyncPolling: function (table) {
|
||||||
|
Controller.api.stopSyncPolling();
|
||||||
|
if (!Controller.api.syncingTicketIds.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Controller.api.syncPollStartedAt = Date.now();
|
||||||
|
Controller.api.syncPollTimer = setInterval(function () {
|
||||||
|
if (!Controller.api.syncingTicketIds.length) {
|
||||||
|
Controller.api.stopSyncPolling();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Fast.api.ajax({
|
||||||
|
url: 'split.ticket/syncpolling',
|
||||||
|
data: {ids: Controller.api.syncingTicketIds.join(',')},
|
||||||
|
loading: false
|
||||||
|
}, function (data, ret) {
|
||||||
|
var still = (ret.data && ret.data.syncing) ? ret.data.syncing : [];
|
||||||
|
still = (still || []).map(function (id) {
|
||||||
|
return parseInt(id, 10);
|
||||||
|
}).filter(function (id) {
|
||||||
|
return !isNaN(id) && id > 0;
|
||||||
|
});
|
||||||
|
var elapsed = Date.now() - (Controller.api.syncPollStartedAt || 0);
|
||||||
|
var graceMs = Controller.api.syncPollGraceMs || 8000;
|
||||||
|
if (still.length === 0 && elapsed < graceMs) {
|
||||||
|
table.bootstrapTable('refresh', {silent: true});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (still.length === 0) {
|
||||||
|
Controller.api.stopSyncPolling();
|
||||||
|
var doneMsg = (typeof Config.syncDoneMsg !== 'undefined' && Config.syncDoneMsg)
|
||||||
|
? Config.syncDoneMsg
|
||||||
|
: '同步完成';
|
||||||
|
Toastr.success(doneMsg);
|
||||||
|
Controller.api.finishTicketsSync(table);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (still.join(',') !== Controller.api.syncingTicketIds.join(',')) {
|
||||||
|
Controller.api.syncingTicketIds = still;
|
||||||
|
var rowData = table.bootstrapTable('getData');
|
||||||
|
table.bootstrapTable('load', rowData);
|
||||||
|
}
|
||||||
|
table.bootstrapTable('refresh', {silent: true});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
|
||||||
|
stopSyncPolling: function () {
|
||||||
|
if (Controller.api.syncPollTimer) {
|
||||||
|
clearInterval(Controller.api.syncPollTimer);
|
||||||
|
Controller.api.syncPollTimer = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将选中工单标记为「同步中」并刷新列表展示(不请求后端)
|
* 将选中工单标记为「同步中」并刷新列表展示(不请求后端)
|
||||||
*/
|
*/
|
||||||
@@ -316,6 +387,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
|||||||
* 同步结束:清除标记并刷新列表数据
|
* 同步结束:清除标记并刷新列表数据
|
||||||
*/
|
*/
|
||||||
finishTicketsSync: function (table) {
|
finishTicketsSync: function (table) {
|
||||||
|
Controller.api.stopSyncPolling();
|
||||||
Controller.api.syncingTicketIds = [];
|
Controller.api.syncingTicketIds = [];
|
||||||
table.bootstrapTable('refresh', {
|
table.bootstrapTable('refresh', {
|
||||||
silent: true
|
silent: true
|
||||||
|
|||||||
@@ -244,6 +244,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
|||||||
|
|
||||||
/** @type {number[]} 正在手动同步的工单 ID */
|
/** @type {number[]} 正在手动同步的工单 ID */
|
||||||
syncingTicketIds: [],
|
syncingTicketIds: [],
|
||||||
|
syncPollTimer: null,
|
||||||
|
syncPollStartedAt: 0,
|
||||||
|
syncPollGraceMs: 8000,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染筛选结果汇总行(全量筛选数据,非当前页)
|
* 渲染筛选结果汇总行(全量筛选数据,非当前页)
|
||||||
@@ -269,7 +272,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 后台同步:标记「同步中」并请求 sync 接口
|
* 后台同步:标记「同步中」、投递 CLI,并轮询直至锁释放
|
||||||
*
|
*
|
||||||
* @param {object} table bootstrapTable 实例
|
* @param {object} table bootstrapTable 实例
|
||||||
* @param {number[]} ids 工单 ID
|
* @param {number[]} ids 工单 ID
|
||||||
@@ -292,13 +295,81 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
|||||||
url: 'split.ticket/sync',
|
url: 'split.ticket/sync',
|
||||||
data: {ids: ids.join(',')},
|
data: {ids: ids.join(',')},
|
||||||
loading: false
|
loading: false
|
||||||
}, function () {
|
}, function (data, ret) {
|
||||||
|
var queued = (ret.data && ret.data.queued) ? ret.data.queued : ids;
|
||||||
|
Controller.api.syncingTicketIds = (queued || []).map(function (id) {
|
||||||
|
return parseInt(id, 10);
|
||||||
|
}).filter(function (id) {
|
||||||
|
return !isNaN(id) && id > 0;
|
||||||
|
});
|
||||||
|
if (Controller.api.syncingTicketIds.length) {
|
||||||
|
Controller.api.startSyncPolling(table);
|
||||||
|
} else {
|
||||||
Controller.api.finishTicketsSync(table);
|
Controller.api.finishTicketsSync(table);
|
||||||
|
}
|
||||||
}, function () {
|
}, function () {
|
||||||
Controller.api.finishTicketsSync(table);
|
Controller.api.finishTicketsSync(table);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮询同步锁状态,全部完成后刷新列表
|
||||||
|
*/
|
||||||
|
startSyncPolling: function (table) {
|
||||||
|
Controller.api.stopSyncPolling();
|
||||||
|
if (!Controller.api.syncingTicketIds.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Controller.api.syncPollStartedAt = Date.now();
|
||||||
|
Controller.api.syncPollTimer = setInterval(function () {
|
||||||
|
if (!Controller.api.syncingTicketIds.length) {
|
||||||
|
Controller.api.stopSyncPolling();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Fast.api.ajax({
|
||||||
|
url: 'split.ticket/syncpolling',
|
||||||
|
data: {ids: Controller.api.syncingTicketIds.join(',')},
|
||||||
|
loading: false
|
||||||
|
}, function (data, ret) {
|
||||||
|
var still = (ret.data && ret.data.syncing) ? ret.data.syncing : [];
|
||||||
|
still = (still || []).map(function (id) {
|
||||||
|
return parseInt(id, 10);
|
||||||
|
}).filter(function (id) {
|
||||||
|
return !isNaN(id) && id > 0;
|
||||||
|
});
|
||||||
|
var elapsed = Date.now() - (Controller.api.syncPollStartedAt || 0);
|
||||||
|
var graceMs = Controller.api.syncPollGraceMs || 8000;
|
||||||
|
if (still.length === 0 && elapsed < graceMs) {
|
||||||
|
table.bootstrapTable('refresh', {silent: true});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (still.length === 0) {
|
||||||
|
Controller.api.stopSyncPolling();
|
||||||
|
var doneMsg = (typeof Config.syncDoneMsg !== 'undefined' && Config.syncDoneMsg)
|
||||||
|
? Config.syncDoneMsg
|
||||||
|
: '同步完成';
|
||||||
|
Toastr.success(doneMsg);
|
||||||
|
Controller.api.finishTicketsSync(table);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (still.join(',') !== Controller.api.syncingTicketIds.join(',')) {
|
||||||
|
Controller.api.syncingTicketIds = still;
|
||||||
|
var rowData = table.bootstrapTable('getData');
|
||||||
|
table.bootstrapTable('load', rowData);
|
||||||
|
}
|
||||||
|
table.bootstrapTable('refresh', {silent: true});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
|
||||||
|
stopSyncPolling: function () {
|
||||||
|
if (Controller.api.syncPollTimer) {
|
||||||
|
clearInterval(Controller.api.syncPollTimer);
|
||||||
|
Controller.api.syncPollTimer = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将选中工单标记为「同步中」并刷新列表展示(不请求后端)
|
* 将选中工单标记为「同步中」并刷新列表展示(不请求后端)
|
||||||
*/
|
*/
|
||||||
@@ -316,6 +387,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
|||||||
* 同步结束:清除标记并刷新列表数据
|
* 同步结束:清除标记并刷新列表数据
|
||||||
*/
|
*/
|
||||||
finishTicketsSync: function (table) {
|
finishTicketsSync: function (table) {
|
||||||
|
Controller.api.stopSyncPolling();
|
||||||
Controller.api.syncingTicketIds = [];
|
Controller.api.syncingTicketIds = [];
|
||||||
table.bootstrapTable('refresh', {
|
table.bootstrapTable('refresh', {
|
||||||
silent: true
|
silent: true
|
||||||
|
|||||||
Reference in New Issue
Block a user