完整版V1 加入爬虫功能

This commit is contained in:
root
2026-06-09 03:36:30 +08:00
parent 34d76cce74
commit a68b83fcbd
69 changed files with 5058 additions and 56 deletions
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace app\admin\command;
use app\common\service\SplitTicketSyncService;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
/**
* 工单云控定时同步 CLI
*
* 用法:
* php think split:sync-tickets
* php think split:sync-tickets --ticket=12
*/
class SplitSyncTickets extends Command
{
protected function configure(): void
{
$this->setName('split:sync-tickets')
->addOption('ticket', 't', Option::VALUE_OPTIONAL, '指定工单 ID(强制同步,忽略周期)', '')
->setDescription('同步分流工单云控数据');
}
protected function execute(Input $input, Output $output): void
{
set_time_limit(0);
$service = new SplitTicketSyncService();
$ticketId = trim((string) $input->getOption('ticket'));
if ($ticketId !== '' && ctype_digit($ticketId)) {
$result = $service->syncOne((int) $ticketId, true);
if (!empty($result['skipped'])) {
$output->writeln('<comment>跳过: ' . ($result['message'] ?? '') . '</comment>');
return;
}
if ($result['success']) {
$output->writeln('<info>工单 #' . $ticketId . ' 同步成功</info>');
} else {
$output->writeln('<error>工单 #' . $ticketId . ' 同步失败: ' . ($result['message'] ?? '') . '</error>');
}
return;
}
$count = $service->syncDueTickets();
$output->writeln('<info>本次处理工单数: ' . $count . '</info>');
}
}
@@ -58,6 +58,7 @@ class Number extends Backend
$this->assignconfig('numberTypeList', $this->model->getNumberTypeList());
$this->assignconfig('statusList', $this->model->getStatusList());
$this->assignconfig('manualManageList', $this->model->getManualManageList());
$this->assignconfig('platformStatusList', $this->model->getPlatformStatusList());
$this->setupPatchFrontend();
}
+116 -8
View File
@@ -7,7 +7,11 @@ 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\SplitTicketSyncService;
use think\Db;
use think\Lang;
use think\Loader;
use think\Exception;
use think\exception\DbException;
use think\exception\PDOException;
@@ -45,11 +49,6 @@ class Ticket extends Backend
$lang = $this->request->langset();
$lang = preg_match('/^([a-zA-Z\-_]{2,10})$/i', $lang) ? $lang : 'zh-cn';
$langFile = ROOT_PATH . 'patches/application/admin/lang/' . $lang . '/split/ticket.php';
if (is_file($langFile)) {
\think\Lang::load($langFile);
}
$this->model = new \app\admin\model\split\Ticket();
$this->view->assign('ticketTypeList', $this->model->getTicketTypeList());
$this->view->assign('numberTypeList', $this->model->getNumberTypeList());
@@ -62,6 +61,32 @@ class Ticket extends Backend
$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 接口
*/
@@ -146,10 +171,9 @@ class Ticket extends Backend
[$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 . '.*')
->fieldRaw($clickSub)
->with(['splitLink'])
->field($ticketTable . '.*,' . $clickSub)
->where($where)
->order($sort, $order)
->paginate($limit);
@@ -177,11 +201,95 @@ class Ticket extends Backend
$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'));
}
$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();
$rows = $this->model->where($pk, 'in', $ids)->select();
parent::multi($ids);
foreach ($rows as $row) {
$fresh = $this->model->get($row['id']);
if ($fresh) {
$ruleService->syncNumbersWithTicketStatus($fresh);
}
}
return;
}
parent::multi($ids);
}
/**
* @return string
* @throws DbException
@@ -2,7 +2,7 @@
return [
'Id' => '号码ID',
'Link_url' => '链接URL',
'Link_url' => '分流链接',
'Ticket_name' => '工单名称',
'Number' => '号码',
'Numbers' => '号码',
@@ -13,6 +13,10 @@ return [
'Inbound_count' => '进线人数',
'Status' => '状态',
'Manual_manage' => '手动管理',
'Platform_status' => '平台状态',
'Platform status online' => '在线',
'Platform status offline' => '离线',
'Platform status unknown' => '未知',
'Createtime' => '创建时间',
'Section basic' => '基础信息',
'Status normal' => '正常',
@@ -24,6 +24,9 @@ return [
'Number_count' => '号码数量',
'Number_count_detail' => '离线 %s / 封号 %s',
'Sync_status' => '同步状态',
'Sync_status_btn' => '同步状态',
'Sync running' => '正在同步,请稍候…',
'Sync done' => '同步完成',
'Sync status success' => '同步成功',
'Sync status error' => '同步异常',
'Sync status pending' => '待同步',
+26 -4
View File
@@ -26,6 +26,7 @@ class Number extends Model
'link_url_text',
'status_text',
'manual_manage_text',
'platform_status_text',
];
/**
@@ -65,6 +66,18 @@ class Number extends Model
];
}
/**
* @return array<string, string>
*/
public function getPlatformStatusList(): array
{
return [
'online' => __('Platform status online'),
'offline' => __('Platform status offline'),
'unknown' => __('Platform status unknown'),
];
}
/**
* 关联分流链接
*/
@@ -94,17 +107,19 @@ class Number extends Model
return $list[$type] ?? $type;
}
/**
* 列表展示用:仅显示分流链接码(非完整 URL)
*/
public function getLinkUrlTextAttr($value, $data): string
{
if ($value !== '' && $value !== null) {
return (string) $value;
if (isset($data['split_link']) && is_array($data['split_link'])) {
return (string) ($data['split_link']['link_code'] ?? '');
}
$linkId = (int) ($data['split_link_id'] ?? 0);
if ($linkId <= 0) {
return '';
}
$code = Link::where('id', $linkId)->value('link_code');
return self::buildLinkUrl((string) $code);
return (string) Link::where('id', $linkId)->value('link_code');
}
public function getStatusTextAttr($value, $data): string
@@ -121,6 +136,13 @@ class Number extends Model
return $list[$key] ?? $key;
}
public function getPlatformStatusTextAttr($value, $data): string
{
$key = (string) ($data['platform_status'] ?? 'unknown');
$list = $this->getPlatformStatusList();
return $list[$key] ?? $key;
}
/**
* 根据链接码生成完整分流 URL
*/
+3 -2
View File
@@ -98,7 +98,8 @@ class Ticket extends Model
*/
public function splitLink()
{
return $this->belongsTo(Link::class, 'split_link_id', 'id', [], 'LEFT')->setEagerlyType(0);
// IN 预载入:避免 eagerlyType=0(JOIN) 与列表 field 子查询冲突导致 SQL 1064
return $this->belongsTo(Link::class, 'split_link_id', 'id', [], 'LEFT')->setEagerlyType(1);
}
/**
@@ -126,7 +127,7 @@ class Ticket extends Model
$total = (int) ($data['ticket_total'] ?? 0);
$complete = (int) ($data['complete_count'] ?? 0);
if ($total <= 0) {
return '';
return '0%';
}
$percent = round($complete / $total * 100, 2);
return $percent . '%';
@@ -11,6 +11,7 @@
<a href="javascript:;" class="btn btn-success btn-add {:$auth->check('split.ticket/add')?'':'hide'}" title="{:__('Add')}"><i class="fa fa-plus"></i> {:__('Add')}</a>
<a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('split.ticket/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.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>
<table id="table" class="table table-striped table-bordered table-hover table-nowrap"
data-operate-edit="{:$auth->check('split.ticket/edit')}"