添加whatshub工单
This commit is contained in:
Regular → Executable
@@ -0,0 +1,6 @@
|
||||
-- Chatknow SCRM 工单云控自动同步周期(分钟,0=不自动同步)
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
INSERT INTO `fa_config` (`name`, `group`, `title`, `tip`, `type`, `visible`, `value`, `content`, `rule`, `extend`, `setting`)
|
||||
SELECT 'split_sync_interval_chatknow', 'split', 'Chatknow SCRM同步周期(分钟)', '0 表示不自动同步', 'number', '', '5', '', '', '', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `fa_config` WHERE `name` = 'split_sync_interval_chatknow' LIMIT 1);
|
||||
@@ -0,0 +1,14 @@
|
||||
-- 修复 SS 云控同步周期配置键名拼写错误:ss_custome -> ss_customer
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
UPDATE `fa_config`
|
||||
SET `name` = 'split_sync_interval_ss_customer'
|
||||
WHERE `name` = 'split_sync_interval_ss_custome'
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT `id` FROM `fa_config` WHERE `name` = 'split_sync_interval_ss_customer') AS `t`);
|
||||
|
||||
UPDATE `fa_config` AS `correct`
|
||||
INNER JOIN `fa_config` AS `wrong` ON `wrong`.`name` = 'split_sync_interval_ss_custome'
|
||||
SET `correct`.`value` = `wrong`.`value`
|
||||
WHERE `correct`.`name` = 'split_sync_interval_ss_customer';
|
||||
|
||||
DELETE FROM `fa_config` WHERE `name` = 'split_sync_interval_ss_custome';
|
||||
Regular → Executable
+29
@@ -29,6 +29,10 @@ INSERT INTO `fa_config` (`name`, `group`, `title`, `tip`, `type`, `visible`, `va
|
||||
SELECT 'split_sync_interval_ss_customer', 'split', 'SS云控(Customer)同步周期(分钟)', '0 表示不自动同步', 'number', '', '5', '', '', '', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `fa_config` WHERE `name` = 'split_sync_interval_ss_customer' LIMIT 1);
|
||||
|
||||
INSERT INTO `fa_config` (`name`, `group`, `title`, `tip`, `type`, `visible`, `value`, `content`, `rule`, `extend`, `setting`)
|
||||
SELECT 'split_sync_interval_chatknow', 'split', 'Chatknow SCRM同步周期(分钟)', '0 表示不自动同步', 'number', '', '5', '', '', '', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `fa_config` WHERE `name` = 'split_sync_interval_chatknow' LIMIT 1);
|
||||
|
||||
INSERT INTO `fa_config` (`name`, `group`, `title`, `tip`, `type`, `visible`, `value`, `content`, `rule`, `extend`, `setting`)
|
||||
SELECT 'split_sync_interval_ceo_scrm', 'split', 'CEO SCRM同步周期(分钟)', '0 表示不自动同步(蜘蛛未实现)', 'number', '', '0', '', '', '', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `fa_config` WHERE `name` = 'split_sync_interval_ceo_scrm' LIMIT 1);
|
||||
@@ -52,3 +56,28 @@ FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `fa_config` WHERE `name` = 'split_sync
|
||||
INSERT INTO `fa_config` (`name`, `group`, `title`, `tip`, `type`, `visible`, `value`, `content`, `rule`, `extend`, `setting`)
|
||||
SELECT 'split_sync_interval_sihai', 'split', '四海云控同步周期(分钟)', '0 表示不自动同步', 'number', '', '0', '', '', '', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `fa_config` WHERE `name` = 'split_sync_interval_sihai' LIMIT 1);
|
||||
|
||||
INSERT INTO `fa_config` (`name`, `group`, `title`, `tip`, `type`, `visible`, `value`, `content`, `rule`, `extend`, `setting`)
|
||||
SELECT 'split_sync_max_per_cron', 'split', '单次定时同步上限', '每分钟 cron 最多处理几条到期工单,防止打满 Node Browser 槽位', 'number', '', '2', '', '', '', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `fa_config` WHERE `name` = 'split_sync_max_per_cron' LIMIT 1);
|
||||
|
||||
INSERT INTO `fa_config` (`name`, `group`, `title`, `tip`, `type`, `visible`, `value`, `content`, `rule`, `extend`, `setting`)
|
||||
SELECT 'split_sync_cron_lock_ttl', 'split', '定时同步全局锁TTL(秒)', '异常退出后自动释放全局锁,避免后续 cron 永久跳过', 'number', '', '1800', '', '', '', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `fa_config` WHERE `name` = 'split_sync_cron_lock_ttl' LIMIT 1);
|
||||
|
||||
INSERT INTO `fa_config` (`name`, `group`, `title`, `tip`, `type`, `visible`, `value`, `content`, `rule`, `extend`, `setting`)
|
||||
SELECT 'split_sync_node_queue_threshold', 'split', 'Node排队跳过阈值', '排队数达到该值时本轮 cron 不再提交新任务', 'number', '', '2', '', '', '', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `fa_config` WHERE `name` = 'split_sync_node_queue_threshold' LIMIT 1);
|
||||
|
||||
-- 修复历史拼写错误:split_sync_interval_ss_custome -> split_sync_interval_ss_customer
|
||||
UPDATE `fa_config`
|
||||
SET `name` = 'split_sync_interval_ss_customer'
|
||||
WHERE `name` = 'split_sync_interval_ss_custome'
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT `id` FROM `fa_config` WHERE `name` = 'split_sync_interval_ss_customer') AS `t`);
|
||||
|
||||
UPDATE `fa_config` AS `correct`
|
||||
INNER JOIN `fa_config` AS `wrong` ON `wrong`.`name` = 'split_sync_interval_ss_custome'
|
||||
SET `correct`.`value` = `wrong`.`value`
|
||||
WHERE `correct`.`name` = 'split_sync_interval_ss_customer';
|
||||
|
||||
DELETE FROM `fa_config` WHERE `name` = 'split_sync_interval_ss_custome';
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+7
@@ -71,6 +71,13 @@ WHERE m.name = 'split.number' AND m.ismenu = 1
|
||||
AND NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'split.number/batchupdate' LIMIT 1)
|
||||
LIMIT 1;
|
||||
|
||||
INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`)
|
||||
SELECT 'file', m.id, 'split.number/batchoperate', '批量操作号码', 'fa fa-circle-o', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal'
|
||||
FROM `fa_auth_rule` m
|
||||
WHERE m.name = 'split.number' AND m.ismenu = 1
|
||||
AND NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'split.number/batchoperate' LIMIT 1)
|
||||
LIMIT 1;
|
||||
|
||||
INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`)
|
||||
SELECT 'file', m.id, 'split.number/multi', '批量更新', 'fa fa-circle-o', '', '列表状态开关', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal'
|
||||
FROM `fa_auth_rule` m
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+15
-2
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\admin\command;
|
||||
|
||||
use app\common\service\SplitCronLockService;
|
||||
use app\common\service\SplitTicketSyncLogger;
|
||||
use app\common\service\SplitTicketSyncService;
|
||||
use think\console\Command;
|
||||
@@ -51,8 +52,20 @@ class SplitSyncTickets extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
$count = $service->syncDueTickets();
|
||||
$output->writeln('<info>本次处理工单数: ' . $count . '</info>');
|
||||
$cronLock = new SplitCronLockService();
|
||||
if (!$cronLock->acquire()) {
|
||||
SplitTicketSyncLogger::log('cli', 'cron lock busy, skip');
|
||||
$output->writeln('<comment>上一轮同步仍在执行,跳过本次</comment>');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$count = $service->syncDueTickets();
|
||||
$output->writeln('<info>本次处理工单数: ' . $count . '</info>');
|
||||
} finally {
|
||||
$cronLock->release();
|
||||
}
|
||||
|
||||
if (SplitTicketSyncLogger::isEnabled()) {
|
||||
$output->writeln('<comment>调试日志已写入 runtime/log/split_sync.log</comment>');
|
||||
}
|
||||
|
||||
Regular → Executable
+25
@@ -64,10 +64,35 @@ class Link extends Backend
|
||||
$this->assignconfig('ipProtectList', $this->model->getIpProtectList());
|
||||
$this->assignconfig('randomShuffleList', $this->model->getRandomShuffleList());
|
||||
$this->assignconfig('statusList', $this->model->getStatusList());
|
||||
$this->assignconfig('linkFilterList', $this->buildLinkFilterList());
|
||||
|
||||
$this->setupPatchFrontend();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分流链接列表筛选下拉(id => 链接码 - 描述)
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function buildLinkFilterList(): array
|
||||
{
|
||||
$query = $this->model->order('id', 'desc');
|
||||
if ($this->dataLimit) {
|
||||
$adminIds = $this->getDataLimitAdminIds();
|
||||
if (is_array($adminIds)) {
|
||||
$query->where('admin_id', 'in', $adminIds);
|
||||
}
|
||||
}
|
||||
$list = [];
|
||||
foreach ($query->field('id,link_code,description')->select() as $row) {
|
||||
$code = (string) $row['link_code'];
|
||||
$desc = (string) $row['description'];
|
||||
$label = $desc !== '' ? $code . ' - ' . $desc : $code;
|
||||
$list[(string) $row['id']] = $label;
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 未部署 JS 到 public 时,或 patches 版本较新时,jsname 指向 script 接口
|
||||
*/
|
||||
|
||||
Regular → Executable
+130
@@ -29,6 +29,9 @@ class Number extends Backend
|
||||
|
||||
protected $dataLimit = 'personal';
|
||||
|
||||
/** 关联 splitLink 预载入时需为筛选/排序字段加表别名,避免 status 等列歧义 */
|
||||
protected $relationSearch = true;
|
||||
|
||||
protected $modelValidate = true;
|
||||
|
||||
protected $modelSceneValidate = true;
|
||||
@@ -62,6 +65,8 @@ class Number extends Backend
|
||||
$this->assignconfig('statusList', $this->model->getStatusList());
|
||||
$this->assignconfig('manualManageList', $this->model->getManualManageList());
|
||||
$this->assignconfig('platformStatusList', $this->model->getPlatformStatusList());
|
||||
$this->assignconfig('splitLinkFilterList', $this->buildNumberSplitLinkFilterList());
|
||||
$this->assignconfig('splitLinkSelectList', $this->buildSplitLinkSelectConfig());
|
||||
|
||||
$this->setupPatchFrontend();
|
||||
}
|
||||
@@ -111,6 +116,52 @@ class Number extends Backend
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 已有号码关联的分流链接(供列表筛选下拉)
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function buildNumberSplitLinkFilterList(): array
|
||||
{
|
||||
$numberTable = $this->model->getTable();
|
||||
$linkTable = (new LinkModel())->getTable();
|
||||
$query = Db::table($numberTable)
|
||||
->alias('n')
|
||||
->join($linkTable . ' l', 'n.split_link_id = l.id')
|
||||
->where('n.split_link_id', '>', 0)
|
||||
->group('n.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('n.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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全部分流链接(供批量操作弹窗可搜索下拉)
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function buildSplitLinkSelectConfig(): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($this->buildSplitLinkList() as $row) {
|
||||
$list[(string) $row['id']] = (string) $row['label'];
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
private function fetchPatch(string $template): string
|
||||
{
|
||||
$patchFile = ROOT_PATH . self::PATCH_VIEW_DIR . $template . '.html';
|
||||
@@ -395,6 +446,85 @@ class Number extends Backend
|
||||
$this->success(__('Batch update success'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按分流链接 + 号码文本批量开启/关闭/删除
|
||||
*/
|
||||
public function batchoperate(): void
|
||||
{
|
||||
if (false === $this->request->isPost()) {
|
||||
$this->error(__('Invalid parameters'));
|
||||
}
|
||||
|
||||
$splitLinkId = (int) $this->request->post('split_link_id', 0);
|
||||
$numbersText = (string) $this->request->post('numbers', '');
|
||||
$action = (string) $this->request->post('action', '');
|
||||
|
||||
if ($splitLinkId <= 0) {
|
||||
$this->error(__('Please select split link'));
|
||||
}
|
||||
|
||||
$numberList = NumberModel::parseNumbersText($numbersText);
|
||||
if ($numberList === []) {
|
||||
$this->error(__('Please fill at least one number'));
|
||||
}
|
||||
|
||||
$allowedActions = ['enable', 'disable', 'delete'];
|
||||
if (!in_array($action, $allowedActions, true)) {
|
||||
$this->error(__('Invalid batch operate action'));
|
||||
}
|
||||
|
||||
$linkQuery = (new LinkModel())->where('id', $splitLinkId)->where('status', 'normal');
|
||||
if ($this->dataLimit) {
|
||||
$adminIds = $this->getDataLimitAdminIds();
|
||||
if (is_array($adminIds)) {
|
||||
$linkQuery->where('admin_id', 'in', $adminIds);
|
||||
}
|
||||
}
|
||||
if (!$linkQuery->find()) {
|
||||
$this->error(__('No Results were found'));
|
||||
}
|
||||
|
||||
$query = $this->model
|
||||
->where('split_link_id', $splitLinkId)
|
||||
->where('number', 'in', $numberList);
|
||||
if ($this->dataLimit) {
|
||||
$adminIds = $this->getDataLimitAdminIds();
|
||||
if (is_array($adminIds)) {
|
||||
$query->where('admin_id', 'in', $adminIds);
|
||||
}
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
Db::startTrans();
|
||||
try {
|
||||
if ($action === 'delete') {
|
||||
$rows = $query->select();
|
||||
foreach ($rows as $row) {
|
||||
if ($row->delete()) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$status = $action === 'enable' ? 'normal' : 'hidden';
|
||||
$manualManage = $action === 'enable' ? 0 : 1;
|
||||
$count = (int) $query->update([
|
||||
'status' => $status,
|
||||
'manual_manage' => $manualManage,
|
||||
]);
|
||||
}
|
||||
Db::commit();
|
||||
} catch (PDOException|Exception $e) {
|
||||
Db::rollback();
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
|
||||
if ($count <= 0) {
|
||||
$this->error(__('No matching numbers found'));
|
||||
}
|
||||
|
||||
$this->success(sprintf(__('Batch operate success'), $count));
|
||||
}
|
||||
|
||||
/**
|
||||
* 排除不可由表单提交的字段
|
||||
*
|
||||
|
||||
Regular → Executable
+129
-1
@@ -58,6 +58,7 @@ class Ticket extends Backend
|
||||
$this->assignconfig('ticketTypeList', $this->model->getTicketTypeList());
|
||||
$this->assignconfig('numberTypeList', $this->model->getNumberTypeList());
|
||||
$this->assignconfig('statusList', $this->model->getStatusList());
|
||||
$this->assignconfig('splitLinkFilterList', $this->buildTicketSplitLinkFilterList());
|
||||
$this->assignconfig([
|
||||
'syncConfirmMsg' => __('Sync confirm'),
|
||||
'syncBackgroundStartedMsg' => __('Sync background started'),
|
||||
@@ -142,6 +143,71 @@ class Ticket extends Backend
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 已有工单关联的分流链接(供列表筛选下拉)
|
||||
*
|
||||
* @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, '.', '') . '%',
|
||||
];
|
||||
}
|
||||
|
||||
private function fetchPatch(string $template): string
|
||||
{
|
||||
$patchFile = ROOT_PATH . self::PATCH_VIEW_DIR . $template . '.html';
|
||||
@@ -179,7 +245,11 @@ class Ticket extends Backend
|
||||
->where($where)
|
||||
->order($sort, $order)
|
||||
->paginate($limit);
|
||||
$result = ['total' => $list->total(), 'rows' => $list->items()];
|
||||
$result = [
|
||||
'total' => $list->total(),
|
||||
'rows' => $list->items(),
|
||||
'summary' => $this->buildListSummary($where),
|
||||
];
|
||||
return json($result);
|
||||
}
|
||||
|
||||
@@ -331,6 +401,10 @@ class Ticket extends Backend
|
||||
public function add()
|
||||
{
|
||||
if (false === $this->request->isPost()) {
|
||||
$copyRow = $this->buildCopyRowData();
|
||||
if ($copyRow !== null) {
|
||||
$this->view->assign('row', $copyRow);
|
||||
}
|
||||
return $this->fetchPatch('add');
|
||||
}
|
||||
$params = $this->request->post('row/a', []);
|
||||
@@ -367,6 +441,60 @@ class Ticket extends Backend
|
||||
$this->success('', null, ['id' => (int) $this->model->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $ids
|
||||
* @return string
|
||||
|
||||
Regular → Executable
Regular → Executable
+1
-1
@@ -37,7 +37,7 @@ return [
|
||||
'System config' => '系统配置',
|
||||
'Auto reply' => '自动回复',
|
||||
'Reply statements' => '回复语句',
|
||||
'Reply statements tip'=> '一行填写一条回复语句,保存后与当前分流链接关联',
|
||||
'Reply statements tip'=> '一行填写一条回复语句,保存后与当前分流链接关联;跳转 WhatsApp / Telegram 时随机附加为预填消息',
|
||||
'Auto reply saved' => '自动回复已保存',
|
||||
'Reply statements column' => '回复语',
|
||||
'NS' => 'NS',
|
||||
|
||||
Regular → Executable
+12
@@ -30,6 +30,18 @@ return [
|
||||
'Batch selected count' => '已选号码',
|
||||
'Batch update status' => '更新状态',
|
||||
'Batch update btn' => '更新状态',
|
||||
'Batch operate btn' => '批量操作',
|
||||
'Batch operate title' => '批量操作',
|
||||
'Batch operate action' => '操作',
|
||||
'Batch operate enable' => '开启状态',
|
||||
'Batch operate disable' => '关闭状态',
|
||||
'Batch operate delete' => '删除号码',
|
||||
'Batch operate success' => '批量操作成功,共处理 %d 条',
|
||||
'Invalid batch operate action' => '操作类型无效',
|
||||
'No matching numbers found' => '未找到匹配的号码',
|
||||
'Please select split link' => '请选择分流链接',
|
||||
'Numbers one per line' => '一行一个号码',
|
||||
'Batch operate delete confirm' => '确定要删除这些号码吗?',
|
||||
'Please fill at least one number' => '请至少填写一个有效号码',
|
||||
'All numbers already exist for this link' => '该链接下号码均已存在,未新增任何记录',
|
||||
'Number already exists for this link' => '该链接下已存在相同号码',
|
||||
|
||||
Regular → Executable
+3
@@ -38,6 +38,8 @@ return [
|
||||
'Sync display pending' => '待同步',
|
||||
'Sync display error' => '同步异常',
|
||||
'Createtime' => '创建时间',
|
||||
'Copy' => '拷贝',
|
||||
'Summary row' => '汇总',
|
||||
'Section basic' => '基础信息',
|
||||
'Section time rule' => '时间与规则',
|
||||
'Section account' => '账号信息',
|
||||
@@ -51,6 +53,7 @@ return [
|
||||
'Ticket type yifafa' => '译发发云控',
|
||||
'Ticket type a2c' => 'A2C云控',
|
||||
'Ticket type ceo_scrm' => 'CEO SCRM',
|
||||
'Ticket type chatknow' => 'Chatknow SCRM',
|
||||
'Ticket type whatshub' => 'Whatshub云控',
|
||||
'Ticket type sihai' => '四海云控',
|
||||
'End time must after start' => '到期时间必须晚于开始时间',
|
||||
|
||||
Regular → Executable
Regular → Executable
+1
-1
@@ -98,7 +98,7 @@ class Number extends Model
|
||||
*/
|
||||
public function splitLink()
|
||||
{
|
||||
return $this->belongsTo(Link::class, 'split_link_id', 'id', [], 'LEFT')->setEagerlyType(0);
|
||||
return $this->belongsTo(Link::class, 'split_link_id', 'id', [], 'LEFT')->setEagerlyType(1);
|
||||
}
|
||||
|
||||
public function setNumberTypeCustomAttr($value): string
|
||||
|
||||
Regular → Executable
+1
@@ -50,6 +50,7 @@ class Ticket extends Model
|
||||
'yifafa' => __('Ticket type yifafa'),
|
||||
'a2c' => __('Ticket type a2c'),
|
||||
'ceo_scrm' => __('Ticket type ceo_scrm'),
|
||||
'chatknow' => __('Ticket type chatknow'),
|
||||
'whatshub' => __('Ticket type whatshub'),
|
||||
'sihai' => __('Ticket type sihai'),
|
||||
];
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+1
@@ -18,6 +18,7 @@
|
||||
<a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('split.number/edit')?'':'hide'}" title="{:__('Edit')}"><i class="fa fa-pencil"></i> {:__('Edit')}</a>
|
||||
<a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('split.number/del')?'':'hide'}" title="{:__('Delete')}"><i class="fa fa-trash"></i> {:__('Delete')}</a>
|
||||
<a href="javascript:;" class="btn btn-warning btn-batch-update-status btn-disabled disabled {:$auth->check('split.number/batchupdate')?'':'hide'}" title="{:__('Batch update btn')}"><i class="fa fa-edit"></i> {:__('Batch update btn')}</a>
|
||||
<a href="javascript:;" class="btn btn-info btn-batch-operate {:$auth->check('split.number/batchoperate')?'':'hide'}" title="{:__('Batch operate btn')}"><i class="fa fa-list-alt"></i> {:__('Batch operate btn')}</a>
|
||||
</div>
|
||||
<table id="table" class="table table-striped table-bordered table-hover table-nowrap"
|
||||
data-operate-edit="{:$auth->check('split.number/edit')}"
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+51
@@ -17,12 +17,56 @@
|
||||
text-align: left;
|
||||
animation: split-ticket-sync-dots 1.4s steps(4, end) infinite;
|
||||
}
|
||||
.split-ticket-url-group {
|
||||
width: 200px;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.split-ticket-url-group .form-control {
|
||||
cursor: default;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
background: #fff;
|
||||
height: 30px;
|
||||
}
|
||||
.split-ticket-url-group .btn {
|
||||
height: 30px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
@keyframes split-ticket-sync-dots {
|
||||
0%, 20% { content: ''; }
|
||||
40% { content: '.'; }
|
||||
60% { content: '..'; }
|
||||
80%, 100% { content: '...'; }
|
||||
}
|
||||
.split-ticket-summary-bar {
|
||||
margin: 0 0 10px;
|
||||
padding: 10px 14px;
|
||||
background: linear-gradient(180deg, #eff6ff 0%, #dbeafe 100%);
|
||||
border: 1px solid #93c5fd;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px 24px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.split-ticket-summary-bar .split-ticket-summary-label {
|
||||
font-weight: 700;
|
||||
color: #1e40af;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.split-ticket-summary-bar .split-ticket-summary-item em {
|
||||
font-style: normal;
|
||||
color: #475569;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.split-ticket-summary-bar .split-ticket-summary-item b {
|
||||
font-size: 14px;
|
||||
color: #0f172a;
|
||||
}
|
||||
</style>
|
||||
<div class="panel panel-default panel-intro">
|
||||
<div class="panel-heading">
|
||||
@@ -39,6 +83,13 @@
|
||||
<a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('split.ticket/del')?'':'hide'}" title="{:__('Delete')}"><i class="fa fa-trash"></i> {:__('Delete')}</a>
|
||||
<a href="javascript:;" class="btn btn-info btn-sync btn-disabled disabled {:$auth->check('split.ticket/sync')?'':'hide'}" title="{:__('Sync_status_btn')}"><i class="fa fa-refresh"></i> {:__('Sync_status_btn')}</a>
|
||||
</div>
|
||||
<div id="split-ticket-summary" class="split-ticket-summary-bar hide">
|
||||
<span class="split-ticket-summary-label">{:__('Summary row')}</span>
|
||||
<span class="split-ticket-summary-item"><em>{:__('Ticket_total')}</em><b data-sum="ticket_total">0</b></span>
|
||||
<span class="split-ticket-summary-item"><em>{:__('Complete_count')}</em><b data-sum="complete_count">0</b></span>
|
||||
<span class="split-ticket-summary-item"><em>{:__('Ticket_progress')}</em><b data-sum="ticket_progress_text">0.00%</b></span>
|
||||
<span class="split-ticket-summary-item"><em>{:__('Speed_per_hour')}</em><b data-sum="speed_per_hour">0.00</b></span>
|
||||
</div>
|
||||
<table id="table" class="table table-striped table-bordered table-hover table-nowrap"
|
||||
data-operate-edit="{:$auth->check('split.ticket/edit')}"
|
||||
data-operate-del="{:$auth->check('split.ticket/del')}"
|
||||
|
||||
Reference in New Issue
Block a user